WIP: translations #69

Closed
fenopy wants to merge 7 commits from (deleted):translations into master
14 changed files with 751 additions and 58 deletions

View File

@@ -0,0 +1,82 @@
name: "Bug Report"
about: "Report a problem or error"
title: "[BUG] "
labels:
- bug
body:
- type: dropdown
id: os
attributes:
label: "Operating System"
options:
- "Windows 10"
- "Windows 11"
- "Ubuntu / Debian"
- "Fedora / RHEL"
- "Arch Linux"
- "macOS"
- "Other"
validations:
required: true
- type: dropdown
id: install_type
attributes:
label: "Installation Type"
options:
- "Docker / Docker Compose"
- "EXE / Binary (Windows)"
- "Binary (Linux)"
validations:
required: true
- type: dropdown
id: slicer
attributes:
label: "Slicer"
options:
- "OrcaSlicer (KX-Patch)"
- "OrcaSlicer (Standard)"
- "BambuStudio"
- "PrusaSlicer"
- "Other"
validations:
required: true
- type: input
id: slicer_version
attributes:
label: "Slicer Version"
placeholder: "e.g. 2.4.0-dev-kx1"
validations:
required: true
- type: input
id: bridge_version
attributes:
label: "KX-Bridge Version"
placeholder: "e.g. v0.9.20"
description: "Found in the web interface or in the logs at startup."
validations:
required: true
- type: textarea
id: description
attributes:
label: "Problem Description"
description: "What is happening? What did you expect to happen?"
placeholder: |
What happened:
What was expected:
validations:
required: true
- type: textarea
id: logs
attributes:
label: "Logs / Error Message"
description: "Relevant output from the KX-Bridge logs. Docker: `docker logs <container>`"
render: text
validations:
required: false

View File

@@ -0,0 +1,85 @@
name: Bug Report
about: File a bug report
title: "[Bug]: "
body:
- type: markdown
attributes:
value: |
Please fill out all required fields to help us resolve your issue faster.
- type: dropdown
id: os
attributes:
label: Operating System
options:
- Windows 10
- Windows 11
- Ubuntu / Debian
- Fedora / RHEL
- Arch Linux
- macOS
- Other
validations:
required: true
- type: dropdown
id: install_type
attributes:
label: Installation Type
options:
- Docker / Docker Compose
- EXE / Binary (Windows)
- Binary (Linux)
validations:
required: true
- type: dropdown
id: slicer
attributes:
label: Slicer
options:
- OrcaSlicer (KX-Patch)
- OrcaSlicer (Standard)
- BambuStudio
- PrusaSlicer
- Other
validations:
required: true
- type: input
id: slicer_version
attributes:
label: Slicer Version
placeholder: "e.g. 2.4.0-dev-kx1"
validations:
required: true
- type: input
id: bridge_version
attributes:
label: KX-Bridge Version
description: "Found in the web interface or in the logs at startup."
placeholder: "e.g. v0.9.20"
validations:
required: true
- type: textarea
id: description
attributes:
label: Problem Description
description: What is happening? What did you expect to happen?
placeholder: |
What happened:
What was expected:
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs / Error Message
description: "Relevant output from the KX-Bridge logs. Docker: `docker logs <container>`"
render: shell
validations:
required: false

82
bug_report.yml Normal file
View File

@@ -0,0 +1,82 @@
name: "Bug Report"
about: "Problem oder Fehler melden"
title: "[BUG] "
labels:
- bug
body:
- type: dropdown
id: os
attributes:
label: "Betriebssystem"
options:
- "Windows 10"
- "Windows 11"
- "Ubuntu / Debian"
- "Fedora / RHEL"
- "Arch Linux"
- "macOS"
- "Anderes"
validations:
required: true
- type: dropdown
id: install_type
attributes:
label: "Installationsart"
options:
- "Docker / Docker Compose"
- "EXE / Binary (Windows)"
- "Binary (Linux)"
validations:
required: true
- type: dropdown
id: slicer
attributes:
label: "Slicer"
options:
- "OrcaSlicer (KX-Patch)"
- "OrcaSlicer (Standard)"
- "BambuStudio"
- "PrusaSlicer"
- "Anderer"
validations:
required: true
- type: input
id: slicer_version
attributes:
label: "Slicer Version"
placeholder: "z.B. 2.4.0-dev-kx1"
validations:
required: true
- type: input
id: bridge_version
attributes:
label: "KX-Bridge Version"
placeholder: "z.B. v0.9.20"
description: "Zu finden im Webinterface oder in den Logs beim Start."
validations:
required: true
- type: textarea
id: description
attributes:
label: "Problembeschreibung"
description: "Was passiert? Was hast du erwartet?"
placeholder: |
Was ist passiert:
Was wurde erwartet:
validations:
required: true
- type: textarea
id: logs
attributes:
label: "Logs / Fehlermeldung"
description: "Relevante Ausgabe aus den KX-Bridge Logs. Docker: `docker logs <container>`"
render: text
validations:
required: false

View File

@@ -34,3 +34,11 @@ auto_leveling = 1
[bridge]
# Poll-Intervall in Sekunden
poll_interval = 3
[spoolman]
# URL der Spoolman-Instanz (leer lassen um Spoolman zu deaktivieren)
# server = http://192.168.x.x:7912
# Wie oft (Sekunden) der Filamentverbrauch während des Drucks gemeldet wird
# (0 = nur beim Druckende)
# sync_rate = 0

View File

@@ -13,6 +13,7 @@ _BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) els
CONFIG_SECTION_CONNECTION = "connection"
CONFIG_SECTION_PRINT = "print"
CONFIG_SECTION_BRIDGE = "bridge"
CONFIG_SECTION_SPOOLMAN = "spoolman"
def _find_config_file() -> pathlib.Path | None:
@@ -63,6 +64,8 @@ def _load_config_file(path: pathlib.Path):
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
"SPOOLMAN_SERVER": (CONFIG_SECTION_SPOOLMAN, "server"),
"SPOOLMAN_SYNC_RATE": (CONFIG_SECTION_SPOOLMAN, "sync_rate"),
}
for env_key, (section, option) in mapping.items():
if env_key not in os.environ:
@@ -317,3 +320,5 @@ AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
SPOOLMAN_SERVER = get("SPOOLMAN_SERVER", "")
SPOOLMAN_SYNC_RATE = int(get("SPOOLMAN_SYNC_RATE", "0"))

View File

@@ -733,6 +733,40 @@ class CameraCache:
await asyncio.sleep(2.0)
class SpoolmanClient:
"""Thin synchronous HTTP client for Spoolman filament tracking.
Designed to be called from daemon threads (poll loop, _on_print callbacks).
Uses requests (already in requirements) so no event-loop dependency.
"""
def __init__(self, server_url: str, sync_rate: int = 0):
self.server_url = server_url.rstrip("/")
self.sync_rate = sync_rate
def _req(self, method: str, path: str, **kwargs):
import requests
r = requests.request(method, f"{self.server_url}{path}", timeout=5, **kwargs)
r.raise_for_status()
return r.json()
def health_check(self) -> bool:
try:
self._req("GET", "/api/v1/health")
return True
except Exception:
return False
def list_spools(self) -> list:
return self._req("GET", "/api/v1/spool")
def use_filament(self, spool_id: int, use_length_mm: float) -> None:
"""Report consumed filament length in mm. Spoolman converts to weight
using the spool's filament profile density."""
self._req("PUT", f"/api/v1/spool/{spool_id}/use",
json={"use_length": round(use_length_mm, 2)})
class KobraXBridge:
def __init__(self, client: KobraXClient, args=None, store=None, printer_id: str = "1", all_bridges=None):
self.client = client
@@ -793,6 +827,7 @@ class KobraXBridge:
"file_ready": "",
"print_start_dialog": getattr(args, "print_start_dialog", 1),
"filament_mode": "toolhead",
"supplies_usage": 0,
"ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None},
}
self._ams_slots: list[dict] = [] # flat global list; each entry has global_index + box_id
@@ -819,6 +854,18 @@ class KobraXBridge:
self._pending_preprint_skip: list[str] = []
self._pending_preprint_skip_deadline: float = 0.0
# Spoolman filament tracking
_sm_url = (getattr(args, "spoolman_server", "") or "").strip()
self._spoolman: SpoolmanClient | None = (
SpoolmanClient(_sm_url, getattr(args, "spoolman_sync_rate", 0))
if _sm_url else None
)
self._spoolman_slot_spools: dict[int, int] = {} # {ams_slot_idx: spoolman_spool_id}
self._spoolman_slot_usage: dict[int, float] = {} # per-slot accumulated mm this print
self._spoolman_slot_reported: dict[int, float] = {} # per-slot mm already sent to Spoolman
self._spoolman_last_usage: float = 0.0 # supplies_usage at last attribution tick
self._spoolman_last_sync: float = 0.0
# Theme-Name prüfen (keine Sonderzeichen oder Umlaute)
raw_theme = (getattr(args, "ui_theme", None) or "default").strip()
if not _UI_THEME_NAME_RE.match(raw_theme):
@@ -837,6 +884,146 @@ class KobraXBridge:
client.callbacks["light/report"] = self._on_light
client.callbacks["skip/report"] = self._on_skip
if self._spoolman:
threading.Thread(
target=lambda: log.info(
f"Spoolman: {'OK' if self._spoolman.health_check() else 'unreachable'} "
f"at {self._spoolman.server_url}"
),
daemon=True, name="spoolman-health",
).start()
# ── Spoolman helpers ──────────────────────────────────────────────────────
def _spoolman_filament_mm(self) -> float:
"""Total filament_used_mm for the current print file from the GCode DB."""
filename = self._state.get("filename", "")
if not filename:
return 0.0
try:
gf = self._store.get_file_by_name(filename)
return float(gf.get("filament_used_mm") or 0.0) if gf else 0.0
except Exception:
return 0.0
def _spoolman_attribute_tick(self, activity_map: dict) -> None:
"""Attribute the supplies_usage delta since last tick to the active slot.
Called every poll cycle after both print/report and multiColorBox/report
have been processed, so _state["supplies_usage"] and _ams_loaded_slot
are both current.
Skips attribution during loading/unloading transitions (tool changes +
purges) to avoid charging the wrong spool for purge material."""
if not self._spoolman or not self._spoolman_slot_spools:
return
if self._state.get("print_state") != "printing":
return
current = self._state.get("supplies_usage", 0)
delta = current - self._spoolman_last_usage
self._spoolman_last_usage = current
if delta <= 0:
return
loaded = self._ams_loaded_slot
if loaded < 0:
return
# Don't attribute during a loading/unloading transition
if activity_map.get(loaded):
return
self._spoolman_slot_usage[loaded] = self._spoolman_slot_usage.get(loaded, 0.0) + delta
def _spoolman_unreported(self) -> dict[int, float]:
"""Return {slot_idx: mm} of usage not yet reported to Spoolman.
Falls back to equal split of total supplies_usage when per-slot
attribution data is absent (e.g. single-extruder with no AMS)."""
total_used = self._state.get("supplies_usage", 0)
if self._spoolman_slot_usage:
return {
slot: self._spoolman_slot_usage.get(slot, 0.0)
- self._spoolman_slot_reported.get(slot, 0.0)
for slot in self._spoolman_slot_spools
}
# Fallback: equal split
n = len(self._spoolman_slot_spools)
already = sum(self._spoolman_slot_reported.values())
per = (total_used - already) / n if n else 0.0
return {slot: per for slot in self._spoolman_slot_spools}
def _spoolman_report(self, unreported: dict[int, float], min_mm: float = 0.1) -> None:
"""Fire-and-forget report of unreported mm to each mapped spool."""
sm = self._spoolman
for slot_idx, mm in unreported.items():
if mm < min_mm:
continue
spool_id = self._spoolman_slot_spools.get(slot_idx)
if not spool_id:
continue
self._spoolman_slot_reported[slot_idx] = (
self._spoolman_slot_reported.get(slot_idx, 0.0) + mm
)
def _send(sid=spool_id, length=mm):
try:
sm.use_filament(sid, length)
log.info(f"Spoolman: {length:.1f} mm → spool {sid}")
except Exception as e:
log.warning(f"Spoolman: report failed (spool {sid}): {e}")
threading.Thread(target=_send, daemon=True, name="spoolman-report").start()
def _spoolman_notify_end(self):
"""Report remaining filament on print end."""
if not self._spoolman or not self._spoolman_slot_spools:
return
self._spoolman_report(self._spoolman_unreported())
def _spoolman_sync_midprint(self):
"""Report incremental filament usage during a print (sync_rate interval)."""
if not self._spoolman or not self._spoolman_slot_spools:
return
self._spoolman_report(self._spoolman_unreported(), min_mm=10.0)
# ── Spoolman API handlers ─────────────────────────────────────────────────
async def handle_kx_spoolman_status(self, request):
"""GET /kx/spoolman/status"""
return self._json_cors({
"configured": bool(self._spoolman),
"server": self._spoolman.server_url if self._spoolman else "",
"sync_rate": self._spoolman.sync_rate if self._spoolman else 0,
"slot_spools": {str(k): v for k, v in self._spoolman_slot_spools.items()},
})
async def handle_kx_spoolman_spools(self, request):
"""GET /kx/spoolman/spools — proxied from Spoolman."""
if not self._spoolman:
return self._json_cors({"error": "Spoolman not configured"}, status=503)
try:
spools = await asyncio.get_event_loop().run_in_executor(
None, self._spoolman.list_spools
)
return self._json_cors({"spools": spools})
except Exception as e:
log.warning(f"Spoolman: list_spools failed: {e}")
return self._json_cors({"error": str(e)}, status=502)
async def handle_kx_spoolman_set_active(self, request):
"""POST /kx/spoolman/active-spool
Body: {"slot_map": {"0": 42, "2": 17}} — AMS slot index → Spoolman spool ID.
An empty slot_map clears all assignments."""
try:
data = await request.json()
except Exception:
return self._json_cors({"error": "invalid JSON"}, status=400)
slot_map = data.get("slot_map") or {}
self._spoolman_slot_spools = {
int(k): int(v) for k, v in slot_map.items()
if str(v).isdigit() and int(v) > 0
}
self._spoolman_slot_usage = {}
self._spoolman_slot_reported = {}
self._spoolman_last_usage = 0.0
return self._json_cors({"slot_spools": {str(k): v for k, v in self._spoolman_slot_spools.items()}})
def _default_ace_dry_presets(self) -> dict[str, dict]:
return {
"pla": {"temp": 45, "duration_sec": 4 * 3600},
@@ -951,15 +1138,21 @@ class KobraXBridge:
printer_id=self._printer_id,
)
log.info(f"Job started: {self._current_job_id} for {filename}")
self._spoolman_slot_usage = {}
self._spoolman_slot_reported = {}
self._spoolman_last_usage = 0.0
self._spoolman_last_sync = 0.0
# Job-History: Druckende erkennen
if kobra_state in ("finished",) and self._current_job_id:
self._store.finish_job(self._current_job_id, status="completed")
log.info(f"Job abgeschlossen: {self._current_job_id}")
self._spoolman_notify_end()
self._current_job_id = ""
elif kobra_state in ("stoped", "canceled") and self._current_job_id:
self._store.finish_job(self._current_job_id, status="cancelled")
log.info(f"Job abgebrochen: {self._current_job_id}")
self._spoolman_notify_end()
self._current_job_id = ""
# Nach Druckende das Upload-Banner verschwinden lassen (Issue #29): der
@@ -976,6 +1169,7 @@ class KobraXBridge:
self._state["slicer_time"] = 0
self._state["layer_height"] = 0.0
self._state["first_layer_height"] = 0.0
self._state["supplies_usage"] = 0
self._thumbnail_b64 = ""
self._state["filename"] = d.get("filename", self._state["filename"])
if "progress" in d:
@@ -990,6 +1184,8 @@ class KobraXBridge:
self._state["total_layers"] = d["total_layers"]
if "taskid" in d:
self._state["taskid"] = str(d["taskid"])
if "supplies_usage" in d:
self._state["supplies_usage"] = int(d["supplies_usage"])
settings = d.get("settings") or {}
if "print_speed_mode" in settings:
self._state["print_speed_mode"] = int(settings["print_speed_mode"])
@@ -4804,6 +5000,14 @@ class KobraXBridge:
print_r = self.client.publish("print", "query", timeout=3.0)
if print_r:
self._on_print(print_r)
# Spoolman mid-print sync
if (self._spoolman and self._spoolman.sync_rate > 0
and self._spoolman_slot_spools
and self._state.get("print_state") == "printing"):
now = time.time()
if now - self._spoolman_last_sync >= self._spoolman.sync_rate:
self._spoolman_sync_midprint()
self._spoolman_last_sync = now
box = self.client.query_multicolor_box()
if box:
data = box.get("data") or {}
@@ -4820,6 +5024,10 @@ class KobraXBridge:
if global_slots:
self._ams_slots = global_slots
self._ams_loaded_slot = global_loaded
self._spoolman_attribute_tick(activity_map)
else:
# No multiColorBox data — still attribute (no transitions to skip)
self._spoolman_attribute_tick({})
except Exception as e:
log.warning(f"Poll-Fehler: {e}")
# Prüfen ob Drucker wirklich weg ist
@@ -4954,6 +5162,9 @@ def build_app(bridge: KobraXBridge) -> web.Application:
r.add_post("/kx/skip", bridge.handle_kx_skip)
r.add_post("/kx/skip/query", bridge.handle_kx_skip_query)
r.add_get("/kx/skip/state", bridge.handle_kx_skip_state)
r.add_get("/kx/spoolman/status", bridge.handle_kx_spoolman_status)
r.add_get("/kx/spoolman/spools", bridge.handle_kx_spoolman_spools)
r.add_post("/kx/spoolman/active-spool", bridge.handle_kx_spoolman_set_active)
r.add_route("OPTIONS", "/kx/{path:.*}", bridge.handle_kx_options)
# Root + Printer-Routen (Single-Page, JS liest Pathname)
@@ -5119,6 +5330,10 @@ def main():
parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG)
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
parser.add_argument("--spoolman-server", default=env_loader.SPOOLMAN_SERVER,
help="Spoolman URL (e.g. http://192.168.x.x:7912); leave empty to disable")
parser.add_argument("--spoolman-sync-rate", type=int, default=env_loader.SPOOLMAN_SYNC_RATE,
help="Mid-print filament sync interval in seconds (0 = only on print end)")
parser.add_argument("--host", default="0.0.0.0",
help="Bind-Adresse für den Bridge-Server")

View File

@@ -44,6 +44,66 @@ var ACE_DRY_PRESETS={
custom_3:{name:'Custom 3',temp:45,duration_sec:4*3600}
};
// Spoolman state
var _spoolmanStatus={configured:false,server:'',sync_rate:0,slot_spools:{}};
var _spoolmanSpools=[];
var _slotSpoolMap={}; // {String(global_index): spoolman_spool_id} — last committed assignment
function _loadSpoolmanStatus(){
fetch(_apiUrl('/kx/spoolman/status')).then(function(r){return r.json();}).then(function(d){
_spoolmanStatus=d;
_slotSpoolMap=d.slot_spools||{};
}).catch(function(){});
}
function _buildSpoolmanSection(){
var sec=document.getElementById('fd-spoolman-section');
var rows=document.getElementById('fd-spoolman-rows');
var loading=document.getElementById('fd-spoolman-loading');
if(!sec||!rows)return;
if(!_spoolmanStatus.configured){sec.style.display='none';return;}
sec.style.display='';
rows.innerHTML='';
if(loading)loading.style.display='';
// Collect the unique AMS slots the user has selected in the paint→slot rows
var usedSlots={};
document.querySelectorAll('#fd-slots select').forEach(function(sel){
var idx=parseInt(sel.value);
if(idx>=0){
var slot=(_amsSlots||[]).find(function(s){return s.slot_index===idx;});
if(slot&&!usedSlots[idx])usedSlots[idx]=slot;
}
});
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){
if(loading)loading.style.display='none';
_spoolmanSpools=d.spools||[];
var slotKeys=Object.keys(usedSlots).map(Number).sort(function(a,b){return a-b;});
if(!slotKeys.length){rows.innerHTML='<span style="font-size:11px;color:var(--txt2)"></span>';return;}
rows.innerHTML=slotKeys.map(function(idx){
var slot=usedSlots[idx];
var col=(slot.color_hex||'#888');
var currentSpool=_slotSpoolMap[String(idx)]||'';
var opts='<option value=""></option>'+_spoolmanSpools.map(function(sp){
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor+' ':'';
var name=sp.filament&&sp.filament.name?sp.filament.name:'Spool';
return '<option value="'+sp.id+'"'+(sp.id==currentSpool?' selected':'')+'>'+
escHtml('#'+sp.id+' '+vendor+name+rem)+'</option>';
}).join('');
return '<div style="display:flex;align-items:center;gap:8px;font-size:12px">'+
'<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:'+col+';border:1px solid var(--border);flex-shrink:0"></span>'+
'<span style="color:var(--txt2);min-width:46px">Slot '+(idx+1)+'</span>'+
'<select data-spool-slot="'+idx+'" style="flex:1;padding:3px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:11px">'+opts+'</select>'+
'</div>';
}).join('');
}).catch(function(){
if(loading)loading.style.display='none';
if(rows)rows.innerHTML='<span style="font-size:11px;color:var(--err)">Spoolman unavailable</span>';
});
}
function _aceAutoRefillGet(aceId){return !!aceAutoRefillPrefs[String(aceId)];}
function _aceAutoRefillSet(aceId,on){
aceAutoRefillPrefs[String(aceId)]=!!on;
@@ -420,6 +480,16 @@ function applyLang(){
setText('file-ready-btn',T.file_ready_btn);
setText('file-slots-btn',T.file_slots_btn);
setText('file-cancel-btn',T.file_cancel_btn);
// Elements not yet covered by setText above
var settingsBtn=document.getElementById('settings-btn');
if(settingsBtn)settingsBtn.title=T.settings_btn_tooltip||T.settings_title||'Einstellungen';
var snpEl=document.getElementById('s-printer-name');
if(snpEl)snpEl.placeholder=T.settings_printer_name_placeholder||'z.B. Kobra X Links';
var sdidEl=document.getElementById('s-device-id');
if(sdidEl)sdidEl.placeholder=T.settings_device_id_placeholder||'32 Hex-Zeichen';
setText('d-fan-off',T.label_off||'Aus');
setText('skip-confirm',T.skip_confirm_btn||'Überspringen');
setText('ams-no-data',T.ams_no_data||'Keine AMS-Daten empfangen');
// GCode-Browser-Karten: Texte sind via innerHTML eingebacken,
// bei Sprachwechsel komplett neu rendern.
if(typeof renderStore==='function' && typeof storeFiles!=='undefined'){
@@ -482,6 +552,7 @@ function ensureAceDryCards(){
fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){
if(!d.result||!d.result.length){showPanel('printers');loadPrinterTab();}
}).catch(function(){});
_loadSpoolmanStatus();
});
})();
@@ -889,11 +960,16 @@ function applyState(){
var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':'');
vendorBadge='<div class="slot-label" style="font-size:9px;color:var(--accent);font-weight:600;margin-top:1px" title="'+tt+'">'+profile.vendor+'</div>';
}
var spoolId=_slotSpoolMap&&_slotSpoolMap[String(globalIdx)];
var spoolBadge=(!empty&&spoolId)
?'<div class="slot-label" style="font-size:9px;color:var(--ok);margin-top:1px" title="Spoolman spool #'+spoolId+'">🧵 #'+spoolId+'</div>'
:'';
html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'')
+'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
+'<div class="slot-circle" style="background:'+col+'"></div>'
+'<div class="slot-material" title="'+(empty?'':genericType)+'">'+materialLabel+'</div>'
+vendorBadge
+spoolBadge
+'<div class="slot-label">'+slotLabel+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
@@ -1169,7 +1245,7 @@ function refreshUserProfileList(){
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;border-bottom:1px solid var(--border)">'
+'<span>★ '+label+'</span>'
+'<button onclick="deleteUserProfile(\''+encodeURIComponent(p.vendor)+'\',\''+encodeURIComponent(p.name)+'\')" '
+'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="löschen">🗑</button>'
+'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="'+tr('btn_delete','Löschen')+'">🗑</button>'
+'</div>';
}).join('');
}).catch(function(){});
@@ -1206,7 +1282,7 @@ function refreshImportDialogList(){
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 6px;border-bottom:1px solid var(--border)">'
+'<span>★ '+label+'</span>'
+'<button onclick="deleteUserProfile(\''+encodeURIComponent(p.vendor)+'\',\''+encodeURIComponent(p.name)+'\')" '
+'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="löschen">🗑</button>'
+'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="'+tr('btn_delete','Löschen')+'">🗑</button>'
+'</div>';
}).join('');
}).catch(function(){});
@@ -1246,7 +1322,7 @@ function doProfileImportUpload(files){
_one(idx+1);
})
.catch(function(e){
status.textContent='Fehler: '+e;
status.textContent=tr('log_error','Fehler:')+' '+e;
status.style.color='var(--err)';
});
}
@@ -1404,7 +1480,7 @@ function startReadyFile(filename){
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
.catch(function(e){
clog(tr('log_error')+' '+e,'msg-err');
clog(tr('log_error','Fehler:')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
}
@@ -1532,7 +1608,7 @@ function saveSlotEdit(){
if(typeof applyState==='function') applyState();
if(typeof poll==='function') poll();
})
.catch(function(e){clog('Fehler: '+e,'msg-err');});
.catch(function(e){clog(tr('log_error','Fehler:')+' '+e,'msg-err');});
}
document.addEventListener('DOMContentLoaded',function(){
document.getElementById('s-printer-ip').addEventListener('input',function(){
@@ -1580,7 +1656,7 @@ function saveSettings(){
},4000);
}).catch(function(e){
btn.disabled=false;setText('btn-save-settings',T.settings_save);
clog('Settings-Fehler: '+e,'msg-err');
clog(T.settings_title+' '+tr('log_error','Fehler:')+' '+e,'msg-err');
});
}
function checkUpdate(){
@@ -1653,8 +1729,8 @@ var pollTimer;
// ── Print actions ──
function printAction(a){
post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()})
.catch(function(e){clog('Fehler: '+e,'msg-err')});
post('/printer/print/'+a,{}).then(function(){clog(tr('log_print_action','Druck:')+' '+a,'msg-ok');poll()})
.catch(function(e){clog(tr('log_error','Fehler:')+' '+e,'msg-err')});
}
function togglePauseResume(){
// Druckt → pause; Pausiert → resume. Status kommt aus dem zuletzt gepollten
@@ -1693,42 +1769,42 @@ function move(axis,dir,dist){
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
var axisMap={0:1,1:2,2:3};
post('/api/axis',{axis:axisMap[axis],move_type:1,distance:dir*dist})
.then(function(){clog('Achse '+(axis===0?'X':axis===1?'Y':'Z')+' '+(dir>0?'+':'')+dir*dist+'mm','msg-ok')})
.catch(function(e){clog('Achse-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('log_axis','Achse')+' '+(axis===0?'X':axis===1?'Y':'Z')+' '+(dir>0?'+':'')+dir*dist+'mm','msg-ok')})
.catch(function(e){clog(tr('log_axis','Achse')+'-'+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
function homeAll(){
post('/api/axis',{axis:5,move_type:2,distance:0})
.then(function(){clog('Home All','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('log_home_all','Home All'),'msg-ok')})
.catch(function(e){clog(tr('log_home_all','Home All')+' '+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
function homeXY(){
post('/api/axis',{axis:4,move_type:2,distance:0})
.then(function(){clog('Home XY','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('btn_home_xy','Home XY'),'msg-ok')})
.catch(function(e){clog(tr('btn_home_xy','Home XY')+' '+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
function homeZ(){
post('/api/axis',{axis:3,move_type:2,distance:0})
.then(function(){clog('Home Z','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('btn_home_z','Home Z'),'msg-ok')})
.catch(function(e){clog(tr('btn_home_z','Home Z')+' '+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
function disableMotors(){
post('/api/axis',{action:'turnOff'})
.then(function(){clog('Motors Off','msg-ok')})
.catch(function(e){clog('Motors-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('btn_disable_motors','Motors Off'),'msg-ok')})
.catch(function(e){clog(tr('btn_disable_motors','Motors Off')+' '+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
// ── Temperature ──
function setNozzle(){
var v=parseFloat(document.getElementById('p-nozzle-inp').value||0);
post('/api/temperature',{nozzle:v,bed:S.bed_target})
.then(function(){clog('Nozzle → '+v+'°C','msg-ok')})
.catch(function(e){clog('Temp-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('log_nozzle','Nozzle → ')+v+'°C','msg-ok')})
.catch(function(e){clog(tr('label_nozzle','Düse')+' '+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
function setBed(){
var v=parseFloat(document.getElementById('p-bed-inp').value||0);
post('/api/temperature',{nozzle:S.nozzle_target,bed:v})
.then(function(){clog(T.label_bed+' → '+v+'°C','msg-ok')})
.catch(function(e){clog('Temp-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('log_bed','Bett → ')+v+'°C','msg-ok')})
.catch(function(e){clog(tr('label_bed','Bett')+' '+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
// ── Light ──
@@ -1748,7 +1824,7 @@ function setSpeed(mode){
if(b) b.classList.toggle('spd-active',m===mode);
});
post('/api/speed',{mode:mode})
.catch(function(e){clog('Speed-Fehler: '+e,'msg-err')});
.catch(function(e){clog(tr('label_speed','Geschwindigkeit')+' '+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
// ── Fan ──
@@ -1756,15 +1832,15 @@ function setFan(){
var v=parseInt(document.getElementById('d-fan').value);
document.getElementById('d-fan-val').textContent=v;
post('/api/fan',{speed:v})
.then(function(){clog('Lüfter → '+v+'%','msg-ok')})
.catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('log_fan','Lüfter → ')+v+'%','msg-ok')})
.catch(function(e){clog(tr('log_fan','Lüfter → ')+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
function quickFan(v){
document.getElementById('d-fan').value=v;
document.getElementById('d-fan-val').textContent=v;
post('/api/fan',{speed:v})
.then(function(){clog('Lüfter → '+v+'%','msg-ok')})
.catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')});
.then(function(){clog(tr('log_fan','Lüfter → ')+v+'%','msg-ok')})
.catch(function(e){clog(tr('log_fan','Lüfter → ')+tr('log_error','Fehler:')+' '+e,'msg-err')});
}
// ── AMS ──
@@ -1779,7 +1855,7 @@ function amsFeed(type,slotIndex){
}
return post('/api/ams/feed',{slot_index:globalIdx,type:type})
.then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(globalIdx+1),'msg-ok')})
.catch(function(e){clog('AMS-Fehler: '+e,'msg-err');throw e;});
.catch(function(e){clog('AMS '+tr('log_error','Fehler:')+' '+e,'msg-err');throw e;});
}
// ── Camera ──
@@ -1805,14 +1881,14 @@ function camStart(){
ph.style.display='flex';
camOn=false;
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
clog(tr('log_error')+' '+tr('cam_stream_unavailable'),'msg-err');
clog(tr('log_error','Fehler:')+' '+tr('cam_stream_unavailable'),'msg-err');
};
img.src='/api/camera/stream?t='+Date.now();
}
}).catch(function(e){
sp.style.display='none';
ph.style.display='flex';
clog(tr('log_error')+' '+e,'msg-err');
clog(tr('log_error','Fehler:')+' '+e,'msg-err');
});
}
@@ -1844,7 +1920,7 @@ function aceDryStart(aceId){
clog('ACE '+(aceId+1)+' - '+tr('ace_dry_dryer')+': '+tr('ace_dry_start')+' ('+t+'°C, '+d+' min)','msg-ok');
poll();
})
.catch(function(e){clog('ACE-Fehler: '+e,'msg-err');});
.catch(function(e){clog('ACE '+tr('log_error','Fehler:')+' '+e,'msg-err');});
}
var _aceAutoFeedPending={};
@@ -2048,7 +2124,7 @@ function saveAceDryPresetAndRestart(){
}).catch(function(e){
btn.disabled=false;
btn.textContent=tr('ace_dry_dialog_save_restart');
clog('ACE-Preset Fehler: '+e,'msg-err');
clog('ACE preset '+tr('log_error','Fehler:')+' '+e,'msg-err');
});
}
@@ -2086,14 +2162,14 @@ function aceDryStop(aceId){
clog('ACE '+(aceId+1)+' - '+tr('ace_dry_dryer')+': '+tr('ace_dry_stop'),'msg-ok');
poll();
})
.catch(function(e){clog('ACE-Fehler: '+e,'msg-err');});
.catch(function(e){clog('ACE '+tr('log_error','Fehler:')+' '+e,'msg-err');});
}
function loadStore(){
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
storeFiles=d.result||[];
renderStore();
}).catch(function(e){clog('Store-Fehler: '+e,'msg-err')});
}).catch(function(e){clog(tr('log_error','Fehler:')+' '+e,'msg-err')});
}
function uploadGcode(file){
@@ -2133,7 +2209,7 @@ function uploadGcode(file){
if(status){ status.textContent=T.store_upload_error.replace('{error}',e.message); status.className='upload-status-err'; }
if(label) label.style.display='';
if(zone) zone.style.pointerEvents='';
clog('Upload-Fehler: '+e,'msg-err');
clog(tr('log_error','Fehler:')+' '+e,'msg-err');
});
}
@@ -2291,7 +2367,7 @@ function clearWebUploadWarningFlag(fileId, onDone){
loadStore();
})
.catch(function(e){
clog('Verifizierungs-Fehler: '+e,'msg-err');
clog(tr('log_error','Fehler:')+' '+e,'msg-err');
});
}
@@ -2369,7 +2445,7 @@ function confirmStoreWebVerify(){
})
.catch(function(e){
if(status){status.textContent='✗ '+e.message;}
clog('Verifizierungs-Fehler: '+e,'msg-err');
clog(tr('log_error','Fehler:')+' '+e,'msg-err');
});
}
@@ -2623,6 +2699,8 @@ function openFilamentDialog(slots){
}).join('');
}
if(dlg)dlg.classList.add('open');
// Spoolman spool picker — loaded async after dialog is visible
setTimeout(_buildSpoolmanSection, 0);
}
function closeFilamentDialog(){
@@ -2675,6 +2753,22 @@ function confirmFilamentPrint(){
var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;});
var fdAlEl=document.getElementById('fd-auto-leveling');
var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0);
// Spoolman: collect slot→spool assignments and submit to backend
if(_spoolmanStatus.configured){
var newSlotMap={};
document.querySelectorAll('[data-spool-slot]').forEach(function(sel){
var spoolId=parseInt(sel.value);
if(!Number.isNaN(spoolId)&&spoolId>0)newSlotMap[sel.dataset.spoolSlot]=spoolId;
});
fetch(_apiUrl('/kx/spoolman/active-spool'),{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({slot_map:newSlotMap})
}).then(function(r){return r.json();}).then(function(d){
_slotSpoolMap=d.slot_spools||{};
}).catch(function(){});
}
closeFilamentDialog();
if(_filamentDialogMode==='banner'){
// Banner-Modus: /kx/print bevorzugen wenn _storeFileId bekannt (gleicher Pfad wie File-Browser).
@@ -2709,7 +2803,7 @@ function confirmFilamentPrint(){
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
}).catch(function(e){
clog(tr('log_error')+' '+e,'msg-err');
clog(tr('log_error','Fehler:')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
} else {
@@ -2719,9 +2813,9 @@ function confirmFilamentPrint(){
headers:{'Content-Type':'application/json'},
body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling})
}).then(function(r){return r.json()}).then(function(d){
if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');}
else{clog('Druckfehler: '+(d.error||'?'),'msg-err');}
}).catch(function(e){clog('Druckfehler: '+e,'msg-err');});
if(d.result==='ok'){clog(tr('log_print_start','Druckstart:')+' '+_storeFilename,'msg-ok');showPanel('dashboard');}
else{clog(tr('log_error','Fehler:')+' '+(d.error||'?'),'msg-err');}
}).catch(function(e){clog(tr('log_error','Fehler:')+' '+e,'msg-err');});
}
}
@@ -2894,7 +2988,7 @@ function confirmSkip(){
fetch(_apiUrl('/kx/skip'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({names:names})})
.then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
.then(function(res){
if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;}
if(!res.ok){st.textContent=(res.j&&res.j.error)||tr('log_error','Fehler:');st.style.color='var(--err)';btn.disabled=false;return;}
st.textContent=tr('skip_success');st.style.color='var(--ok)';
// Dialog offen lassen + neu laden damit der "übersprungen"-Status erscheint
setTimeout(function(){ _refreshSkipDialog(); btn.disabled=false; st.textContent=''; }, 1500);
@@ -2906,7 +3000,7 @@ function storeDelete(fileId){
if(!confirm(T.store_delete_confirm)) return;
fetch(_apiUrl('/kx/files/'+fileId),{method:'DELETE'}).then(function(r){
if(r.ok){loadStore();}
else{clog('Löschen fehlgeschlagen','msg-err');}
else{clog(tr('log_delete_failed','Löschen fehlgeschlagen'),'msg-err');}
});
}
@@ -2940,7 +3034,7 @@ function confirmAddPrinter(){
body:JSON.stringify({printer_ip:ip,name:name})})
.then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
.then(function(res){
if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;}
if(!res.ok){st.textContent=(res.j&&res.j.error)||tr('log_error','Fehler:');st.style.color='var(--err)';btn.disabled=false;return;}
st.textContent=T.apd_success;st.style.color='var(--ok)';
setTimeout(function(){location.reload();},2500);
})
@@ -2951,7 +3045,7 @@ function removePrinter(id,name){
fetch('/kx/printers/'+encodeURIComponent(id),{method:'DELETE'})
.then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
.then(function(res){
if(!res.ok){alert((res.j&&res.j.error)||'Fehler');return;}
if(!res.ok){alert((res.j&&res.j.error)||tr('log_error','Fehler:'));return;}
setTimeout(function(){location.href='/printer1';},2000);
})
.catch(function(e){alert(''+e);});
@@ -3020,6 +3114,6 @@ function loadPrinterTab(){
}).join('');
});
}).catch(function(e){
if(grid)grid.innerHTML='<div style="color:var(--err);font-size:13px;padding:20px">Fehler: '+e+'</div>';
if(grid)grid.innerHTML='<div style="color:var(--err);font-size:13px;padding:20px">'+tr('log_error','Fehler:')+' '+e+'</div>';
});
}

View File

@@ -315,7 +315,7 @@
<span class="slider-val" id="d-fan-val">0</span>
</div>
<div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(0)">Aus</button>
<button class="btn btn-sm" id="d-fan-off" style="background:var(--raised);color:var(--txt)" onclick="quickFan(0)">Aus</button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(25)">25%</button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(50)">50%</button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(75)">75%</button>
@@ -646,6 +646,13 @@
<label for="fd-auto-leveling" style="margin:0;cursor:pointer;font-size:13px" id="fd-lbl-auto-leveling">Auto-Leveling</label>
</div>
</div>
<div id="fd-spoolman-section" style="display:none;margin-bottom:16px;border-top:1px solid var(--border);padding-top:12px">
<p style="font-size:12px;color:var(--txt2);margin-bottom:8px;display:flex;align-items:center;gap:6px">
<span id="fd-spoolman-lbl">🧵 Spoolman</span>
<span id="fd-spoolman-loading" style="display:none;font-size:10px"></span>
</p>
<div id="fd-spoolman-rows" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="fd-cancel" onclick="closeFilamentDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
<button id="fd-print" onclick="confirmFilamentPrint()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">▶ Drucken</button>

View File

@@ -287,5 +287,23 @@
"log_clear": "✕ Leeren",
"log_filter_placeholder": "Filtern…",
"skip_cancel": "Abbrechen",
"skip_confirm": "Überspringen"
"skip_confirm": "Überspringen",
"settings_cat_connection": "Verbindung",
"settings_cat_printer": "Drucker",
"settings_cat_system": "System",
"settings_btn_tooltip": "Einstellungen",
"settings_printer_name_placeholder": "z.B. Kobra X Links",
"settings_device_id_placeholder": "32 Hexzeichen",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_device_id_hint": "32 Hexzeichen",
"settings_mode_id_placeholder": "20030",
"settings_language": "Sprache",
"settings_theme_toggle": "Wechsel Hell / Dunkel",
"settings_orca_profiles_label": "OrcaSlicer-Profile",
"settings_orca_profiles_import": "Profile importieren",
"skip_confirm_btn": "Überspringen",
"btn_delete": "Löschen",
"log_print_start": "Druckstart:",
"log_print_action": "Druck:",
"log_delete_failed": "Löschung fehlgeschlagen"
}

View File

@@ -136,10 +136,6 @@
"settings_print": "Print Settings",
"settings_poll": "Poll Interval (seconds)",
"nav_settings": "Settings",
"settings_cat_display": "Appearance",
"settings_cat_filament": "Filament",
"settings_cat_language": "Language",
"settings_cat_theme": "Toggle light / dark",
"settings_filament_mapping": "Filament profile mapping (per slot)",
"settings_filament_mapping_save": "Save mapping",
"settings_visible_vendors": "Visible vendors (profile dropdown)",
@@ -161,6 +157,14 @@
"settings_default_slot": "Default Slot (single color)",
"settings_slot_auto": "Auto (all loaded slots)",
"settings_auto_leveling": "Auto-Leveling Default",
"settings_cat_connection": "Connection",
"settings_cat_printer": "Printer",
"settings_cat_display": "Appearance",
"settings_cat_filament": "Filament",
"settings_cat_system": "System",
"settings_auto_leveling_label": "Auto-Leveling before print",
"settings_poll_interval_label": "Poll Interval (seconds)",
"settings_poll_interval_hint": "How often the bridge queries printer status",
"fd_options_title": "Print Options",
"print_auto_leveling": "Auto-Leveling",
"settings_file_ready_mode": "Start Print Behavior",
@@ -168,6 +172,12 @@
"settings_file_ready_dialog": "Print Dialog",
"settings_camera_on_print": "Turn camera on at print start",
"settings_web_upload_warning": "Show warning when printing web uploads",
"settings_filament_mapping_label": "Filament profile mapping (per slot)",
"settings_filament_mapping_hint": "Fixed Orca profile per AMS slot. On slicer sync, the bridge sends this profile instead of \"Generic\".",
"settings_filament_mapping_save_label": "Save mapping",
"settings_visible_vendors_label": "Visible vendors (profile dropdown)",
"settings_visible_vendors_save_label": "Save selection",
"settings_vendor_filter_placeholder": "Search vendors…",
"update_check": "Check for Updates",
"update_checking": "Checking...",
"update_available": "available",
@@ -287,5 +297,20 @@
"sf_new": "New",
"ss_date": "↓ Date",
"ss_name": "AZ Name",
"ss_dur": "⏱ Print time"
"ss_dur": "⏱ Print time",
"settings_btn_tooltip": "Settings",
"settings_printer_name_placeholder": "e.g. Kobra X Left",
"settings_device_id_placeholder": "32 hex characters",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_device_id_hint": "32 hex characters",
"settings_mode_id_placeholder": "20030",
"settings_language": "Language",
"settings_theme_toggle": "Toggle light / dark",
"settings_orca_profiles_label": "OrcaSlicer Profiles",
"settings_orca_profiles_import": "Import profiles",
"skip_confirm_btn": "Skip",
"btn_delete": "Delete",
"log_print_start": "Print start:",
"log_print_action": "Print:",
"log_delete_failed": "Delete failed"
}

View File

@@ -287,5 +287,23 @@
"log_clear": "✕ Limpiar",
"log_filter_placeholder": "Filtrar…",
"skip_cancel": "Cancelar",
"skip_confirm": "Omitir"
"skip_confirm": "Omitir",
"settings_cat_connection": "Conexión",
"settings_cat_printer": "Impresora",
"settings_cat_system": "Sistema",
"settings_btn_tooltip": "Ajustes",
"settings_printer_name_placeholder": "p. ej. Kobra X Sala",
"settings_device_id_placeholder": "32 caracteres hexadecimales",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_device_id_hint": "32 caracteres hexadecimales",
"settings_mode_id_placeholder": "20030",
"settings_language": "Idioma",
"settings_theme_toggle": "Alternar claro / oscuro",
"settings_orca_profiles_label": "Perfiles de OrcaSlicer",
"settings_orca_profiles_import": "Importar perfiles",
"skip_confirm_btn": "Omitir",
"btn_delete": "Eliminar",
"log_print_start": "Inicio de impresión:",
"log_print_action": "Impresión:",
"log_delete_failed": "Error al eliminar"
}

View File

@@ -287,5 +287,23 @@
"log_clear": "✕ Effacer",
"log_filter_placeholder": "Filtrer…",
"skip_cancel": "Annuler",
"skip_confirm": "Ignorer"
"skip_confirm": "Ignorer",
"settings_cat_connection": "Connexion",
"settings_cat_printer": "Imprimante",
"settings_cat_system": "Système",
"settings_btn_tooltip": "Paramètres",
"settings_printer_name_placeholder": "p. ex. Kobra X Salon",
"settings_device_id_placeholder": "32 caractères hexadécimaux",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_device_id_hint": "32 caractères hexadécimaux",
"settings_mode_id_placeholder": "20030",
"settings_language": "Langue",
"settings_theme_toggle": "Basculer clair / sombre",
"settings_orca_profiles_label": "Profils OrcaSlicer",
"settings_orca_profiles_import": "Importer des profils",
"skip_confirm_btn": "Ignorer",
"btn_delete": "Supprimer",
"log_print_start": "Début de l'impression :",
"log_print_action": "Impression :",
"log_delete_failed": "Échec de la suppression"
}

View File

@@ -287,5 +287,23 @@
"sf_new": "Nuovo",
"ss_date": "↓ Data",
"ss_name": "Nome AZ",
"ss_dur": "⏱ Tempo di stampa"
}
"ss_dur": "⏱ Tempo di stampa",
"settings_cat_connection": "Connessione",
"settings_cat_printer": "Stampante",
"settings_cat_system": "Sistema",
"settings_btn_tooltip": "Impostazioni",
"settings_printer_name_placeholder": "p. es. Kobra X Sala",
"settings_device_id_placeholder": "32 caratteri esadecimali",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_device_id_hint": "32 caratteri esadecimali",
"settings_mode_id_placeholder": "20030",
"settings_language": "Lingua",
"settings_theme_toggle": "Attiva/disattiva chiaro / scuro",
"settings_orca_profiles_label": "Profili OrcaSlicer",
"settings_orca_profiles_import": "Importa profili",
"skip_confirm_btn": "Salta",
"btn_delete": "Elimina",
"log_print_start": "Inizio stampa:",
"log_print_action": "Stampa:",
"log_delete_failed": "Eliminazione non riuscita"
}

View File

@@ -287,5 +287,23 @@
"log_clear": "✕ 清空",
"log_filter_placeholder": "筛选…",
"skip_cancel": "取消",
"skip_confirm": "跳过"
"skip_confirm": "跳过",
"settings_cat_connection": "连接",
"settings_cat_printer": "打印机",
"settings_cat_system": "系统",
"settings_btn_tooltip": "设置",
"settings_printer_name_placeholder": "例如 Kobra X 左",
"settings_device_id_placeholder": "32 个十六进制字符",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_device_id_hint": "32 个十六进制字符",
"settings_mode_id_placeholder": "20030",
"settings_language": "语言",
"settings_theme_toggle": "切换浅色 / 深色",
"settings_orca_profiles_label": "OrcaSlicer 配置文件",
"settings_orca_profiles_import": "导入配置文件",
"skip_confirm_btn": "跳过",
"btn_delete": "删除",
"log_print_start": "打印开始:",
"log_print_action": "打印:",
"log_delete_failed": "删除失败"
}