diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.ym b/.gitea/ISSUE_TEMPLATE/bug_report.ym new file mode 100644 index 0000000..d870369 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.ym @@ -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 `" + render: text + validations: + required: false \ No newline at end of file diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.yml b/.gitea/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..5ad695d --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.yml @@ -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 `" + render: shell + validations: + required: false \ No newline at end of file diff --git a/bug_report.yml b/bug_report.yml new file mode 100644 index 0000000..f4f7d41 --- /dev/null +++ b/bug_report.yml @@ -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 `" + render: text + validations: + required: false diff --git a/config/config.ini.example b/config/config.ini.example index b954b90..2fe3f48 100644 --- a/config/config.ini.example +++ b/config/config.ini.example @@ -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 diff --git a/config_loader.py b/config_loader.py index b1012e1..bf41b85 100644 --- a/config_loader.py +++ b/config_loader.py @@ -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")) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 930c34f..1595ff8 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -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") diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 52bacf9..96817b6 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -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='';return;} + rows.innerHTML=slotKeys.map(function(idx){ + var slot=usedSlots[idx]; + var col=(slot.color_hex||'#888'); + var currentSpool=_slotSpoolMap[String(idx)]||''; + var opts=''+_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 ''; + }).join(''); + return '
'+ + ''+ + 'Slot '+(idx+1)+''+ + ''+ + '
'; + }).join(''); + }).catch(function(){ + if(loading)loading.style.display='none'; + if(rows)rows.innerHTML='Spoolman unavailable'; + }); +} + 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='
'+profile.vendor+'
'; } + var spoolId=_slotSpoolMap&&_slotSpoolMap[String(globalIdx)]; + var spoolBadge=(!empty&&spoolId) + ?'
🧵 #'+spoolId+'
' + :''; html+='
' +'
' +'
'+materialLabel+'
' +vendorBadge + +spoolBadge +'
'+slotLabel+'
' +'
'+pct+'
' +'
' @@ -1169,7 +1245,7 @@ function refreshUserProfileList(){ return '
' +'★ '+label+'' +'' + +'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="'+tr('btn_delete','Löschen')+'">🗑' +'
'; }).join(''); }).catch(function(){}); @@ -1206,7 +1282,7 @@ function refreshImportDialogList(){ return '
' +'★ '+label+'' +'' + +'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="'+tr('btn_delete','Löschen')+'">🗑' +'
'; }).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='
Fehler: '+e+'
'; + if(grid)grid.innerHTML='
'+tr('log_error','Fehler:')+' '+e+'
'; }); } diff --git a/web/themes/default/index.html b/web/themes/default/index.html index 843d553..a9fff33 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -315,7 +315,7 @@ 0
- + @@ -646,6 +646,13 @@
+
diff --git a/web/translations/de.json b/web/translations/de.json index c5011df..4be3b79 100644 --- a/web/translations/de.json +++ b/web/translations/de.json @@ -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" } diff --git a/web/translations/en.json b/web/translations/en.json index 61c5c86..778e9de 100644 --- a/web/translations/en.json +++ b/web/translations/en.json @@ -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": "A–Z 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" } diff --git a/web/translations/es.json b/web/translations/es.json index 822cf4b..cb8f3eb 100644 --- a/web/translations/es.json +++ b/web/translations/es.json @@ -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" } diff --git a/web/translations/fr.json b/web/translations/fr.json index 01fc6f6..eaed5c1 100644 --- a/web/translations/fr.json +++ b/web/translations/fr.json @@ -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" } diff --git a/web/translations/it.json b/web/translations/it.json index 56ed376..4a4c4b2 100644 --- a/web/translations/it.json +++ b/web/translations/it.json @@ -287,5 +287,23 @@ "sf_new": "Nuovo", "ss_date": "↓ Data", "ss_name": "Nome A–Z", - "ss_dur": "⏱ Tempo di stampa" -} \ No newline at end of file + "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" +} diff --git a/web/translations/zh-cn.json b/web/translations/zh-cn.json index 8225ee0..e883fbe 100644 --- a/web/translations/zh-cn.json +++ b/web/translations/zh-cn.json @@ -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": "删除失败" }