diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md index ed5a469..440e5b7 100644 --- a/CHANGELOG.de.md +++ b/CHANGELOG.de.md @@ -1,5 +1,21 @@ # Changelog +## [0.9.26] – 2026-06-21 + +### Neu +- **Italienische Sprachunterstützung** (PR #66, @Alex_M). Die Bridge-UI ist jetzt vollständig auf Italienisch verfügbar. + +### Behoben +- **Kamera startete immer beim Druckbeginn** (Issue #50). `camera_on_print` fehlte in der `/api/state`-Antwort — JavaScript las `undefined` und startete die Kamera unabhängig vom Setting. Jetzt korrekt im State enthalten. +- **Auto-Leveling-Setting wurde im Moonraker-Druckpfad ignoriert** (Issue #57). `handle_print_start` las den Wert nur aus den Bridge-Args, nicht aus dem Request-Body — Dialog-Checkbox und Per-Print-Override hatten keine Wirkung. Verhält sich jetzt identisch zum direkten Druckpfad. +- **Filament-Mapping: Freitext-Felder durch Dropdowns ersetzt** (Issue #57). Falsch getippte Vendor/Name-Kombination brach das Profil-Matching ohne Fehlermeldung; Felder sind jetzt Dropdowns (Vendor → Profil, vendor-gefiltert), sodass nur gültige Kombinationen gespeichert werden können. +- **Dashboard zeigte generischen Materialtyp statt Profilname** (Issue #57). AMS-Slot-Karten zeigen jetzt den gemappten Profilnamen (z.B. „eSUN PLA-Basic") statt nur „PLA". Fallback auf generischen Typ wenn kein Profil gemappt ist. +- **Ghost-Profil auf leerem Slot** (Issue #57). Verwaiste Mappings für leere Slots wurden weiterhin angezeigt; leere Slots zeigen jetzt korrekt „–". +- **Skip-Objects-Panel fehlte im Orca-Upload-Flow** (Issue #57). Panel erscheint jetzt in allen Druckflows; bei frischem Upload fragt die Bridge `fileDetails` beim Drucker nach und pollt die Objektliste bis zu 6 Sekunden nach. +- **Banner und Dialog erschienen gleichzeitig** (Issue #57). Settings-Save setzt jetzt den Dialog-Cancel-State zurück, sodass der Slot-Mapper nach Wechsel des Start-Print-Verhaltens zuverlässig öffnet. +- **„Leeren" lud idle-Datei beim nächsten Poll nach** (Issue #57). Leeren setzt jetzt den lokalen State sofort zurück (`file_ready`, `filename`, `thumbnail`) und löscht alle Dialog-Sperren — Vorschaubild und Aktions-Buttons verschwinden sofort und kommen nicht zurück. +- **Material-Matching für „PLA Silk", „Matte PLA" etc.** (PR #64, @p2l). Modifier+Basis-Muster in beliebiger Wortreihenfolge werden jetzt auf den Basis-Typ normalisiert; Dash-Varianten (PLA-CF) bleiben weiterhin korrekt inkompatibel mit ihrem Basis-Typ. + ## [0.9.25] – 2026-06-17 ### Behoben diff --git a/CHANGELOG.md b/CHANGELOG.md index 282b6a0..40bb001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## [0.9.26] – 2026-06-21 + +### New +- **Italian language support** (PR #66, @Alex_M). The bridge UI is now fully + available in Italian. + +### Fixed +- **Camera always started at print begin** (issue #50). `camera_on_print` was + missing from the `/api/state` response — JavaScript read `undefined` and started + the camera regardless of the setting. Now correctly exposed in state. +- **Auto-leveling setting ignored in Moonraker print path** (issue #57). + `handle_print_start` read the value only from bridge args, not from the request + body, so the dialog checkbox and the per-print override had no effect. Now + behaves identically to the direct print path. +- **Filament mapping free-text fields replaced by dropdowns** (issue #57). A + mistyped vendor/name broke profile matching silently; fields are now dropdowns + (vendor → profile, vendor-filtered) so only valid combinations can be saved. +- **Dashboard showed generic material type instead of profile name** (issue #57). + AMS slot cards now display the mapped profile name (e.g. "eSUN PLA-Basic") + instead of just "PLA". Falls back to the generic type when no profile is mapped. +- **Ghost profile shown on empty slot** (issue #57). Stale mappings for empty + slots were still rendered; empty slots now correctly show "–". +- **Skip-Objects panel missing in Orca upload flow** (issue #57). Panel now + appears in all print flows; on fresh upload the bridge requests `fileDetails` + from the printer and retries the object list for up to 6 s. +- **Banner and dialog appeared simultaneously** (issue #57). Settings save now + resets the dialog cancel state so the slot mapper reliably opens after toggling + Start Print Behavior. +- **"Clear" reloaded idle file on next poll** (issue #57). Clear now immediately + resets local state (`file_ready`, `filename`, `thumbnail`) and clears all dialog + locks — the preview and action buttons disappear instantly and do not return. +- **Material matching for "PLA Silk", "Matte PLA" etc.** (PR #64, @p2l). + Modifier+base patterns in any word order are now normalised to the base type; + dash-suffix variants (PLA-CF) remain correctly incompatible with their base. + ## [0.9.25] – 2026-06-17 ### Fixed diff --git a/VERSION b/VERSION index ec9b691..46e7a71 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.25 +0.9.26 diff --git a/env_loader.py b/env_loader.py index 5ed9379..0ff9433 100644 --- a/env_loader.py +++ b/env_loader.py @@ -48,4 +48,6 @@ MODE_ID = get("MODE_ID", "") DEVICE_ID = get("DEVICE_ID", "") DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto") 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"))) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 774e7e5..930c34f 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -1077,6 +1077,25 @@ class KobraXBridge: if (not skipped and self._pending_preprint_skip and now <= self._pending_preprint_skip_deadline): return + + # Während eines aktiven Drucks sind Skip-Zustände effektiv monoton. + # Manche Firmware-Reports kommen zwischenzeitlich leer/teilweise zurück; + # diese dürfen bereits bestätigte Skip-Objekte nicht aus der UI löschen. + existing_skipped = [str(n) for n in (self._skip_state.get("skipped") or []) if n] + existing_set = set(existing_skipped) + incoming_skipped = [str(n) for n in (skipped or []) if n] + incoming_set = set(incoming_skipped) + active_print = self._state.get("print_state") in ("printing", "paused") + if active_print and existing_set: + if not incoming_set: + skipped = list(existing_skipped) + elif not incoming_set.issuperset(existing_set): + merged = list(existing_skipped) + for n in incoming_skipped: + if n not in existing_set: + merged.append(n) + skipped = merged + # Pending-Lock aufheben sobald Drucker die gewünschten Objekte bestätigt if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip): self._pending_preprint_skip = [] @@ -1123,7 +1142,7 @@ class KobraXBridge: return False for i in range(max(1, int(retries))): try: - if self._state.get("kobra_state") != "printing": + if self._state.get("print_state") not in ("printing", "paused"): time.sleep(max(0.1, float(delay_s))) continue resp = self.client.skip_objects(wanted) @@ -2453,6 +2472,18 @@ class KobraXBridge: names = json.loads(f.get("objects_skip_parts") or "[]") except Exception: names = [] + # Noch keine Objekte im Store (frischer Orca-/Web-Upload): einmal aktiv + # file/fileDetails beim Drucker anfragen. _on_file() füllt den Store nach, + # das Frontend pollt diesen Endpoint und bekommt die Liste beim nächsten + # Versuch (Issue #57 — Skip-Parität auch außerhalb des File-Browsers). + if not names: + fn = f.get("filename") or "" + if fn: + try: + self.client.publish("file", "fileDetails", + {"root": "local", "filename": fn}, timeout=0) + except Exception as e: + log.debug(f"fileDetails-Nachfrage fehlgeschlagen: {e}") return self._json_cors({ "result": { "names": names, @@ -2479,29 +2510,8 @@ class KobraXBridge: return self._json_cors({"error": str(e)}, status=502) return self._json_cors({"result": "ok", "names": names}) - async def handle_kx_skip_query(self, request): - """Druck-Objektliste vom Drucker neu abfragen. - - POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten - Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state). - """ - try: - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: self.client.query_skip_objects()) - except Exception as e: - return self._json_cors({"error": str(e)}, status=502) - return self._json_cors({"result": self._skip_state}) - - async def handle_kx_skip_state(self, request): - """Aktueller Skip-State. - - Kombiniert: - - Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell - laufenden filename (file/report beim Druckstart hat die Liste gefüllt). - skip/query_obj liefert nämlich NUR die bereits geskippten zurück, - nicht die Gesamtliste. - - Geskippt: aus self._skip_state (von skip/report aktualisiert). - """ + def _build_skip_state_result(self) -> dict: + """Baut den kombinierten Skip-State für UI-Endpunkte.""" filename = self._state.get("filename", "") all_objects: list[str] = [] svg = "" @@ -2513,14 +2523,46 @@ class KobraXBridge: svg = f.get("svg_image") or "" except Exception as e: log.warning(f"skip_state lookup failed: {e}") - result = { + return { "objects": all_objects, "skipped": list(self._skip_state.get("skipped", [])), "svg_b64": svg, "ts": self._skip_state.get("ts", 0), "filename": filename, } - return self._json_cors({"result": result}) + + async def handle_kx_skip_query(self, request): + """Druck-Objektliste vom Drucker neu abfragen. + + POST /kx/skip/query → triggert skip/query_obj, wartet kurz auf den + async skip/report und gibt den zusammengeführten Skip-State zurück. + """ + prev_ts = int(self._skip_state.get("ts", 0) or 0) + try: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, lambda: self.client.query_skip_objects()) + except Exception as e: + return self._json_cors({"error": str(e)}, status=502) + + deadline = time.time() + 1.5 + while time.time() < deadline: + if int(self._skip_state.get("ts", 0) or 0) > prev_ts: + break + await asyncio.sleep(0.1) + + return self._json_cors({"result": self._build_skip_state_result()}) + + async def handle_kx_skip_state(self, request): + """Aktueller Skip-State. + + Kombiniert: + - Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell + laufenden filename (file/report beim Druckstart hat die Liste gefüllt). + skip/query_obj liefert nämlich NUR die bereits geskippten zurück, + nicht die Gesamtliste. + - Geskippt: aus self._skip_state (von skip/report aktualisiert). + """ + return self._json_cors({"result": self._build_skip_state_result()}) async def handle_kx_printers(self, request): # Aktive Drucker (mit IP) sammeln @@ -2615,8 +2657,8 @@ class KobraXBridge: }, } - # Pre-Print-Skip sofort im UI-Status spiegeln - self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())} + # UI erst nach echter Drucker-Bestätigung als "geskippt" markieren. + self._skip_state = {"skipped": [], "ts": int(time.time())} if excluded_objects: self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n] self._pending_preprint_skip_deadline = time.time() + 12.0 @@ -3139,7 +3181,8 @@ class KobraXBridge: ams_box_mapping = self._build_auto_ams_box_mapping() use_ams = len(ams_box_mapping) > 0 - auto_leveling = getattr(self._args, "auto_leveling", 1) + # Dialog-Checkbox (body) hat Vorrang, sonst Setting-Default (wie handle_kx_print). + auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1))) url = self._state.get("last_upload_url", "") filesize = self._state.get("last_upload_size", 0) md5 = self._state.get("last_upload_md5", "") @@ -3169,6 +3212,15 @@ class KobraXBridge: }, } + # UI erst nach echter Drucker-Bestätigung als "geskippt" markieren. + self._skip_state = {"skipped": [], "ts": int(time.time())} + if excluded_objects: + self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n] + self._pending_preprint_skip_deadline = time.time() + 12.0 + else: + self._pending_preprint_skip = [] + self._pending_preprint_skip_deadline = 0.0 + log.info( f"print/start api=1 mode={self._filament_mode} " f"ams={len(ams_box_mapping)} slots assignments={filament_assignments is not None}" @@ -3181,6 +3233,9 @@ class KobraXBridge: if result is None: return web.json_response({"error": "Keine Antwort vom Drucker"}, status=504) + if excluded_objects: + loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects)) + return web.json_response({"result": "ok"}) async def handle_print_pause(self, request): @@ -3815,6 +3870,8 @@ class KobraXBridge: "camera_url": s["camera_url"], "fan_speed": s["fan_speed"], "print_speed_mode": s["print_speed_mode"], + "auto_leveling": getattr(self._args, "auto_leveling", 1), + "camera_on_print": getattr(self._args, "camera_on_print", 0), "web_upload_warning": getattr(self._args, "web_upload_warning", 1), "light_on": s["light_on"], "light_brightness": s["light_brightness"], diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 02e0de0..52bacf9 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -9,9 +9,10 @@ var camOn=false; var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support) var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55) +var _idleCleared=false; // User hat idle-Datei explizit „geleert" → kein Nachladen von s.filename (Issue #57) var _fdDialogOpen=false; // Dialog ist gerade offen -var _fdAutoOpenedFile=null; // Dateiname für den der Dialog auto-geöffnet wurde -var _fdUserCancelled=false; // User hat den Auto-Open-Dialog abgebrochen +var _fdAutoOpenedFile=sessionStorage.getItem('fdAutoOpenedFile')||null; +var _fdUserCancelled=sessionStorage.getItem('fdUserCancelled')==='1'; var currentStep=1; var currentPanel='dashboard'; var aceAutoRefillPrefs=(function(){ @@ -108,6 +109,7 @@ function _langToggleLabel(lang){ if(lang==='de')return 'Deutsch'; if(lang==='en')return 'English'; if(lang==='fr')return 'Français'; + if(lang==='it')return 'Italiano'; if(lang==='zh-cn')return '简体中文'; return 'Espanol'; } @@ -115,10 +117,10 @@ function _langToggleLabel(lang){ function _mapSupportedLang(lang){ if(!lang)return ''; var l=String(lang).toLowerCase().replace(/_/g,'-').trim(); - if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='zh-cn')return l; + if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='it'||l==='zh-cn')return l; var base=l.split('-')[0]; - if(base==='de'||base==='en'||base==='es'||base==='fr')return base; + if(base==='de'||base==='en'||base==='es'||base==='fr'||base==='it')return base; if(base==='zh'){ if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn'; @@ -664,10 +666,18 @@ function applyState(){ if(s.file_ready&&s.print_state==='standby'){ document.getElementById('file-ready-name').textContent=s.file_ready; // Neue Datei → Abbruch-Sperre aufheben - if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready) _fdUserCancelled=false; - if(shouldAutoOpen&&!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ - _fdAutoOpenedFile=s.file_ready; - startReadyFileWithSlots(s.file_ready,true); + if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready){ + _fdUserCancelled=false; + sessionStorage.removeItem('fdUserCancelled'); + sessionStorage.removeItem('fdAutoOpenedFile'); + } + if(shouldAutoOpen){ + // Dialog-Modus: Banner niemals anzeigen. + frb.style.display='none'; + if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ + _fdAutoOpenedFile=s.file_ready; + startReadyFileWithSlots(s.file_ready,true); + } } else { frb.style.display='flex'; bannerVisible=true; @@ -686,8 +696,11 @@ function applyState(){ // Zuletzt geladene Datei merken (Issue #55): solange sie über den State // sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename // (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen. + // Echte ready-Datei oder laufender Druck hebt einen vorherigen „Clear" auf. + if(s.file_ready||printing) _idleCleared=false; if(s.file_ready) _lastLoadedFile=s.file_ready; - else if(s.filename) _lastLoadedFile=s.filename; + else if(s.filename && !_idleCleared) _lastLoadedFile=s.filename; + else if(_idleCleared) _lastLoadedFile=null; // Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine // Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion // anbietet. @@ -864,7 +877,13 @@ function applyState(){ var activity=(slot.activity||''); var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–'); var slotLabel=T.label_slot+' '+(globalIdx+1); - var profile=(window._slotProfileMap||{})[globalIdx]; + // Gemapptes Profil nur für belegte Slots verwenden — sonst zeigt ein + // verwaistes Mapping (Slot wurde geleert) ein „Geister"-Profil (Issue #57). + var profile=empty?null:(window._slotProfileMap||{})[globalIdx]; + var genericType=(slot.type||slot.material_type||'–'); + // Material-Label: bei belegtem Slot mit Mapping den konkreten Profilnamen + // (z.B. „eSUN PLA+") statt nur des generischen Typs zeigen (Issue #57 Punkt 4). + var materialLabel=empty?'–':((profile&&profile.name)?profile.name:genericType); var vendorBadge=''; if(!empty && profile && profile.vendor){ var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':''); @@ -873,7 +892,7 @@ function applyState(){ html+='
' +'
' - +'
'+(empty?'–':(slot.type||slot.material_type||'–'))+'
' + +'
'+materialLabel+'
' +vendorBadge +'
'+slotLabel+'
' +'
'+pct+'
' @@ -895,7 +914,7 @@ function applyState(){ if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none'; // auto-start camera during print (unless user explicitly stopped it) - if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){ + if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped&&s.camera_on_print){ camStart(); } // reset user-stopped flag when print ends so next print auto-starts again @@ -1011,6 +1030,11 @@ function onPollIntervalInput(){ } // ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ── +// Pro Slot ein einzelnes Profil-Dropdown (vendor+name gemeinsam, gekeyt per +// _profileKey). Kein Freitext mehr → das (vendor,name)→id-Matching kann nicht +// mehr durch manuelle Eingabe brechen (Issue #57 Punkt 1). Optionen werden aus +// /kx/filament/profiles geladen, nach Vendor gruppiert, User-Profile zuerst, +// mit demselben Vendor-Sichtbarkeitsfilter wie das Slot-Edit-Dropdown. function renderFilamentMapping(map){ var el=document.getElementById('filament-mapping-list'); if(!el)return; @@ -1020,21 +1044,61 @@ function renderFilamentMapping(map){ var idHint=m.id?' ('+m.id+')':''; rows+=''; + +'' + +'
'; } el.innerHTML=rows; + // Dropdowns befüllen (async, geteilter Profil-Cache + Vendor-Filter) + for(var j=0;j<4;j++){ _fillMappingDropdown(j); } +} +function _fillMappingDropdown(slot){ + var sel=document.getElementById('fmap-'+slot); + if(!sel) return; + var wantKey=_profileKey(sel.dataset.vendor, sel.dataset.name); + _loadOrcaFilaments(function(profiles){ + sel.innerHTML=''; + var userProfs=profiles.filter(function(p){return p.is_user;}); + var systemProfs=profiles.filter(function(p){return !p.is_user;}); + function _opt(g,p){ + var o=document.createElement('option'); + o.value=_profileKey(p.vendor,p.name); + o.dataset.vendor=p.vendor; o.dataset.name=p.name; o.dataset.id=p.id||''; + o.textContent=(p.is_user?'★ ':'')+p.name+(p.vendor?' — '+p.vendor:''); + if(o.value===wantKey)o.selected=true; + g.appendChild(o); + } + if(userProfs.length){ + var gUser=document.createElement('optgroup'); + gUser.label='★ '+(tr('orca_profile_user_label')||'Eigene Profile'); + userProfs.forEach(function(p){_opt(gUser,p);}); + sel.appendChild(gUser); + } + _loadVisibleVendors(function(vis){ + var filtered=systemProfs; + if(vis&&vis.length){ + var allow={};vis.forEach(function(v){allow[v]=1;});allow['Generic']=1; + filtered=systemProfs.filter(function(p){return allow[p.vendor];}); + } + var byVendor={}; + filtered.forEach(function(p){(byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p);}); + Object.keys(byVendor).sort().forEach(function(v){ + var g=document.createElement('optgroup');g.label=v; + byVendor[v].forEach(function(p){_opt(g,p);}); + sel.appendChild(g); + }); + }); + }); } function saveFilamentMapping(){ // Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend). - // Leere Felder = Mapping entfernen. + // Leere Auswahl ("") = Mapping entfernen. var chain=Promise.resolve(); for(var i=0;i<4;i++){ (function(slot){ - var vendor=((document.getElementById('fmap-'+slot+'-vendor')||{}).value||'').trim(); - var name=((document.getElementById('fmap-'+slot+'-name')||{}).value||'').trim(); + var sel=document.getElementById('fmap-'+slot); + var opt=sel?sel.options[sel.selectedIndex]:null; + var vendor=(opt&&opt.dataset.vendor)||''; + var name=(opt&&opt.dataset.name)||''; chain=chain.then(function(){ return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'), {method:'POST',headers:{'Content-Type':'application/json'}, @@ -1193,6 +1257,7 @@ function doProfileImportUpload(files){ var _slotEditIndex=-1; var _slotEditLoaded=false; var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS']; +var _BASE_MATERIAL_TYPES=['PLA','PETG','ABS','ASA','TPU','TPE','PA','PC','HIPS','PEI','PEEK']; function updateSlotEditFeedButton(){ var btn=document.getElementById('btn-slot-edit-feed'); if(!btn)return; @@ -1329,23 +1394,39 @@ function slotEditFeed(){ } function startReadyFile(filename){ var fn=filename||S.file_ready; + function _doStartReadyFile(){ + var btn=document.getElementById('file-ready-btn'); + if(btn){btn.disabled=true;btn.textContent='…';} + post('/printer/print/start',{filename:fn}) + .then(function(r){return r.json();}) + .then(function(){ + 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'); + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }); + } + function _gateAndStart(fileObj){ + if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){ + maybeGateWebUpload(fileObj, function(){ startReadyFile(fn); }); + return; + } + _doStartReadyFile(); + } var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); - if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); }); + if(currentFile){ + _gateAndStart(currentFile); return; } - var btn=document.getElementById('file-ready-btn'); - if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:fn}) - .then(function(r){return r.json();}) - .then(function(r){ - 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'); - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }); + fetch(_apiUrl('/kx/files')).then(function(r){return r.json();}).then(function(d){ + storeFiles=d.result||[]; + var refreshed=(storeFiles||[]).find(function(f){return f.filename===fn;})||null; + _gateAndStart(refreshed); + }).catch(function(){ + _doStartReadyFile(); + }); } function cancelReadyFile(){ post('/api/file_ready/clear',{}) @@ -1361,8 +1442,17 @@ function startIdleFileWithSlots(){ } function clearIdleFile(){ _lastLoadedFile=null; + _idleCleared=true; // verhindert Nachladen von s.filename im nächsten poll() (Issue #57) + _fdAutoOpenedFile=null; // nächster Upload derselben Datei soll Dialog wieder öffnen + _fdUserCancelled=false; + _fdDialogOpen=false; + sessionStorage.removeItem('fdAutoOpenedFile'); + sessionStorage.removeItem('fdUserCancelled'); + sessionStorage.removeItem('webVerifyCancelledFileId'); + S.file_ready=''; S.filename=''; S.thumbnail=''; // sofort lokal leeren, kein Warten auf nächsten Poll var ib=document.getElementById('d-idle-btns');if(ib)ib.style.display='none'; var fn=document.getElementById('d-fname');if(fn){fn.textContent='–';fn.title='';} + var thumb=document.getElementById('d-thumbnail');if(thumb){thumb.style.display='none';thumb.src='';} post('/api/file_ready/clear',{}).catch(function(){}); } function selectMatPreset(m){ @@ -1461,6 +1551,11 @@ function saveSettings(){ btn.disabled=true;btn.textContent='…'; var webUploadWarning=(document.getElementById('s-web-upload-warning')||{}).checked?1:0; S.web_upload_warning=webUploadWarning; + // Start-Print-Behavior-Wechsel könnte den Auto-Open sonst dauerhaft blockieren + // (alter _fdUserCancelled bei gleicher file_ready) → Dialog-State zurücksetzen (Issue #57). + _fdUserCancelled=false;_fdAutoOpenedFile=null; + sessionStorage.removeItem('fdUserCancelled');sessionStorage.removeItem('fdAutoOpenedFile'); + sessionStorage.removeItem('webVerifyCancelledFileId'); post('/api/settings',{ printer_name: document.getElementById('s-printer-name').value, printer_ip: document.getElementById('s-printer-ip').value, @@ -2132,6 +2227,7 @@ var _filamentDialogMode='store'; // 'store' oder 'banner' var _pendingWebVerifyFileId=null; var _pendingWebVerifyFilename=''; var _pendingWebVerifyAction=null; +var _pendingWebVerifyAutoOpen=false; // GCode-Store-Dateiliste. MUSS deklariert sein – sonst ReferenceError, wenn // "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen // wurde (Issue #29 / Theme-Auslagerung PR #27). @@ -2199,7 +2295,8 @@ function clearWebUploadWarningFlag(fileId, onDone){ }); } -function maybeGateWebUpload(fileObj, onContinue){ +function maybeGateWebUpload(fileObj, onContinue, opts){ + opts=opts||{}; if(!fileObj || !fileObj.web_unverified){ if(onContinue) onContinue(); return; @@ -2208,15 +2305,20 @@ function maybeGateWebUpload(fileObj, onContinue){ if(onContinue) onContinue(); return; } + var cancelledId=sessionStorage.getItem('webVerifyCancelledFileId')||''; + if(opts.autoOpen && cancelledId && cancelledId===String(fileObj.id||'')){ + return; + } openWebVerifyDialog(fileObj.id, fileObj.filename, function(){ clearWebUploadWarningFlag(fileObj.id, onContinue); - }); + }, !!opts.autoOpen); } -function openWebVerifyDialog(fileId, filename, onConfirm){ +function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen){ _pendingWebVerifyFileId=fileId; _pendingWebVerifyFilename=filename; _pendingWebVerifyAction=onConfirm||null; + _pendingWebVerifyAutoOpen=!!autoOpen; var status=document.getElementById('store-web-verify-status'); if(status){status.textContent='';} openStoreWebVerifyDialog(); @@ -2230,9 +2332,13 @@ function openStoreWebVerifyDialog(){ function closeStoreWebVerifyDialog(){ var modal=document.getElementById('store-web-verify-dialog'); if(modal){modal.classList.remove('open');} + if(_pendingWebVerifyAutoOpen && _pendingWebVerifyFileId){ + sessionStorage.setItem('webVerifyCancelledFileId', String(_pendingWebVerifyFileId)); + } _pendingWebVerifyFileId=null; _pendingWebVerifyFilename=''; _pendingWebVerifyAction=null; + _pendingWebVerifyAutoOpen=false; } function confirmStoreWebVerify(){ @@ -2252,9 +2358,11 @@ function confirmStoreWebVerify(){ .then(function(){ var fileObj=(storeFiles||[]).find(function(f){return f.id===fileId;}); if(fileObj){fileObj.web_unverified=false;} + sessionStorage.removeItem('webVerifyCancelledFileId'); _pendingWebVerifyFileId=null; _pendingWebVerifyFilename=''; _pendingWebVerifyAction=null; + _pendingWebVerifyAutoOpen=false; closeStoreWebVerifyDialog(); loadStore(); if(typeof action==='function') action(); @@ -2270,7 +2378,7 @@ function startReadyFileWithSlots(filename,_autoOpen){ var fn=filename||S.file_ready; var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn); }); + maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen}); return; } _filamentDialogMode='banner'; @@ -2291,23 +2399,31 @@ function startReadyFileWithSlots(filename,_autoOpen){ }); } + function _proceedWithFileObj(fileObj){ + if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){ + // Verify-Gate war beim ersten Lookup noch nicht aktiv (storeFiles leer) — jetzt prüfen. + if(_autoOpen){_fdDialogOpen=false;} + maybeGateWebUpload(fileObj, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen}); + return; + } + if(fileObj){ + _storeFileId=fileObj.id; + _setGcodeFilamentsFromFileObj(fileObj); + } + openWithSlots(); + } + var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); if(fileObj){ - _storeFileId=fileObj.id; - _setGcodeFilamentsFromFileObj(fileObj); - openWithSlots(); + _proceedWithFileObj(fileObj); return; } // Fallback: refresh file list, then resolve current file by filename. fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){ storeFiles=d.result||[]; - var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); - if(refreshed){ - _storeFileId=refreshed.id; - _setGcodeFilamentsFromFileObj(refreshed); - } - openWithSlots(); + var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;})||null; + _proceedWithFileObj(refreshed); }).catch(function(){ openWithSlots(); }); @@ -2332,6 +2448,17 @@ function _normalizeMaterialKey(material){ var key=(material||'').toUpperCase().replace(/[^A-Z0-9+]/g,''); // Orca often uses PLA for PLA+, while AMS may report PLA+. if(key==='PLA+'||key==='PLAPLUS') return 'PLA'; + // Handle modifier+base patterns in either order: "Matte PLA", "Silk PETG", + // "PLA Silk", "PLA Matte". OrcaSlicer always writes the base type in GCode + // (filament_type = PLA), but users label slots with the full product-style name. + var trimmed=(material||'').trim(); + if(trimmed.indexOf(' ')>=0){ + var words=trimmed.toUpperCase().split(/\s+/); + for(var i=0;i=0) return w; + } + } return key; } function _materialsCompatible(a,b){ @@ -2387,19 +2514,30 @@ function openFilamentDialog(slots){ if(objBody)objBody.style.display='none'; // immer eingeklappt starten if(objArrow)objArrow.style.transform=''; if(_storeFileId){ - fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects')) - .then(function(r){return r.json()}) - .then(function(d){ - var names=(d.result&&d.result.names)||[]; - _printObjectsSvg=(d.result&&d.result.svg_b64)||''; - if(names.length>=2){ - _printObjects=names.map(function(n){return {name:n,skip:false};}); - renderObjectChecklist(); renderObjectSvg(); - var cnt=document.getElementById('fd-objects-count'); - if(cnt)cnt.textContent='('+names.length+')'; - if(objSection)objSection.style.display='block'; - } - }).catch(function(){}); + // Bei frischem Orca-/Web-Upload liefert der Drucker die Objektliste + // (objects_skip_parts) erst per fileDetails nach → ist im Store kurz leer. + // Daher mehrfach nachfragen, bis Objekte da sind (Issue #57 Skip-Parität). + var _objFid=_storeFileId; + var _objTries=0; + (function _loadObjects(){ + if(_objFid!==_storeFileId) return; // Dialog wechselte Datei → abbrechen + fetch(_apiUrl('/kx/files/'+encodeURIComponent(_objFid)+'/objects')) + .then(function(r){return r.json()}) + .then(function(d){ + var names=(d.result&&d.result.names)||[]; + var svg=(d.result&&d.result.svg_b64)||''; + if(names.length>=2){ + _printObjectsSvg=svg; + _printObjects=names.map(function(n){return {name:n,skip:false};}); + renderObjectChecklist(); renderObjectSvg(); + var cnt=document.getElementById('fd-objects-count'); + if(cnt)cnt.textContent='('+names.length+')'; + if(objSection)objSection.style.display='block'; + } else if(_objTries++ < 6){ + setTimeout(_loadObjects, 1000); // bis ~6s auf fileDetails warten + } + }).catch(function(){}); + })(); } // GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten @@ -2491,7 +2629,11 @@ function closeFilamentDialog(){ var dlg=document.getElementById('filament-dialog'); if(dlg)dlg.classList.remove('open'); _fdDialogOpen=false; - if(_fdAutoOpenedFile) _fdUserCancelled=true; + if(_fdAutoOpenedFile){ + _fdUserCancelled=true; + sessionStorage.setItem('fdUserCancelled','1'); + sessionStorage.setItem('fdAutoOpenedFile',_fdAutoOpenedFile); + } } function confirmFilamentPrint(){ @@ -2535,19 +2677,41 @@ function confirmFilamentPrint(){ var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0); closeFilamentDialog(); if(_filamentDialogMode==='banner'){ - // Banner-Modus: normaler print/start mit Slot-Override + // Banner-Modus: /kx/print bevorzugen wenn _storeFileId bekannt (gleicher Pfad wie File-Browser). var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling}) - .then(function(r){return r.json();}) - .then(function(){ - 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'); - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + var startPromise; + if(_storeFileId){ + startPromise=fetch(_apiUrl('/kx/print'),{ + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({ + file_id:_storeFileId, + filament_assignments:assignments, + excluded_objects:excludedObjects, + auto_leveling:fdAutoLeveling + }) }); + }else{ + startPromise=post('/printer/print/start',{ + filename:S.file_ready||_storeFilename, + filament_assignments:assignments, + excluded_objects:excludedObjects, + auto_leveling:fdAutoLeveling + }); + } + startPromise.then(function(r){ + if(!r.ok){return r.text().then(function(t){throw new Error(t||('HTTP '+r.status));});} + return r.json(); + }).then(function(d){ + if(d&&d.error) throw new Error(d.error); + if(d&&d.result&&d.result!=='ok') throw new Error(String(d.result)); + 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'); + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }); } else { // Store-Modus: POST /kx/print fetch(_apiUrl('/kx/print'),{ @@ -2618,35 +2782,45 @@ function renderObjectSvg(){ // ── Mid-Print Skip ── var _skipObjects=[]; // [{name, skipped, willSkip}] var _skipSvg=''; +var _skipPollTimer=null; +function _applySkipDialogState(s){ + s=s||{}; + _skipSvg=s.svg_b64||''; + var skipped=s.skipped||[]; + // Pending-Auswahl (willSkip) beim Refresh erhalten. + var prevWillSkip={}; + (_skipObjects||[]).forEach(function(o){if(o&&o.name)prevWillSkip[o.name]=!!o.willSkip;}); + _skipObjects=(s.objects||[]).map(function(n){ + var isSkipped=(skipped.indexOf(n)>=0); + return {name:n, skipped:isSkipped, willSkip:isSkipped?false:!!prevWillSkip[n]}; + }); + renderSkipList(); renderSkipSvg(); +} function openSkipDialog(){ document.getElementById('skip-status').textContent=''; document.getElementById('skip-confirm').disabled=false; _refreshSkipDialog(); + if(_skipPollTimer)clearInterval(_skipPollTimer); + _skipPollTimer=setInterval(function(){ + var dlg=document.getElementById('skip-dialog'); + if(!(dlg&&dlg.classList.contains('open')))return; + _refreshSkipDialog(); + },2000); document.getElementById('skip-dialog').classList.add('open'); } function _refreshSkipDialog(){ - // Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped - fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ - var s=d.result||{}; - _skipSvg=s.svg_b64||''; - _skipObjects=(s.objects||[]).map(function(n){ - return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false}; + // query-Endpoint wartet intern auf frischen skip/report (bis 1.5s). + fetch(_apiUrl('/kx/skip/query'),{method:'POST'}) + .then(function(r){return r.json();}) + .then(function(d){_applySkipDialogState(d.result||{});}) + .catch(function(){ + fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json();}).then(function(d){ + _applySkipDialogState(d.result||{}); + }).catch(function(){}); }); - renderSkipList(); renderSkipSvg(); - }); - // Frisch nachfragen (skipped-Liste aktualisieren) - fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){ - setTimeout(function(){ - fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ - var s=d.result||{}; - var skipped=s.skipped||[]; - _skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; }); - renderSkipList(); renderSkipSvg(); - }); - }, 500); - }).catch(function(){}); } function closeSkipDialog(){ + if(_skipPollTimer){clearInterval(_skipPollTimer);_skipPollTimer=null;} document.getElementById('skip-dialog').classList.remove('open'); } function _shortLabel(name){ diff --git a/web/themes/default/index.html b/web/themes/default/index.html index a783654..843d553 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -39,6 +39,7 @@ + @@ -521,6 +522,7 @@ + diff --git a/web/translations/it.json b/web/translations/it.json new file mode 100644 index 0000000..56ed376 --- /dev/null +++ b/web/translations/it.json @@ -0,0 +1,291 @@ +{ + "header_status_standby": "Pronto", + "header_status_printing": "In stampa", + "header_status_complete": "Completato", + "header_status_error": "Errore", + "kobra_free": "Pronto", + "kobra_busy": "Occupato", + "kobra_printing": "In stampa", + "kobra_preheating": "Preriscaldamento", + "kobra_auto_leveling": "Livellamento automatico", + "kobra_checking": "Verifica", + "kobra_updated": "Aggiornamento", + "kobra_init": "Inizializzazione", + "kobra_pausing": "Pausa in corso...", + "kobra_paused": "In pausa", + "kobra_resuming": "Ripresa...", + "kobra_resumed": "Ripreso", + "kobra_stopping": "Arresto...", + "kobra_stoped": "Arrestato", + "kobra_finished": "Finito", + "kobra_failed": "Errore", + "kobra_canceled": "Annullato", + "kobra_offline": "Offline", + "nav_dashboard": "Dashboard", + "nav_print": "Stampa", + "nav_temps": "Temperature", + "nav_motion": "Movimento", + "nav_ams": "AMS", + "nav_extras": "Luce / Ventola", + "nav_console": "Console", + "card_progress": "Avanzamento", + "card_temps": "Temperature", + "card_light_fan": "Ventola", + "card_speed": "Velocità di stampa", + "card_cam": "Camera", + "lbl_elapsed": "Trascorso:", + "lbl_remaining": "Rimanente:", + "lbl_slicer_time": "Stima slicer:", + "lbl_layers": "Layer", + "lbl_zpos": "Z (mm)", + "speed_silent": "🐢 Silenzioso", + "speed_normal": "⚡ Normale", + "speed_sport": "🚀 Sport", + "lbl_light": "💡 Luce", + "lbl_feed": "Carica", + "lbl_unload": "Rimuovi", + "card_ace_dry": "Essiccazione ACE", + "ace_dry_dryer": "Essiccatore", + "ace_dry_status_off": "Stato: Spento", + "ace_dry_status_on": "Stato: Attivo", + "ace_dry_status_remaining": "Rimanente", + "ace_dry_humidity": "Umidità", + "ace_dry_current_temp": "Temperatura", + "ace_dry_chart": "Cronologia (Temp/Umidità)", + "ace_dry_temp": "Temperatura (°C)", + "ace_dry_duration": "Durata (min)", + "ace_dry_start": "▶ Avvia", + "ace_dry_stop": "■ Ferma", + "ace_dry_auto_refill": "Ricarica automatica", + "ace_dry_enable": "Abilita essiccazione", + "ace_dry_temp_line": "Temperatura di essiccazione", + "ace_dry_time_line": "Tempo di essiccazione", + "ace_dry_ui_pending": "(Solo interfaccia, backend a seguire)", + "ace_dry_dialog_title": "Impostazioni Temp/Tempo essiccatore", + "ace_dry_dialog_temp": "Temperatura (30-80°C)", + "ace_dry_dialog_time": "Tempo rim. (h:m:s)", + "ace_dry_dialog_confirm": "Conferma", + "ace_dry_dialog_cancel": "Annulla", + "ace_dry_dialog_save_restart": "Salva e riavvia", + "ace_dry_dialog_custom_name": "Nome personalizzato", + "ace_dry_dialog_reset_default": "Ripristina predefiniti", + "ace_dry_preset_pla": "PLA", + "ace_dry_preset_pla_plus": "PLA+", + "ace_dry_preset_petg": "PETG", + "ace_dry_preset_tpu": "TPU", + "ace_dry_preset_abs_asa": "ABS / ASA", + "ace_dry_preset_pa_pc": "PA / PC", + "ace_dry_preset_custom": "Personalizzato", + "cam_placeholder": "📷 Camera non avviata", + "cam_stream_unavailable": "Flusso video non disponibile", + "btn_cam_start": "▶ Camera", + "btn_cam_stop": "◼ Camera", + "btn_pause": "⏸ Pausa", + "btn_resume": "▶ Riprendi", + "btn_cancel": "✕ Stop", + "label_nozzle": "Ugello", + "label_bed": "Piatto", + "label_fan": "🌀 Ventola", + "label_light": "💡 Luce", + "label_on_off": "On / Off", + "label_speed": "Velocità", + "panel_print_title": "Controllo stampa", + "panel_print_btn_pause": "⏸ Pausa", + "panel_print_btn_resume": "▶ Riprendi", + "panel_print_btn_cancel": "✕ Annulla", + "panel_print_temps_live": "Temperature (In tempo reale)", + "label_set": "Imposta", + "label_off": "Off", + "panel_temps_nozzle": "Ugello", + "panel_temps_bed": "Piatto riscaldato", + "panel_temps_chart": "Cronologia (ultime 60 letture)", + "label_target_c": "Target:", + "panel_motion_xy": "Assi XY", + "panel_motion_z": "Asse Z", + "label_step": "Ampiezza passo:", + "btn_home_z": "Home Z", + "btn_home_xy": "Home XY", + "btn_home_all": "Home generale", + "btn_disable_motors": "Spegni motori", + "panel_ams_title": "Filamento", + "card_ams": "Filamento", + "ams_no_data": "Nessun dato ricevuto dall' AMS", + "label_slot": "Slot", + "ams_empty": "Vuoto", + "panel_extras_light": "Luce", + "panel_extras_fan": "Ventola", + "panel_extras_camera": "Camera", + "btn_cam_start2": "▶ Avvia", + "btn_cam_stop2": "◼ Ferma", + "panel_console_title": "Registro eventi", + "log_light_on": "Luce accesa", + "log_light_off": "Luce spenta", + "log_fan": "Ventola →", + "log_nozzle": "Ugello →", + "log_bed": "Piatto →", + "log_axis": "Asse", + "log_home": "Home", + "log_home_all": "Home generale", + "log_cam_start": "Camera avviata:", + "log_cam_stop": "Camera arrestata", + "log_poll_error": "Errore di sincronizzazione:", + "log_error": "Errore:", + "confirm_cancel": "Annullare davvero la stampa?", + "settings_title": "Impostazioni", + "settings_connection": "Connessione", + "settings_print": "Impostazioni di stampa", + "settings_poll": "Intervallo di sincronizzazione (secondi)", + "nav_settings": "Impostazioni", + "settings_cat_display": "Aspetto", + "settings_cat_filament": "Filamento", + "settings_cat_language": "Lingua", + "settings_cat_theme": "Alterna chiaro / scuro", + "settings_filament_mapping": "Mappatura profilo filamento (per slot)", + "settings_filament_mapping_save": "Salva mappatura", + "settings_visible_vendors": "Produttori visibili (menu del profilo)", + "settings_visible_vendors_hint": "Solo questi produttori appariranno nel menu del profilo dello slot. Se non selezioni nulla = mostra tutti. I profili \"Generici\" e i tuoi personali sono sempre visibili.", + "settings_visible_vendors_save": "Salva selezione", + "progress_action_print": "Stampa", + "progress_action_slots": "Mappa slot", + "progress_action_clear": "Cancella", + "settings_version": "Versione", + "settings_save": "Salva e riavvia", + "settings_printer_name": "Nome stampante", + "settings_printer_ip": "IP stampante", + "settings_mqtt_port": "Porta MQTT", + "settings_username": "Nome utente MQTT", + "settings_password": "Password MQTT", + "settings_device_id": "ID dispositivo", + "settings_mode_id": "ID modalità", + "hint_ip_no_port": "Solo indirizzo IP, senza porta (es. 192.168.1.102)", + "settings_default_slot": "Slot predefinito (colore singolo)", + "settings_slot_auto": "Auto (tutti gli slot caricati)", + "settings_auto_leveling": "Livellamento automatico predefinito", + "fd_options_title": "Opzioni di stampa", + "print_auto_leveling": "Livellamento automatico", + "settings_file_ready_mode": "Comportamento all'avvio stampa", + "settings_file_ready_banner": "Barra di stampa", + "settings_file_ready_dialog": "Finestra di dialogo di stampa", + "settings_camera_on_print": "Attiva la camera all'avvio della stampa", + "settings_web_upload_warning": "Mostra un avviso quando si stampano caricamenti web", + "update_check": "Controlla aggiornamenti", + "update_checking": "Verifica in corso...", + "update_available": "disponibile", + "update_none": "Già aggiornato", + "update_apply": "Installa ora", + "update_applying": "Download in corso...", + "update_restarting": "Riavvio in corso...", + "update_error": "Errore", + "btn_connect": "⚡ Connetti", + "btn_disconnect": "✕ Disconnetti", + "lbl_conn_error": "Errore di connessione:", + "slot_edit_title": "Modifica slot", + "slot_edit_color": "Colore", + "slot_edit_material": "Materiale", + "slot_edit_load": "⬇ Carica", + "slot_edit_unload": "⬆ Rimuovi", + "slot_edit_save": "💾 Salva", + "slot_edit_custom": "es. PLA, PETG, ABS…", + "slot_edit_ok": "Slot AMS", + "slot_edit_profile": "Profilo OrcaSlicer", + "slot_edit_profile_hint": "Inviato durante la sincronizzazione con OrcaSlicer come marchio specifico invece di un semplice \"Generico\"", + "slot_edit_profile_default": "— Generico (predefinito) —", + "orca_profile_section": "Profili OrcaSlicer", + "orca_profile_hint": "Importa i tuoi profili di filamento OrcaSlicer (apri la cartella utente tramite Aiuto → Mostra cartella di configurazione)", + "orca_profile_import_btn": "Importa profili", + "orca_profile_import_link": "★ Importa i tuoi profili…", + "orca_profile_import_title": "Importa i tuoi profili OrcaSlicer", + "orca_profile_help_html": "Carica un file ZIP della tua cartella filamenti di OrcaSlicer o file singoli .json.
In OrcaSlicer: Aiuto → Mostra cartella di configurazione → user/<id>/filament/", + "orca_profile_dropmsg": "Trascina qui o fai clic", + "orca_profile_list_label": "Attualmente importati", + "orca_profile_user_label": "Profili personali", + "orca_profile_user_empty": "– nessuno –", + "orca_profile_uploading": "Caricamento in corso…", + "orca_profile_done": "Importato", + "orca_profile_skipped": "saltato", + "log_dir_all": "Tutti", + "log_dir_rx": "RX", + "log_dir_tx": "TX", + "log_dir_label": "Dir:", + "log_lvl_label": "Livello:", + "log_lvl_err": "⛔ Errori", + "log_lvl_warn": "⚠ Avvisi", + "log_topic_label": "Argomento:", + "log_topic_ams": "AMS", + "log_topic_print": "Stampa", + "log_topic_info": "Info", + "log_topic_status": "Stato", + "log_download": "⬇ Scarica", + "log_auto": "⬇ Auto", + "log_clear": "✕ Cancella", + "log_filter_placeholder": "Filtra…", + "file_ready_btn": "▶ Avvia stampa", + "file_slots_btn": "🎨 Seleziona slot", + "file_cancel_btn": "✕ Annulla", + "nav_printers": "Stampanti", + "skip_title": "✂ Salta oggetti", + "skip_hint": "Deseleziona gli oggetti che non vuoi più stampare:", + "skip_btn_label": "Oggetti", + "skip_no_objects": "Nessun oggetto in questa stampa.", + "skip_already": "saltato", + "skip_cancel": "Annulla", + "skip_confirm": "Salta", + "skip_select_at_least_one": "Seleziona almeno un oggetto.", + "skip_sending": "Invio in corso …", + "skip_success": "Gli oggetti verranno saltati.", + "fd_objects_hint": "Salta oggetti (opzionale):", + "fd_objects_toggle": "Salta oggetti", + "fd_slots_hint": "Assegna il canale GCode allo slot AMS:", + "fd_cancel": "Annulla", + "fd_print": "▶ Stampa", + "fd_no_slots_msg": "Nessuno slot AMS caricato.{br}Avviare comunque la stampa?", + "fd_slot": "Slot", + "fd_no_matching_material": "Nessun materiale corrispondente", + "fd_used": "USATO", + "add_printer": "Aggiungi stampante", + "apd_lbl_ip": "IP stampante", + "apd_lbl_name": "Nome (opzionale)", + "apd_placeholder_name": "es. Kobra X Soggiorno", + "apd_cancel": "Annulla", + "apd_confirm": "Aggiungi", + "apd_fetching": "Recupero dati dalla stampante…", + "apd_success": "Stampante aggiunta, riavvio del bridge in corso…", + "apd_err_ip": "Inserisci un indirizzo IP", + "printers_remove": "Rimuovi stampante", + "printers_remove_confirm": "Rimuovere la stampante \"{name}\"? Il bridge si riavvierà.", + "printers_active": "● attiva", + "printers_switch": "Cambia →", + "printers_current": "Stampante corrente", + "printers_loading": "Caricamento in corso…", + "printers_none": "Nessuna stampante configurata.", + "printers_empty_hint": "Nessuna stampante ancora configurata.", + "nav_browser": "Browser", + "panel_browser_title": "Browser dei file", + "store_search_placeholder": "🔍 Cerca…", + "store_empty": "Nessun file caricato.", + "store_refresh": "↻ Aggiorna", + "store_print": "▶ Stampa", + "store_download": "⬇ Scarica", + "store_delete_confirm": "Eliminare il file?", + "store_print_confirm": "Stampare il file?", + "store_web_verify_title": "Verifica file", + "store_web_verify_msg": "Verifica che questo file sia stato creato per Anycubic Kobra X.", + "store_web_verify_confirm": "Conferma", + "store_web_verify_abort": "Interrompi", + "store_no_results": "Nessun file trovato.", + "store_never": "mai stampato", + "store_estimate": "Stima", + "store_upload_label_prefix": "Trascina il GCode qui o ", + "store_upload_label_browse": "sfoglia", + "store_upload_busy": "⏳ Caricamento in corso…", + "store_upload_success": "✓ {file}", + "store_upload_error": "✗ {error}", + "store_upload_only_gcode": "✗ Sono consentiti solo file GCode (.gcode, .3mf, .bgcode)", + "sf_all": "Tutti", + "sf_ok": "✓ Completato", + "sf_err": "✗ Fallito", + "sf_new": "Nuovo", + "ss_date": "↓ Data", + "ss_name": "Nome A–Z", + "ss_dur": "⏱ Tempo di stampa" +} \ No newline at end of file