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+='
'
@@ -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