Compare commits

..

6 Commits

Author SHA1 Message Date
Gangoke
c91ba66801 feat: rename file_ready_dialog to print_start_dialog for consistency 2026-06-16 00:57:36 -10:00
Gangoke
9517493c39 revert docker-compose.yml to image 2026-06-16 00:44:52 -10:00
Gangoke
5d691c6153 feat: auto-leveling in print start dialog
fix: add missing translation mappings
2026-06-16 00:43:00 -10:00
Gangoke
2a02c598b5 feat: Start/Clear buttons in Progress section
fix: file uploaded during print replacing currently printing in progress section
2026-06-15 22:47:17 -10:00
Gangoke
e64aebd145 fix skip object from print dialog 2026-06-15 22:25:38 -10:00
Gangoke
778c4ce85a Auto Print Dialog checkpoint: setting and basic function 2026-06-15 21:34:13 -10:00
14 changed files with 636 additions and 1144 deletions

View File

@@ -1,59 +1,5 @@
# Changelog
## [0.9.24] 2026-06-16
### Neu
- **Objekte überspringen in jedem Druck-Flow (Issue #57).** Der „Objekte
überspringen"-Bereich im Slot-Mapper erschien bisher nur beim Druck aus dem
Browser-Tab. Er ist jetzt in allen Flows verfügbar (inkl. Upload / Print-Leiste),
standardmäßig eingeklappt hinter einem `✂ Objekte überspringen (N)`-Header, damit
der Dialog kompakt bleibt — Klick klappt Vorschau + Checkliste auf.
- **Slot-Mapper zeigt konkreten Profilnamen (Issue #57).** Jeder Slot zeigt nun das
zugeordnete Filament-Profil (z.B. „PolyTerra PLA — Polymaker") in den Dropdown-
Optionen und als Hover-Tooltip am Slot-Marker, statt nur des generischen Typs.
Fällt auf den generischen Typ zurück, wenn kein Profil gemappt ist.
## [0.9.23] 2026-06-16
### Neu
- **Druckdialog nach Upload automatisch öffnen.** Eine neue Einstellung
`print_start_dialog` (Einstellungen → Drucker → „Druckstart-Verhalten") steuert,
was nach einem Upload bei leerlaufendem Drucker passiert: „Print-Dialog" öffnet
den Slot-Zuordnungs-Dialog automatisch, „Print-Leiste" behält das bisherige
Banner. Basiert auf PR #56 von @gangoke.
- **Auto-Leveling-Schalter pro Druck.** Der Druckdialog hat jetzt eine eigene
Auto-Leveling-Checkbox, die den globalen Standard für einen einzelnen Druck
überschreibt.
### Behoben
- **Objekt-Skip wurde beim Druckstart still ignoriert (PR #56, @gangoke).** Der
Skip-Befehl wurde gesendet, *bevor* der Drucker im `printing`-Status war, und
daher verworfen. Der Skip wird nun in einer Retry-Schleife erneut angewendet,
sobald der Druck bestätigt läuft — mit einer Pending-Sperre, damit die UI den
Skip-Status nicht vorzeitig zurücksetzt.
- **Upload während eines laufenden Drucks überschrieb die Vorschau des laufenden
Auftrags.** Ein neuer Upload während des Drucks ersetzt nicht mehr Thumbnail /
file_ready des Auftrags auf dem Druckbett.
## [0.9.22] 2026-06-16
### Neu
- **Neu strukturiertes Einstellungs-Panel.** Das Einstellungs-Modal wurde durch
ein dauerhaftes Master-Detail-Panel mit fünf Kategorien ersetzt: Verbindung,
Drucker, Darstellung, Filament und System. Das Poll-Intervall ist nun live
einstellbar.
- **Vendor-Sichtbarkeitsfilter (Issue #41).** Eine neue Checkliste in den
Filament-Einstellungen beschränkt das Slot-Profil-Dropdown auf bestimmte
Hersteller. „Generic" und eigene importierte Profile sind immer sichtbar.
- **Idle-Datei-Aktionen in der Fortschritts-Karte (Issue #55).** Nach einem
Upload bei leerlaufendem Drucker erscheinen drei Schnellaktionen direkt in der
Fortschritts-Karte: ▶ Drucken, ⚙ Slots zuordnen und ✕ Leeren.
### Behoben
- **Mobileraker-Kompatibilität (Issue #48).** Absturz in `ConfigExtruder.fromJson`
(leeres `configfile.config`), Hänger beim Refresh (Metadata-Endlosschleife) und
fehlende ETA/Restzeit behoben.
## [0.9.21] 2026-06-14
### Behoben

View File

@@ -1,77 +1,5 @@
# Changelog
## [0.9.24] 2026-06-16
### New
- **Skip Objects available in every print flow (issue #57).** The "Skip objects"
panel in the Slot Mapper used to appear only when printing from the Browser tab.
It now shows in all flows (upload / print bar included), collapsed by default
behind a `✂ Skip objects (N)` header to keep the dialog compact, expanding on
click with the object preview and checklist.
- **Slot Mapper shows the specific profile name (issue #57).** Each slot now
displays its mapped filament profile (e.g. "PolyTerra PLA — Polymaker") in the
dropdown options and as a hover tooltip on the slot marker, instead of just the
generic type. Falls back to the generic type when no profile is mapped.
## [0.9.23] 2026-06-16
### New
- **Auto-open print dialog after upload.** A new `print_start_dialog` setting
(Settings → Printer → "Start Print Behavior") controls what happens after a
file is uploaded while the printer is idle: `Print Dialog` opens the
slot-assignment dialog automatically, `Print Bar` keeps the previous banner
behaviour. Based on PR #56 by @gangoke.
- **Per-print auto-leveling toggle.** The print dialog now has its own
auto-leveling checkbox that overrides the global default for a single print.
### Fixed
- **Object skip was silently ignored at print start (PR #56, @gangoke).** The
skip command was sent *before* the printer entered the `printing` state, so it
was dropped. The skip is now re-applied in a retry loop once the print is
confirmed running, with a pending-lock so the UI doesn't reset the skip state
prematurely.
- **Upload during an active print overwrote the running job's preview.**
Uploading a new file while printing no longer replaces the thumbnail /
file_ready of the job currently on the bed.
## [0.9.22] 2026-06-16
### New
- **Restructured Settings panel.** The settings modal has been replaced by a
persistent Master-Detail panel with five categories: Connection, Printer,
Appearance, Filament, and System. Poll interval is now adjustable live.
- **Vendor visibility filter (issue #41).** A new checklist in the Filament
settings lets you restrict the slot profile dropdown to specific manufacturers.
"Generic" and your own imported profiles are always visible. The list updates
automatically after a profile import.
- **Idle file actions in the progress card (issue #55).** After uploading a file
while the printer is idle, three quick-action buttons appear directly in the
progress card: ▶ Print, ⚙ Map Slots, and ✕ Clear — matching the file browser
workflow without navigating away.
### Fixed
- **Mobileraker: app crashed with `Null is not a subtype of Object` in
`ConfigExtruder.fromJson` (issue #48).** `configfile.config` was returned as
an empty object `{}`. Mobileraker parses both `configfile.settings` and
`configfile.config` through the same strict Dart parser — both are now
populated with the same extruder/bed/stepper stub.
- **Mobileraker: app hung indefinitely on refresh (issue #48).** The WebSocket
`server.files.metadata` handler called a non-existent store method
(`get_file_by_filename`), always returning empty metadata. Mobileraker retried
this thousands of times per second. Both the HTTP and WS paths now share a
single `_build_file_metadata()` method.
- **Mobileraker: ETA / remaining time not shown (issue #48).** A side effect of
the metadata loop fix — once `currentFile` resolves, Mobileraker can calculate
ETA from `estimated_time`.
- **Mobileraker: `notify_status_update` triggered repeated `ConfigFile.parse`
(issue #48).** Static objects (`configfile`, `webhooks`, `heaters`, `history`)
were included in every live status push. They are now filtered out; only live
telemetry is broadcast.
- `motion_report` (`live_position`, `live_velocity`) added to printer objects
for Mobileraker motion display.
- Saving filament slot profiles no longer silently drops the `visible_vendors`
setting from `config.ini`.
## [0.9.21] 2026-06-14
### Fixed

View File

@@ -1 +1 @@
0.9.24
0.9.21

View File

@@ -34,6 +34,9 @@ auto_leveling = 1
# Kamera-Stream bei Druckstart automatisch einschalten (1 = an, 0 = aus)
camera_on_print = 0
# Statt grünem Ready-Banner den Filament/Color-Selector automatisch öffnen (1 = an, 0 = aus)
print_start_dialog = 1
# Warnung vor Druck von Web-Uploads (1 = an, 0 = aus)
web_upload_warning = 1

View File

@@ -60,8 +60,8 @@ def _load_config_file(path: pathlib.Path):
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
}
for env_key, (section, option) in mapping.items():
@@ -73,8 +73,7 @@ def _load_config_file(path: pathlib.Path):
except (configparser.NoSectionError, configparser.NoOptionError):
pass
# Backward compatibility: old key FILE_READY_DIALOG → PRINT_START_DIALOG
# Backward compatibility: legacy option and env var
if "PRINT_START_DIALOG" not in os.environ:
try:
legacy = cfg.get(CONFIG_SECTION_PRINT, "file_ready_dialog")
@@ -111,6 +110,7 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
"print_start_dialog": env_vals.get("PRINT_START_DIALOG", env_vals.get("FILE_READY_DIALOG", "1")),
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
}
cfg[CONFIG_SECTION_BRIDGE] = {
@@ -240,17 +240,10 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
return False
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
preserved_vendors = None
if cfg.has_option("filament_profiles", "visible_vendors"):
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
if cfg.has_section("filament_profiles"):
cfg.remove_section("filament_profiles")
if profiles or preserved_vendors:
if profiles:
cfg["filament_profiles"] = {}
if preserved_vendors:
cfg["filament_profiles"]["visible_vendors"] = preserved_vendors
for slot_idx in sorted(profiles.keys()):
entry = profiles[slot_idx] or {}
if entry.get("vendor"):
@@ -264,43 +257,6 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
return True
def list_visible_vendors() -> list[str]:
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
"""
path = _find_config_file()
if not path:
return []
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if not cfg.has_option("filament_profiles", "visible_vendors"):
return []
raw = cfg.get("filament_profiles", "visible_vendors")
return [v.strip() for v in raw.split(",") if v.strip()]
def save_visible_vendors(vendors: list[str]) -> bool:
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder."""
path = _find_config_file()
if not path:
return False
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if not cfg.has_section("filament_profiles"):
cfg.add_section("filament_profiles")
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
if clean:
cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean)
elif cfg.has_option("filament_profiles", "visible_vendors"):
cfg.remove_option("filament_profiles", "visible_vendors")
with open(path, "w", encoding="utf-8") as f:
cfg.write(f)
return True
def get(key: str, default: str = "") -> str:
return os.environ.get(key, default)
@@ -315,5 +271,5 @@ 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"))
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG","1")))
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))

View File

@@ -34,7 +34,6 @@ except ImportError:
import env_loader
import asyncio
import hashlib
import copy
import json
import logging
import os
@@ -752,13 +751,6 @@ class KobraXBridge:
self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles()
except Exception:
self._filament_profiles = {}
# Vendor-Sichtbarkeitsfilter fürs Slot-Profil-Dropdown (Issue #41 Option A).
# Leere Liste = alle Vendoren sichtbar (rückwärtskompatibel).
try:
import config_loader as _cl
self._visible_vendors: list[str] = _cl.list_visible_vendors()
except Exception:
self._visible_vendors = []
self._last_state: dict = {}
self._state = {
"nozzle_temp": 0.0,
@@ -791,7 +783,7 @@ class KobraXBridge:
"print_speed_mode": 2,
"connection_error": "",
"file_ready": "",
"print_start_dialog": getattr(args, "print_start_dialog", 1),
"print_start_dialog": getattr(args, "print_start_dialog", 0),
"filament_mode": "toolhead",
"ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None},
}
@@ -815,7 +807,7 @@ class KobraXBridge:
# Part-Skip: zuletzt vom Drucker gemeldete Skip-Liste (v0.9.10)
self._skip_state: dict = {"objects": [], "skipped": [], "ts": 0}
# Pre-Print-Skip: pending until printer enters printing state
# Pre-Print-Skip: pending until confirmed/applied via skip/start
self._pending_preprint_skip: list[str] = []
self._pending_preprint_skip_deadline: float = 0.0
@@ -1071,30 +1063,31 @@ class KobraXBridge:
"""
d = payload.get("data") or {}
skipped = d.get("objects_skip_parts") or d.get("skipped") or d.get("skipped_parts") or []
# Während ein Pre-Print-Skip noch pending ist, leere Früh-Reports ignorieren
# damit die UI nicht sofort zurückspringt bevor der Drucker den Skip bestätigt.
# Während ein Pre-Print-Skip noch als pending markiert ist, ignorieren wir
# leere Früh-Reports kurzzeitig, damit die UI nicht sofort zurückspringt.
now = time.time()
if (not skipped and self._pending_preprint_skip
and now <= self._pending_preprint_skip_deadline):
return
# 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 = []
self._pending_preprint_skip_deadline = 0.0
# Liste übernehmen (auch leer), sobald kein Pending-Lock aktiv ist.
self._skip_state = {
"skipped": list(skipped),
"ts": int(time.time()),
}
if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip):
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
if payload.get("state") == "done" or payload.get("code") == 200:
log.info(f"Skip-Antwort: state={payload.get('state')} code={payload.get('code')} skipped={skipped}")
def _on_file(self, payload: dict):
d = payload.get("data") or {}
details = d.get("file_details") or {}
thumb = details.get("thumbnail") or details.get("png_image") or ""
file_name = d.get("filename") or details.get("filename") or self._last_uploaded_file
active_print = self._state.get("print_state") in ("printing", "paused")
current_print_file = self._state.get("filename") or ""
thumb = details.get("thumbnail") or details.get("png_image") or ""
# Uploads während eines laufenden Drucks dürfen die aktive
# Fortschritts-Vorschau nicht überschreiben.
if thumb and (not active_print or (file_name and file_name == current_print_file)):
@@ -1113,33 +1106,6 @@ class KobraXBridge:
log.warning(f"update_file_objects fehlgeschlagen: {e}")
self._push_status_update()
def _apply_preprint_skip_after_start(self, names: list[str], retries: int = 20, delay_s: float = 0.75):
"""Sendet Skip-Befehl erst nachdem Drucker in printing-State gewechselt hat.
Vorher sendet der Drucker den Befehl ins Leere (kein aktiver Druck).
"""
wanted = [str(n) for n in (names or []) if isinstance(n, str) and n]
if not wanted:
return False
for i in range(max(1, int(retries))):
try:
if self._state.get("kobra_state") != "printing":
time.sleep(max(0.1, float(delay_s)))
continue
resp = self.client.skip_objects(wanted)
if resp is not None:
log.info(f"Pre-Print skip applied ({len(wanted)} objects) on attempt {i+1}/{retries}")
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
return True
except Exception as e:
log.debug(f"Pre-Print skip attempt {i+1}/{retries} failed: {e}")
time.sleep(max(0.1, float(delay_s)))
log.warning(f"Pre-Print skip could not be confirmed after {retries} attempts")
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
return False
@staticmethod
def _detect_filament_mode(boxes: list, head_tools_model: int = -1) -> str:
"""Detect active filament topology mode.
@@ -1747,22 +1713,13 @@ class KobraXBridge:
# WebSocket push
# -------------------------------------------------------------------------
# Statische Objekte, die sich zur Laufzeit nie ändern. Sie werden einmalig
# per objects.query/subscribe ausgeliefert, aber NICHT in jedem
# notify_status_update mitgeschickt — sonst läuft Mobilerakers
# ConfigFile.parse (teuer + strikt) bei jedem Status-Tick erneut und die App
# hängt/crasht beim Refresh (Issue #48).
_STATIC_STATUS_OBJECTS = ("configfile", "webhooks", "heaters", "history")
def _push_status_update(self):
if not self.ws_clients:
return
objs = self._build_printer_objects()
live = {k: v for k, v in objs.items() if k not in self._STATIC_STATUS_OBJECTS}
msg = {
"jsonrpc": "2.0",
"method": "notify_status_update",
"params": [live, time.time()],
"params": [self._build_printer_objects(), time.time()],
}
text = json.dumps(msg)
dead = set()
@@ -1920,17 +1877,6 @@ class KobraXBridge:
"homing_origin": [0, 0, 0, 0],
"position": [0, 0, self._estimate_current_z(), 0],
},
# motion_report: Mobileraker liest die Live-Geschwindigkeit hier
# (live_velocity). Der Kobra-X-MQTT liefert KEINE echte mm/s, nur
# einen print_speed_mode (1-4). live_velocity bleibt daher 0 — das
# Objekt muss aber existieren, sonst zeigt Mobileraker nichts an
# (motion_report war zuvor null). live_position spiegelt die
# geschätzte Z-Höhe (wie gcode_move).
"motion_report": {
"live_position": [0, 0, self._estimate_current_z(), 0],
"live_velocity": 0.0,
"live_extruder_velocity": 0.0,
},
"fan": {
"speed": (int(s.get("fan_speed") or 0)) / 100.0,
"rpm": None,
@@ -1967,78 +1913,40 @@ class KobraXBridge:
# configfile stub — Mobileraker und andere Clients crashen ohne
# dieses Objekt (Missing field: configFile). Werte aus der
# entschlüsselten avata_main.conf (ACCFG1.0 — Kobra X Firmware).
# Mobileraker (Issue #48) parst BEIDE Zweige config + settings durch
# denselben ConfigFile.parse → ConfigExtruder.fromJson; ein leeres
# config:{} ließ den nicht-nullbaren Dart-Parser crashen. Daher wird
# config identisch zu settings gespiegelt.
"configfile": self._klipper_configfile_stub(),
}
def _klipper_configfile_stub(self) -> dict:
"""Minimaler Klipper-configfile-Stub für Mobileraker/OctoApp (Issue #48).
Mobileraker parst BEIDE Zweige `config` und `settings` durch denselben
ConfigFile.parse → ConfigExtruder.fromJson. Ein leeres `config: {}`
ließ den nicht-nullbaren Dart-Parser crashen, daher wird `config`
identisch zu `settings` gespiegelt. Werte aus der entschlüsselten
avata_main.conf (ACCFG1.0 — Kobra X Firmware).
"""
settings = {
"printer": {
"kinematics": "cartesian",
"max_velocity": 450,
"max_accel": 10000,
"max_z_velocity": 12,
"max_z_accel": 100,
"square_corner_velocity": 20.0,
"configfile": {
"config": {},
"settings": {
"printer": {
"kinematics": "cartesian",
"max_velocity": 450,
"max_accel": 10000,
"max_z_velocity": 12,
"max_z_accel": 100,
"square_corner_velocity": 20.0,
},
"extruder": {
"nozzle_diameter": 0.4,
"filament_diameter": 1.75,
"sensor_type": "ATC Semitec 104GT-2",
"min_temp": 0,
"max_temp": 320,
"min_extrude_temp": 10,
},
"heater_bed": {
"min_temp": 0,
"max_temp": 120,
},
"stepper_x": {"position_min": -18.5, "position_max": 280},
"stepper_y": {"position_min": -6.5, "position_max": 272.5},
"stepper_z": {"position_min": -4, "position_max": 262},
"virtual_sdcard": {"path": "/data/gcodes"},
"pause_resume": {},
"display_status": {},
},
"warnings": [],
"save_config_pending": False,
"save_config_pending_items": {},
},
"extruder": {
"nozzle_diameter": 0.4,
"filament_diameter": 1.75,
"sensor_type": "ATC Semitec 104GT-2",
"min_temp": 0,
"max_temp": 320,
"min_extrude_temp": 10,
# Mobileraker ConfigExtruder erwartet diese Felder non-nullable
# (max_extrude_only_distance, max_power) bzw. als Key präsent
# (max_extrude_only_velocity/accel dürfen null sein). Fehlen =
# Crash in ConfigExtruder.fromJson (Issue #48).
"max_extrude_only_distance": 100.0,
"max_power": 1.0,
"max_extrude_only_velocity": None,
"max_extrude_only_accel": None,
},
"heater_bed": {
# Mobileraker ConfigHeaterBed: heater_pin, sensor_type, control
# sind non-nullable. Werte sind Platzhalter (Bridge kennt die
# echten Pins nicht — Anycubic-Firmware, kein Klipper-printer.cfg).
"heater_pin": "PA0",
"sensor_type": "ATC Semitec 104GT-2",
"control": "pid",
"min_temp": 0,
"max_temp": 120,
},
# stepper_* mit non-nullable Pflichtfeldern (step_pin, dir_pin,
# rotation_distance) füllen, sonst crasht ConfigStepper.fromJson.
"stepper_x": {"step_pin": "PA1", "dir_pin": "PA2", "rotation_distance": 40,
"position_min": -18.5, "position_max": 280},
"stepper_y": {"step_pin": "PA3", "dir_pin": "PA4", "rotation_distance": 40,
"position_min": -6.5, "position_max": 272.5},
"stepper_z": {"step_pin": "PA5", "dir_pin": "PA6", "rotation_distance": 8,
"position_min": -4, "position_max": 262},
"virtual_sdcard": {"path": "/data/gcodes"},
"pause_resume": {},
"display_status": {},
}
# config + settings müssen dieselben Felder enthalten — Mobileraker
# parst beide. deepcopy, damit kein Client durch geteilte Referenz
# versehentlich beide Zweige mutiert.
return {
"config": copy.deepcopy(settings),
"settings": settings,
"warnings": [],
"save_config_pending": False,
"save_config_pending_items": {},
}
# -------------------------------------------------------------------------
@@ -2358,31 +2266,6 @@ class KobraXBridge:
"name": entry.get("name", ""),
"id": entry.get("id", "")})
async def handle_kx_visible_vendors(self, request):
"""GET/POST /kx/filament/visible_vendors — Vendor-Sichtbarkeitsfilter
fürs Slot-Profil-Dropdown (Issue #41 Option A).
GET → {"result": ["Polymaker", "eSUN", ...]}
POST {"vendors": [...]} → speichert in config.ini [filament_profiles]
visible_vendors. Leere Liste = alle sichtbar. KEIN Bridge-Neustart
nötig (nur Anzeigefilter)."""
if request.method == "POST":
try:
data = await request.json()
except Exception:
data = {}
vendors = data.get("vendors") or []
if not isinstance(vendors, list):
return self._json_cors({"error": "vendors must be a list"}, status=400)
self._visible_vendors = [str(v).strip() for v in vendors if str(v).strip()]
try:
import config_loader as _cl
_cl.save_visible_vendors(self._visible_vendors)
except Exception as e:
log.warning(f"save_visible_vendors failed: {e}")
return self._json_cors({"error": str(e)}, status=500)
return self._json_cors({"result": self._visible_vendors})
def _load_orca_filaments(self) -> list[dict]:
"""Lädt System- + User-Profile aus dem Cache. System-Profile kommen
aus bridge/data/orca_filaments.json (Image-embedded), User-Profile
@@ -2569,6 +2452,15 @@ class KobraXBridge:
excluded_objects = body.get("excluded_objects") or []
if not isinstance(excluded_objects, list):
excluded_objects = []
# Pre-Print-Auswahl sofort im UI-Status spiegeln (wird später durch
# skip/report vom Drucker überschrieben/aktualisiert).
self._skip_state = {"skipped": list(excluded_objects), "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
if assignments:
ams_box_mapping, unused_count, invalid_count = self._build_assigned_ams_box_mapping(assignments)
@@ -2611,19 +2503,13 @@ class KobraXBridge:
"ai_settings": {"status": 0, "count": 0, "type": 1},
"timelapse": {"status": 0, "count": 0, "type": 64},
"drying_settings": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0},
# Firmware-Variante A (funktioniert bei mehreren Builds)
"objects_skip_parts": excluded_objects,
# Firmware-Variante B (beibehalten für Kompatibilität)
"model_objects_skip_parts": excluded_objects,
},
}
# Pre-Print-Skip sofort im UI-Status spiegeln
self._skip_state = {"skipped": list(excluded_objects), "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"KX-Store Druckstart: {filename} ams={len(ams_box_mapping)} slots assignments={bool(assignments)} excluded={len(excluded_objects)}")
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
@@ -2739,16 +2625,18 @@ class KobraXBridge:
})
return web.json_response({"result": files})
def _build_file_metadata(self, filename: str) -> dict:
"""Baut die Moonraker-file-metadata für eine Datei. Gemeinsame Quelle
für HTTP /server/files/metadata UND den WS-RPC server.files.metadata
(vorher hatte der WS-Pfad eigene, kaputte Logik mit einer nicht
existierenden Store-Methode → leere Antwort → Mobileraker fragte in
Endlosschleife, App hing beim Refresh, Issue #48).
async def handle_files_metadata(self, request):
"""Moonraker /server/files/metadata — moonraker-obico-Plugin holt das
einmal pro Druck und liest daraus `object_height` (für `currentZ`-
Anzeige im Obico-UI: `mmProgress` braucht maxZ), `layer_count`,
`layer_height` und `first_layer_height` (für die Layer-Berechnung).
Liefert Mobileraker-kompatible Pflichtfelder: `filename`, `size`,
`modified` sind in GCodeFile non-nullable; `print_start_time` und die
Slicer-Felder optional."""
Quelle: aktueller `_state` + GCode-Store-Eintrag wenn vorhanden.
Wenn Layer-Heights weder im State noch im Store sind, Fallback auf die
OrcaSlicer-Default-Filename-Heuristik (`_layer_height_from_filename`)."""
filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "")
if not filename:
return web.json_response({"result": {}})
s = self._state
layer_h = float(s.get("layer_height") or 0.0)
first_h = float(s.get("first_layer_height") or 0.0)
@@ -2772,27 +2660,16 @@ class KobraXBridge:
if layer_h and not first_h:
first_h = layer_h
object_height = round(first_h + max(0, total_layers - 1) * layer_h, 3) if (layer_h and total_layers) else 0.0
return {
return web.json_response({"result": {
"filename": filename,
# GCodeFile (Mobileraker) verlangt size als non-nullable int.
"size": size_bytes or 1,
"size": size_bytes,
"modified": time.time(),
"estimated_time": est_time or None,
"layer_height": layer_h or None,
"first_layer_height": first_h or None,
"layer_count": total_layers or None,
"object_height": object_height or None,
"thumbnails": [],
}
async def handle_files_metadata(self, request):
"""Moonraker /server/files/metadata — moonraker-obico + Mobileraker
holen Datei-Metadaten (Slicer-Zeit, Layer, object_height).
Logik in _build_file_metadata (gemeinsam mit WS-RPC)."""
filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "")
if not filename:
return web.json_response({"result": {}})
return web.json_response({"result": self._build_file_metadata(filename)})
}})
# ── Moonraker-Stubs für moonraker-obico ──────────────────────────────────
async def handle_access_api_key(self, request):
@@ -2898,6 +2775,7 @@ class KobraXBridge:
ct = request.headers.get("Content-Type", "")
if "multipart" not in ct:
return web.json_response({"error": "expected multipart"}, status=400)
active_print = self._state.get("print_state") in ("printing", "paused")
auto_print = False
web_upload = False
reader = await request.multipart()
@@ -2930,12 +2808,15 @@ class KobraXBridge:
# Slicer-Zeitschätzung + Thumbnail aus GCode auslesen
est_time = _parse_gcode_estimated_time(file_data)
self._state["slicer_time"] = est_time
thumbnail_b64 = _extract_thumbnail(file_data)
gcode_filaments = _extract_filament_info(file_data)
layer_h, first_h = _parse_gcode_layer_heights(file_data)
self._state["layer_height"] = layer_h
self._state["first_layer_height"] = first_h
# Aktiven Druck nicht mit Metadaten eines neu hochgeladenen Files
# überschreiben (Fortschrittsansicht muss beim laufenden Job bleiben).
if not active_print:
self._state["slicer_time"] = est_time
self._state["layer_height"] = layer_h
self._state["first_layer_height"] = first_h
# Datei persistent im GCode-Store ablegen
self._store.save_file(
@@ -2976,9 +2857,12 @@ class KobraXBridge:
if not auto_print:
auto_print = request.rel_url.query.get("print", "false").lower() == "true"
# Thumbnail immer anfordern (Drucker antwortet async mit file/report)
self._thumbnail_b64 = ""
self.client.publish("file", "fileDetails", {"root": "local", "filename": remote_filename}, timeout=0)
# Thumbnail nur dann im Runtime-State aktualisieren, wenn kein aktiver
# Druck läuft. Sonst könnte das Upload-Thumbnail die laufende
# Fortschrittsvorschau verdrängen.
if not active_print:
self._thumbnail_b64 = ""
self.client.publish("file", "fileDetails", {"root": "local", "filename": remote_filename}, timeout=0)
self._state["last_upload_url"] = serve_url
self._state["last_upload_md5"] = file_md5
@@ -3016,6 +2900,10 @@ class KobraXBridge:
def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0,
gcode_filaments: list | None = None):
self._state["file_ready"] = ""
# Neuer Druck ohne Pre-Print-Excludes: Skip-State zurücksetzen.
self._skip_state = {"skipped": [], "ts": int(time.time())}
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
loaded = self._select_loaded_slots_for_print(warn_on_empty_default=True)
# Nur die im GCode TATSÄCHLICH genutzten Paints auf Slots mappen. OrcaSlicer
@@ -3039,7 +2927,7 @@ class KobraXBridge:
use_ams = len(loaded) > 0
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded)
log.debug(f"AMS-Slots: {len(loaded)} gemappt (genutzte Paints: {used_paint_indices}) → {[i for i, _ in loaded]}")
auto_leveling = getattr(self._args, "auto_leveling", 1)
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
payload = {
"taskid": "-1",
"url": url,
@@ -3071,6 +2959,37 @@ class KobraXBridge:
else:
log.warning("Druckstart: keine Antwort vom Drucker")
def _apply_preprint_skip_after_start(self, names: list[str], retries: int = 20, delay_s: float = 0.75):
wanted = [str(n) for n in (names or []) if isinstance(n, str) and n]
if not wanted:
return False
self._pending_preprint_skip = list(wanted)
self._pending_preprint_skip_deadline = time.time() + max(3.0, float(retries) * float(delay_s) + 2.0)
# UI should already reflect intended skips immediately.
self._skip_state = {"skipped": list(wanted), "ts": int(time.time())}
for i in range(max(1, int(retries))):
try:
if self._state.get("kobra_state") != "printing":
time.sleep(max(0.1, float(delay_s)))
continue
resp = self.client.skip_objects(wanted)
if resp is not None:
log.info(f"Pre-Print skip applied ({len(wanted)} objects) on attempt {i+1}/{retries}")
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
return True
except Exception as e:
log.debug(f"Pre-Print skip attempt {i+1}/{retries} failed: {e}")
time.sleep(max(0.1, float(delay_s)))
log.warning(f"Pre-Print skip could not be confirmed after {retries} attempts")
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
return False
def _theme_index_path(self) -> str:
return os.path.join(_WEB_BASE, "web", "themes", self._ui_theme, "index.html")
@@ -3114,6 +3033,15 @@ class KobraXBridge:
excluded_objects = body.get("excluded_objects") or []
if not isinstance(excluded_objects, list):
excluded_objects = []
# Pre-Print-Auswahl sofort im UI-Status spiegeln (wird später durch
# skip/report vom Drucker überschrieben/aktualisiert).
self._skip_state = {"skipped": list(excluded_objects), "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
if filament_assignments is not None:
ams_box_mapping, unused_count, invalid_count = self._build_assigned_ams_box_mapping(filament_assignments)
if unused_count:
@@ -3127,7 +3055,7 @@ 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)
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", "")
@@ -3153,6 +3081,9 @@ class KobraXBridge:
"ai_settings": {"status": 0, "count": 0, "type": 0},
"timelapse": {"status": 0, "count": 0, "type": 0},
"drying_settings": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0},
# Firmware-Variante A (funktioniert bei mehreren Builds)
"objects_skip_parts": excluded_objects,
# Firmware-Variante B (beibehalten für Kompatibilität)
"model_objects_skip_parts": excluded_objects,
},
}
@@ -3169,6 +3100,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):
@@ -3191,6 +3125,15 @@ class KobraXBridge:
async def handle_api_file_ready_clear(self, request):
self._state["file_ready"] = ""
self._state["filename"] = ""
self._state["progress"] = 0.0
self._state["print_duration"] = 0
self._state["remain_time"] = 0
self._state["curr_layer"] = 0
self._state["total_layers"] = 0
self._state["slicer_time"] = 0
self._state["layer_height"] = 0.0
self._state["first_layer_height"] = 0.0
self._thumbnail_b64 = ""
self._push_status_update()
return web.json_response({"result": "ok"})
@@ -3803,6 +3746,7 @@ 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),
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
"light_on": s["light_on"],
"light_brightness": s["light_brightness"],
@@ -3816,7 +3760,7 @@ class KobraXBridge:
"thumbnail": thumbnail,
"connection_error": s["connection_error"],
"file_ready": s["file_ready"],
"print_start_dialog": s.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1)),
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
"version": self._read_version(),
})
@@ -3954,11 +3898,8 @@ class KobraXBridge:
"default_ams_slot": getattr(self._args, "default_ams_slot", "auto"),
"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),
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
"poll_interval": getattr(self._args, "poll_interval", 3),
"filament_profiles": {str(k): v for k, v in self._filament_profiles.items()},
"visible_vendors": self._visible_vendors,
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
"ace_dry_presets": self._ace_dry_presets,
})
@@ -3988,15 +3929,10 @@ class KobraXBridge:
cfg.set("print", "default_ams_slot", str(data.get("default_ams_slot", getattr(self._args, "default_ams_slot", "auto"))))
cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1))))
cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0))))))
print_start_dialog = int(bool(data.get("print_start_dialog", data.get("file_ready_dialog", getattr(self._args, "print_start_dialog", 0)))))
cfg.set("print", "print_start_dialog", str(print_start_dialog))
cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1))))))
cfg.set("print", "print_start_dialog", str(int(bool(data.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1))))))
if "poll_interval" in data:
try:
pi = max(1, min(60, int(data["poll_interval"])))
except (TypeError, ValueError):
pi = 3
cfg.set("bridge", "poll_interval", str(pi))
elif not cfg.has_option("bridge", "poll_interval"):
if not cfg.has_option("bridge", "poll_interval"):
cfg.set("bridge", "poll_interval", "3")
printer_name = str(data.get("printer_name", "")).strip()
if printer_name:
@@ -4004,6 +3940,9 @@ class KobraXBridge:
elif cfg.has_option("bridge", "printer_name"):
cfg.remove_option("bridge", "printer_name")
self._args.print_start_dialog = print_start_dialog
self._args.auto_leveling = int(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
incoming_presets = data.get("ace_dry_presets") if isinstance(data, dict) else None
presets = self._sanitize_ace_dry_presets(incoming_presets if isinstance(incoming_presets, dict) else self._ace_dry_presets)
for key, val in presets.items():
@@ -4161,9 +4100,8 @@ class KobraXBridge:
# Bei einem Restart muss environ bereinigt werden, sonst liest der neue Prozess
# die alten Werte statt der geänderten config.ini.
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "PRINT_START_DIALOG",
"FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME"):
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
"CAMERA_ON_PRINT", "PRINT_START_DIALOG", "FILE_READY_DIALOG", "WEB_UPLOAD_WARNING", "BRIDGE_PRINTER_NAME"):
os.environ.pop(_k, None)
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
@@ -4661,13 +4599,23 @@ class KobraXBridge:
elif method == "machine.update.status":
result = {"busy": False, "version_info": {}}
elif method == "server.files.metadata":
# Obico + Mobileraker fragen Metadaten zu einer Datei. Dieselbe
# Logik wie der HTTP-Endpoint (vorher eigener kaputter Pfad mit
# nicht existierender Store-Methode → leere Antwort →
# Mobileraker-Endlosschleife, Issue #48).
# Obico fragt nach Metadaten zu einer Datei (filename in params)
fname = (params or {}).get("filename") if isinstance(params, dict) else None
fname = fname or self._state.get("filename", "")
result = self._build_file_metadata(fname) if fname else {}
meta = {}
if fname:
try:
rec = self._store.get_file_by_filename(fname) if hasattr(self._store, "get_file_by_filename") else None
except Exception:
rec = None
if rec:
meta = {
"filename": rec.get("filename"),
"size": rec.get("size_bytes") or 0,
"modified": time.time(),
"estimated_time": rec.get("est_print_time_sec") or 0,
"thumbnails": [],
}
result = meta
else:
log.debug(f"Unbekannte RPC-Methode: {method}")
result = {}
@@ -4871,8 +4819,6 @@ def build_app(bridge: KobraXBridge) -> web.Application:
r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots)
r.add_get("/kx/filament/profiles", bridge.handle_kx_filament_profiles)
r.add_post("/kx/filament/slots/{idx}/profile", bridge.handle_kx_filament_slot_profile)
r.add_get("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors)
r.add_post("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors)
# Custom-Profile-Import (Issue #41) — User lädt eigene Orca-Filament-
# Profile als ZIP/JSON hoch (z.B. aus ~/.config/OrcaSlicer/user/<id>/filament/),
# weil die Bridge typischerweise nicht auf demselben Host wie OrcaSlicer läuft.
@@ -5047,9 +4993,9 @@ def main():
parser.add_argument("--default-ams-slot",default=env_loader.DEFAULT_AMS_SLOT)
parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING)
parser.add_argument("--camera-on-print", type=int, default=env_loader.CAMERA_ON_PRINT)
parser.add_argument("--print-start-dialog", type=int, default=env_loader.PRINT_START_DIALOG)
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG)
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
parser.add_argument("--host", default="0.0.0.0",
help="Bind-Adresse für den Bridge-Server")

View File

@@ -3,17 +3,16 @@ var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
curr_layer:0,total_layers:0,z_mm:0,printer_name:'Kobra X',firmware_version:'',
camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,
auto_leveling:1,
ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]},web_upload_warning:1};
var tempHistory={n:[],b:[]};
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 _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 currentStep=1;
var currentPanel='dashboard';
var _printOptionsAutoLeveling=1;
var _printOptionsAutoLevelingSnapshot=1;
var aceAutoRefillPrefs=(function(){
try{return JSON.parse(localStorage.getItem('aceAutoRefillPrefs')||'{}')||{};}catch(_){return {};}
})();
@@ -171,8 +170,6 @@ async function setLanguage(lang){
localStorage.setItem('lang',l);
var langSel=document.getElementById('lang-select');
if(langSel)langSel.value=l;
var sLangSel=document.getElementById('s-lang-select');
if(sLangSel)sLangSel.value=l;
document.documentElement.setAttribute('lang',l);
applyLang();
}
@@ -259,13 +256,15 @@ function applyLang(){
setText('skip-title',T.skip_title);
setText('skip-hint',T.skip_hint);
setText('d-btn-skip-label',T.skip_btn_label);
setText('fd-objects-toggle-lbl',T.fd_objects_toggle);
setText('fd-objects-hint',T.fd_objects_hint);
setText('apd-lbl-ip',T.apd_lbl_ip);
setText('apd-lbl-name',T.apd_lbl_name);
var apn=document.getElementById('apd-name');if(apn)apn.setAttribute('placeholder',T.apd_placeholder_name);
setText('apd-cancel',T.apd_cancel);
setText('apd-confirm',T.apd_confirm);
setText('fd-slots-hint',T.fd_slots_hint);
setText('skip-cancel',T.skip_cancel);
setText('skip-confirm',T.skip_confirm);
setText('fd-cancel',T.fd_cancel);
setText('fd-print',T.fd_print);
setText('store-panel-title','🗂 '+T.panel_browser_title);
@@ -316,26 +315,12 @@ function applyLang(){
document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
// Console
setText('ptitle-console',T.panel_console_title);
// Settings-Panel
// Settings modal
setText('modal-title-settings',T.settings_title);
setText('modal-sec-connection',T.settings_connection);
setText('modal-sec-print',T.settings_print);
setText('modal-sec-poll',T.settings_poll);
setText('modal-sec-version',T.settings_version);
// Nav + Kategorie-Labels (mit Fallback falls i18n-Key noch fehlt)
setText('nav-settings',T.nav_settings||'Einstellungen');
setText('setcat-lbl-connection',T.settings_connection||'Verbindung');
setText('setcat-lbl-printer',T.settings_print||'Drucker');
setText('setcat-lbl-display',T.settings_cat_display||'Darstellung');
setText('setcat-lbl-display2',T.settings_cat_display||'Darstellung');
setText('setcat-lbl-filament',T.settings_cat_filament||'Filament');
setText('setcat-lbl-system',T.settings_version||'System');
setText('lbl-set-lang',T.settings_cat_language||'Sprache');
setText('lbl-set-theme',T.settings_cat_theme||'Hell / Dunkel umschalten');
setText('lbl-poll-interval',T.settings_poll||'Poll-Intervall (Sekunden)');
setText('lbl-filament-mapping',T.settings_filament_mapping||'Filament-Profil-Mapping (pro Slot)');
setText('lbl-filament-mapping-save',T.settings_filament_mapping_save||'Mapping speichern');
setText('lbl-visible-vendors',T.settings_visible_vendors||'Sichtbare Hersteller (Profil-Dropdown)');
setText('visible-vendors-hint',T.settings_visible_vendors_hint||'Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.');
setText('lbl-visible-vendors-save',T.settings_visible_vendors_save||'Auswahl speichern');
// Custom-Profile-Import (Issue #41)
setText('modal-sec-orca-profiles',T.orca_profile_section);
setText('orca-profiles-hint',T.orca_profile_hint);
@@ -358,20 +343,16 @@ function applyLang(){
setText('lbl-default-slot',T.settings_default_slot);
setText('opt-slot-auto',T.settings_slot_auto);
setText('lbl-auto-leveling',T.settings_auto_leveling);
setText('lbl-file-ready-mode',T.settings_file_ready_mode);
setText('opt-file-ready-dialog',T.settings_file_ready_dialog);
setText('opt-file-ready-banner',T.settings_file_ready_banner);
setText('lbl-camera-on-print',T.settings_camera_on_print);
setText('lbl-web-upload-warning',T.settings_web_upload_warning);
setText('fd-options-title',T.fd_options_title);
setText('fd-lbl-auto-leveling',T.print_auto_leveling);
setText('lbl-fd-auto-leveling',T.print_auto_leveling);
setText('lbl-camera-on-print',T.settings_camera_on_print);
setText('lbl-file-ready-mode',T.settings_file_ready_mode);
setText('opt-file-ready-banner',T.settings_file_ready_banner);
setText('opt-file-ready-dialog',T.settings_file_ready_dialog);
setText('lbl-web-upload-warning',T.settings_web_upload_warning);
setText('lbl-update-check',T.update_check);
setText('lbl-update-apply',T.update_apply);
// Progress-Karten-Aktionen für geladene/idle Datei (Issue #55)
setText('d-idle-print-lbl',T.progress_action_print||'Drucken');
setText('d-idle-slots-lbl',T.progress_action_slots||'Slots zuordnen');
setText('d-idle-clear-lbl',T.progress_action_clear||'Leeren');
// Speed buttons
setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,''));
setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,''));
@@ -414,7 +395,22 @@ function applyLang(){
var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom);
setText('logdir-all',T.log_dir_all);
setText('loglvl-all',T.log_dir_all);
setText('logdir-rx',T.log_dir_rx);
setText('logdir-tx',T.log_dir_tx);
setText('loglvl-err',T.log_lvl_err);
setText('loglvl-warn',T.log_lvl_warn);
setText('log-lbl-dir',T.log_dir_label);
setText('log-lbl-level',T.log_lvl_label);
setText('log-lbl-topic',T.log_topic_label);
setText('log-topic-ams',T.log_topic_ams);
setText('log-topic-print',T.log_topic_print);
setText('log-topic-info',T.log_topic_info);
setText('log-topic-status',T.log_topic_status);
setText('btn-log-dl',T.log_download);
setText('btn-autoscroll',T.log_auto);
setText('btn-log-clear',T.log_clear);
var lf=document.getElementById('log-filter');if(lf)lf.setAttribute('placeholder',T.log_filter_placeholder);
var sb=document.getElementById('settings-btn');if(sb)sb.setAttribute('title',T.settings_title);
setText('file-ready-btn',T.file_ready_btn);
setText('file-slots-btn',T.file_slots_btn);
setText('file-cancel-btn',T.file_cancel_btn);
@@ -491,15 +487,6 @@ function showPanel(id){
var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active');
var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active');
currentPanel=id;
if(id==='settings')openSettings();
}
// Settings-Kategorie umschalten (Master-Detail)
function showSettingsCat(name){
document.querySelectorAll('.set-group').forEach(g=>g.classList.remove('active'));
document.querySelectorAll('.set-cat').forEach(b=>b.classList.remove('active'));
var g=document.getElementById('setgrp-'+name);if(g)g.classList.add('active');
var c=document.getElementById('setcat-'+name);if(c)c.classList.add('active');
}
// ── Console log ──
@@ -657,23 +644,26 @@ function applyState(){
// connection error banner nur wenn überhaupt ein Drucker konfiguriert ist
var banner=document.getElementById('conn-error-banner');
if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+tr('lbl_conn_error')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}}
var bannerVisible=false;
var frb=document.getElementById('file-ready-banner');
var filamentDialog=document.getElementById('filament-dialog');
var filamentDialogOpen=!!(filamentDialog&&filamentDialog.classList.contains('open'));
var readyFile=s.file_ready||'';
var readyStandby=!!(readyFile&&s.print_state==='standby');
var dialogMode=!!(s.print_start_dialog!==undefined?s.print_start_dialog:s.file_ready_dialog);
if(frb){
var shouldAutoOpen=(s.print_start_dialog===undefined?true:!!s.print_start_dialog);
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);
} else {
frb.style.display='flex';
bannerVisible=true;
}
}else{frb.style.display='none';}
if(readyStandby&&!dialogMode){
document.getElementById('file-ready-name').textContent=readyFile;
frb.style.display='flex';
}else{
frb.style.display='none';
}
}
var shouldAutoOpen=(_readyStateInitialized&&readyStandby&&dialogMode&&!filamentDialogOpen&&readyFile!==_lastReadyFile);
if(shouldAutoOpen){
setTimeout(function(){ startReadyFileWithSlots(); },0);
}
_lastReadyFile=readyFile;
_readyStateInitialized=true;
// skip-button (mid-print) nur sichtbar wenn aktuell gedruckt wird
var printing=(s.print_state==='printing'||s.print_state==='paused');
var skipBtn=document.getElementById('d-btn-skip');
@@ -682,26 +672,11 @@ function applyState(){
// der Drucker idle ist). Pause-Button rendert sich passend zum State um.
var ctrlBtns=document.getElementById('d-ctrl-btns');
if(ctrlBtns) ctrlBtns.style.display=printing?'':'none';
var hasLoadedFile=!!((s.file_ready||'').trim()||(s.filename||'').trim());
var showReadyBtns=!printing&&hasLoadedFile;
var readyBtns=document.getElementById('d-ready-btns');
if(readyBtns) readyBtns.style.display=showReadyBtns?'':'none';
updatePauseResumeBtn();
// 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.
if(s.file_ready) _lastLoadedFile=s.file_ready;
else if(s.filename) _lastLoadedFile=s.filename;
// 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.
var idleBtns=document.getElementById('d-idle-btns');
if(idleBtns){
var showIdle=(!printing && _lastLoadedFile && !bannerVisible);
idleBtns.style.display=showIdle?'':'none';
if(showIdle){
var dfn=document.getElementById('d-fname');
if(dfn && (!dfn.textContent || dfn.textContent==='')){
dfn.textContent=_lastLoadedFile;dfn.title=_lastLoadedFile;
}
}
}
// header
var b=document.getElementById('h-badge');
@@ -741,7 +716,7 @@ function applyState(){
else{dslrow.style.display='none';}
}
var fn=s.filename||'';
var fn=printing?(s.filename||''):(s.filename||s.file_ready||'');
var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn};
var pfname=document.getElementById('p-fname');if(pfname){pfname.textContent=fn;pfname.title=fn};
var cfo=document.getElementById('cam-fname');if(cfo)cfo.textContent=fn!==''?fn:'';
@@ -963,6 +938,10 @@ function drawChart(id,_,series){
var _updateTag='';
var _updateUrl='';
function openSettings(){
// Titel mit aktivem Drucker-Namen aktualisieren
var pname=_activePrinter&&_activePrinter.name?_activePrinter.name:null;
var title=document.getElementById('modal-title-settings');
if(title)title.textContent=T.settings_title+(pname?' '+pname:'');
fetch(_apiUrl('/api/settings')).then(function(r){return r.json()}).then(function(d){
document.getElementById('s-printer-name').value=d.printer_name||'';
document.getElementById('s-printer-ip').value=d.printer_ip||'';
@@ -973,121 +952,27 @@ function openSettings(){
document.getElementById('s-mode-id').value=d.mode_id||'';
document.getElementById('s-default-slot').value=d.default_ams_slot||'auto';
document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling);
S.auto_leveling=(d.auto_leveling===undefined?1:(d.auto_leveling?1:0));
var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print;
var frm=document.getElementById('s-file-ready-mode');if(frm)frm.value=(d.print_start_dialog===undefined?'1':String(d.print_start_dialog?1:0));
var frm=document.getElementById('s-file-ready-mode');if(frm)frm.value=Number((d.print_start_dialog!==undefined)?d.print_start_dialog:d.file_ready_dialog)?'dialog':'banner';
var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning);
// Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage
var pi=document.getElementById('s-poll-interval');
if(pi){
var sec=d.poll_interval||Math.round((parseInt(localStorage.getItem('pollInterval')||'2000'))/1000)||3;
pi.value=sec;
}
renderFilamentMapping(d.filament_profiles||{});
});
// Sprach-Select im Settings-Panel mit aktueller Sprache spiegeln
var ls=document.getElementById('s-lang-select');
if(ls)ls.value=(localStorage.getItem('lang')||document.documentElement.lang||'de');
var v=localStorage.getItem('pollInterval')||'2000';
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
var pb=document.getElementById('poll-'+Math.round(parseInt(v)/1000));
if(pb)pb.classList.add('active');
document.getElementById('s-version-label').textContent='v'+('__VERSION__'||'?');
document.getElementById('update-status').textContent='';
document.getElementById('btn-update-apply').style.display='none';
var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none';
_updateTag='';_updateUrl='';
// Custom-Profile-Liste laden (Issue #41)
document.getElementById('settings-modal').classList.add('open');
// Custom-Profile-Liste laden (Issue #41 — Verwaltung von User-importierten
// OrcaSlicer-Filament-Profilen)
refreshUserProfileList();
// Vendor-Sichtbarkeitsfilter (Issue #41 Option A)
loadVendorChecklist();
}
function closeSettings(){
// Panel-Variante: zurück zum Dashboard
showPanel('dashboard');
}
// Poll-Intervall-Feld → Live-Poll sofort übernehmen (Persistenz erst beim Speichern)
function onPollIntervalInput(){
var pi=document.getElementById('s-poll-interval');
if(!pi)return;
var sec=parseInt(pi.value,10);
if(sec>=1&&sec<=60)setPoll(sec*1000);
}
// ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ──
function renderFilamentMapping(map){
var el=document.getElementById('filament-mapping-list');
if(!el)return;
var rows='';
for(var i=0;i<4;i++){
var m=map[i]||map[String(i)]||{};
var idHint=m.id?' <span style="color:var(--txt2);font-size:11px">('+m.id+')</span>':'';
rows+='<div class="modal-field" style="margin-bottom:8px">'
+'<label>Slot '+(i+1)+idHint+'</label>'
+'<div style="display:flex;gap:6px;flex-wrap:wrap">'
+'<input type="text" id="fmap-'+i+'-vendor" placeholder="Vendor (z.B. Polymaker)" value="'+(m.vendor||'')+'" style="flex:1;min-width:120px">'
+'<input type="text" id="fmap-'+i+'-name" placeholder="Name (z.B. PolyTerra PLA)" value="'+(m.name||'')+'" style="flex:1;min-width:140px">'
+'</div></div>';
}
el.innerHTML=rows;
}
function saveFilamentMapping(){
// Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend).
// Leere Felder = 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();
chain=chain.then(function(){
return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'),
{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({vendor:vendor,name:name})});
});
})(i);
}
chain.then(function(){
clog(tr('log_filament_mapping_saved')||'Filament-Mapping gespeichert','msg-ok');
openSettings(); // neu laden → ID-Hints aktualisieren
}).catch(function(e){clog('Mapping-Fehler: '+e,'msg-err');});
}
// ── Vendor-Sichtbarkeitsfilter (Issue #41 Option A) ──
var _vendorChecklistSel={}; // {vendor:true} — laufende Auswahl im UI
function loadVendorChecklist(){
// aktuelle Auswahl aus Backend, dann alle verfügbaren Vendoren rendern
_visibleVendors=null; // Cache invalidieren
_loadVisibleVendors(function(vis){
_vendorChecklistSel={};
(vis||[]).forEach(function(v){_vendorChecklistSel[v]=true;});
renderVendorChecklist();
});
}
function renderVendorChecklist(){
var el=document.getElementById('visible-vendors-list');
if(!el)return;
_loadOrcaFilaments(function(profiles){
// alle System-Vendoren (ohne Generic — der ist immer sichtbar) sammeln
var set={};
profiles.forEach(function(p){ if(!p.is_user && p.vendor && p.vendor!=='Generic') set[p.vendor]=1; });
var vendors=Object.keys(set).sort();
var q=((document.getElementById('vendor-filter-search')||{}).value||'').toLowerCase();
if(q)vendors=vendors.filter(function(v){return v.toLowerCase().indexOf(q)>=0;});
el.innerHTML=vendors.map(function(v){
var ck=_vendorChecklistSel[v]?'checked':'';
var safe=v.replace(/"/g,'&quot;');
return '<label style="display:flex;align-items:center;gap:8px;padding:3px 0;cursor:pointer;font-size:13px">'
+'<input type="checkbox" data-vendor="'+safe+'" '+ck+' onchange="_vendorCheck(this)" style="width:auto;margin:0"> '+v+'</label>';
}).join('')||'<i style="color:var(--txt2)"></i>';
});
}
function _vendorCheck(cb){
var v=cb.getAttribute('data-vendor');
if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v];
}
function saveVisibleVendors(){
var vendors=Object.keys(_vendorChecklistSel);
fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({vendors:vendors})}).then(function(r){return r.json();}).then(function(){
_visibleVendors=vendors.slice(); // Dropdown-Cache aktualisieren
clog(tr('log_visible_vendors_saved')||'Hersteller-Auswahl gespeichert','msg-ok');
}).catch(function(e){clog('Vendor-Filter-Fehler: '+e,'msg-err');});
document.getElementById('settings-modal').classList.remove('open');
}
// ── Custom Filament Profile Import (Issue #41) ──
@@ -1161,9 +1046,6 @@ function doProfileImportUpload(files){
_orcaFilamentCache=null;
refreshImportDialogList();
refreshUserProfileList();
// Vendor-Checkliste neu aufbauen — ein Import kann einen bisher
// unbekannten System-Vendor mitbringen (Issue #41).
if(document.getElementById('visible-vendors-list')) renderVendorChecklist();
// Wenn Slot-Edit offen ist, Dropdown gleich neu befüllen
var mat=document.getElementById('slot-edit-mat');
if(mat && document.getElementById('slot-edit-modal').classList.contains('open')){
@@ -1204,14 +1086,6 @@ function updateSlotEditFeedButton(){
btn.textContent=_slotEditLoaded?tr('slot_edit_unload'):tr('slot_edit_load');
}
var _orcaFilamentCache=null; // [{id,name,vendor,type,color}, …]
var _visibleVendors=null; // Vendor-Sichtbarkeitsfilter (Issue #41); [] = alle
function _loadVisibleVendors(cb){
if(_visibleVendors!==null){ cb(_visibleVendors); return; }
fetch(_apiUrl('/kx/filament/visible_vendors')).then(function(r){return r.json();}).then(function(d){
_visibleVendors=d.result||[];
cb(_visibleVendors);
}).catch(function(){ _visibleVendors=[]; cb([]); });
}
function _loadOrcaFilaments(cb){
if(_orcaFilamentCache){ cb(_orcaFilamentCache); return; }
fetch(_apiUrl('/kx/filament/profiles')).then(function(r){return r.json();}).then(function(d){
@@ -1257,23 +1131,13 @@ function _fillSlotProfileDropdown(material, currentVendor, currentName){
userProfs.forEach(function(p){ _appendOption(gUser, p); });
sel.appendChild(gUser);
}
// Vendor-Sichtbarkeitsfilter (Issue #41 Option A): nur gewählte Vendoren +
// Generic. Leere Liste = alle (rückwärtskompatibel). Eigene Profile (is_user)
// sind oben bereits unkonditional drin.
_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){ _appendOption(g, p); });
sel.appendChild(g);
});
// System-Profile nach Vendor gruppieren
var byVendor={};
systemProfs.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){ _appendOption(g, p); });
sel.appendChild(g);
});
});
}
@@ -1327,16 +1191,15 @@ function slotEditFeed(){
})
.catch(function(){});
}
function startReadyFile(filename){
var fn=filename||S.file_ready;
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
function startReadyFile(){
var currentFile=(storeFiles||[]).find(function(f){return f.filename===S.file_ready;});
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); });
maybeGateWebUpload(currentFile, function(){ startReadyFile(); });
return;
}
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:fn})
post('/printer/print/start',{filename:S.file_ready})
.then(function(r){return r.json();})
.then(function(r){
document.getElementById('file-ready-banner').style.display='none';
@@ -1351,19 +1214,25 @@ function cancelReadyFile(){
post('/api/file_ready/clear',{})
.then(function(){document.getElementById('file-ready-banner').style.display='none';});
}
// ── Aktionen für geladene/idle Datei in der Progress-Karte (Issue #55) ──
function startIdleFile(){
if(_lastLoadedFile) startReadyFile(_lastLoadedFile);
function startProgressReadyFile(){
var target=((S.file_ready||'')||(S.filename||'')).trim();
if(!target)return;
S.file_ready=target;
startReadyFileWithSlots();
}
function startIdleFileWithSlots(){
if(_lastLoadedFile) startReadyFileWithSlots(_lastLoadedFile);
}
function clearIdleFile(){
_lastLoadedFile=null;
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='';}
post('/api/file_ready/clear',{}).catch(function(){});
function clearProgressReadyFile(){
post('/api/file_ready/clear',{})
.then(function(){
S.file_ready='';
S.filename='';
S.thumbnail='';
S.slicer_time=0;
applyState();
poll();
})
.catch(function(e){
clog((T.log_error||'Error')+' '+e,'msg-err');
});
}
function selectMatPreset(m){
document.getElementById('slot-edit-mat').value=m;
@@ -1452,6 +1321,9 @@ document.addEventListener('DOMContentLoaded',function(){
});
});
function setPoll(ms){
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
var id='poll-'+Math.round(ms/1000);
var pb=document.getElementById(id);if(pb)pb.classList.add('active');
localStorage.setItem('pollInterval',ms);
clearInterval(pollTimer);
pollTimer=setInterval(poll,ms);
@@ -1461,6 +1333,9 @@ function saveSettings(){
btn.disabled=true;btn.textContent='…';
var webUploadWarning=(document.getElementById('s-web-upload-warning')||{}).checked?1:0;
S.web_upload_warning=webUploadWarning;
var printStartDialog=(document.getElementById('s-file-ready-mode')||{}).value==='dialog'?1:0;
S.print_start_dialog=printStartDialog;
S.auto_leveling=(document.getElementById('s-auto-leveling')||{}).checked?1:0;
post('/api/settings',{
printer_name: document.getElementById('s-printer-name').value,
printer_ip: document.getElementById('s-printer-ip').value,
@@ -1472,9 +1347,8 @@ function saveSettings(){
default_ams_slot: document.getElementById('s-default-slot').value,
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
print_start_dialog: parseInt((document.getElementById('s-file-ready-mode')||{}).value||'1',10),
print_start_dialog:printStartDialog,
web_upload_warning:webUploadWarning,
poll_interval: Math.min(60,Math.max(1,parseInt((document.getElementById('s-poll-interval')||{}).value,10)||3)),
}).then(function(){
btn.textContent=T.update_restarting;
setTimeout(function(){
@@ -1809,9 +1683,13 @@ function aceDryDialogIsCustomPreset(key){
}
function aceDryDialogSyncCustomButtonNames(){
['pla','pla_plus','petg','tpu','abs_asa','pa_pc'].forEach(function(k){
var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]');
if(b)b.textContent=tr('ace_dry_preset_'+k);
});
['custom_1','custom_2','custom_3'].forEach(function(k){
var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]');
if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||('Custom '+k.slice(-1));
if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||(tr('ace_dry_preset_custom')+' '+k.slice(-1));
});
}
@@ -1875,7 +1753,7 @@ function aceDryDialogInputsChanged(){
var i=document.getElementById('ace-dry-dialog-custom-name');
if(b&&i){
var t=(i.value||'').trim();
b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||('Custom '+_aceDryDialogPresetKey.slice(-1)));
b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||(tr('ace_dry_preset_custom')+' '+_aceDryDialogPresetKey.slice(-1)));
}
}
aceDryDialogUpdateSaveButton();
@@ -2121,6 +1999,8 @@ function formatDur(sec){
var _storeFileId=null;
var _storeFilename=null;
var _filamentDialogMode='store'; // 'store' oder 'banner'
var _readyStateInitialized=false;
var _lastReadyFile='';
var _pendingWebVerifyFileId=null;
var _pendingWebVerifyFilename='';
var _pendingWebVerifyAction=null;
@@ -2154,9 +2034,13 @@ function storePrint(fileId, filename){
}
function openStorePrintDialog(fileId, filename, fileObj){
openStoreLikePrintDialog(fileId, filename, fileObj, 'store');
}
function openStoreLikePrintDialog(fileId, filename, fileObj, dialogMode){
_storeFileId=fileId;
_storeFilename=filename;
_filamentDialogMode='store';
_filamentDialogMode=dialogMode||'store';
maybeGateWebUpload(fileObj, function(){
// GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog)
_setGcodeFilamentsFromFileObj(fileObj);
@@ -2257,51 +2141,42 @@ function confirmStoreWebVerify(){
});
}
function startReadyFileWithSlots(filename,_autoOpen){
if(!_autoOpen) _fdAutoOpenedFile=null; // manueller Aufruf → Auto-Open-Sperre aufheben
var fn=filename||S.file_ready;
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
function startReadyFileWithSlots(){
var currentFile=(storeFiles||[]).find(function(f){return f.filename===S.file_ready;});
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn); });
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(); });
return;
}
_filamentDialogMode='banner';
_storeFilename=fn||'';
// Banner must never reuse stale store-file context.
_storeFileId=null;
_gcodeFilaments=[];
var _autoOpenFile=_autoOpen?fn:null;
if(_autoOpen) _fdDialogOpen=true; // bereits während Fetch sperren
function openWithSlots(){
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
openFilamentDialog(d.result||[]);
}).catch(function(){
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
openFilamentDialog([]);
});
}
var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
var targetFilename=S.file_ready||'';
var fileObj=(storeFiles||[]).find(function(f){return f.filename===targetFilename;});
if(fileObj){
_storeFileId=fileObj.id;
_setGcodeFilamentsFromFileObj(fileObj);
openWithSlots();
openStoreLikePrintDialog(fileObj.id, targetFilename, fileObj, 'store');
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;});
var refreshed=(storeFiles||[]).find(function(f){return f.filename===targetFilename;});
if(refreshed){
_storeFileId=refreshed.id;
_setGcodeFilamentsFromFileObj(refreshed);
openStoreLikePrintDialog(refreshed.id, targetFilename, refreshed, 'store');
return;
}
openWithSlots();
_filamentDialogMode='banner';
_storeFilename=targetFilename;
_storeFileId=null;
_gcodeFilaments=[];
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(data){
openFilamentDialog(data.result||[]);
}).catch(function(){openFilamentDialog([]);});
}).catch(function(){
openWithSlots();
_filamentDialogMode='banner';
_storeFilename=targetFilename;
_storeFileId=null;
_gcodeFilaments=[];
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(data){
openFilamentDialog(data.result||[]);
}).catch(function(){openFilamentDialog([]);});
});
}
@@ -2329,18 +2204,6 @@ function _normalizeMaterialKey(material){
function _materialsCompatible(a,b){
return _normalizeMaterialKey(a)===_normalizeMaterialKey(b);
}
// Issue #57 Punkt 4: konkreter Profilname (User-Override) statt generischem Typ.
// Fällt auf den Material-Typ zurück wenn kein Profil gemappt ist.
function _slotProfileLabel(slot){
if(!slot)return '';
if(slot.filament_name){
return slot.filament_name+(slot.filament_vendor?' — '+slot.filament_vendor:'');
}
return slot.material||'';
}
function _escAttr(s){
return String(s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function _updateSlotMarker(sel){
var opt=sel.options[sel.selectedIndex];
var color=opt&&opt.dataset.color?opt.dataset.color:'#888';
@@ -2351,7 +2214,6 @@ function _updateSlotMarker(sel){
marker.style.background=color;
marker.style.color=_contrastText(color);
marker.textContent=(slotIdx+1);
marker.title=(opt&&opt.dataset.profile)?opt.dataset.profile:'';
}
}
@@ -2363,22 +2225,12 @@ function openFilamentDialog(slots){
var title=document.getElementById('fd-title');
var body=document.getElementById('fd-slots');
if(title)title.textContent='▶ '+_storeFilename;
// Auto-Leveling-Checkbox mit globalem Default vorbelegen
var fdAl=document.getElementById('fd-auto-leveling');
if(fdAl) fdAl.checked=(S.auto_leveling===undefined?true:!!S.auto_leveling);
// Objekt-Liste laden — sobald eine File-ID auflösbar ist (Issue #57 Punkt 3:
// Skip-Parität auch im Banner-/Upload-Modus, nicht nur im Store-Modus).
// startReadyFileWithSlots() setzt _storeFileId auch im Banner-Modus per
// filename→fileObj-Lookup, daher reicht hier die _storeFileId-Prüfung.
// Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID)
_printObjects=[];
_printObjectsSvg='';
var objSection=document.getElementById('fd-objects-section');
var objBody=document.getElementById('fd-objects-body');
var objArrow=document.getElementById('fd-objects-arrow');
if(objSection)objSection.style.display='none';
if(objBody)objBody.style.display='none'; // immer eingeklappt starten
if(objArrow)objArrow.style.transform='';
if(_storeFileId){
if(_filamentDialogMode==='store'&&_storeFileId){
fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects'))
.then(function(r){return r.json()})
.then(function(d){
@@ -2387,12 +2239,12 @@ function openFilamentDialog(slots){
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(){});
}
_printOptionsAutoLeveling=(S.auto_leveling===undefined?1:(S.auto_leveling?1:0));
syncPrintOptionsDialog();
// GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten
var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){
@@ -2452,8 +2304,8 @@ function openFilamentDialog(slots){
var defaultSlot=compatible.find(function(s){return s.slot_index===defaultSlotIndex;})||null;
var opts=compatible.map(function(s){
var sel=(defaultSlot&&s.slot_index===defaultSlot.slot_index)?'selected':'';
return '<option value="'+s.slot_index+'" data-color="'+s.color_hex+'" data-material="'+s.material+'" data-profile="'+_escAttr(_slotProfileLabel(s))+'" '+sel+'>'+
'● '+T.fd_slot+' '+(s.slot_index+1)+' · '+_slotProfileLabel(s)+'</option>';
return '<option value="'+s.slot_index+'" data-color="'+s.color_hex+'" data-material="'+s.material+'" '+sel+'>'+
'● '+T.fd_slot+' '+(s.slot_index+1)+' · '+s.material+'</option>';
}).join('');
if(!compatible.length){
opts='<option value="-1" data-color="#888888" data-material="" selected>⚠ '+T.fd_no_matching_material+'</option>';
@@ -2470,7 +2322,7 @@ function openFilamentDialog(slots){
'<span style="font-size:11px;color:var(--txt2);min-width:36px">'+gc.material+'</span>'+
usedBadge+
'<span style="font-size:16px;color:var(--txt2)">→</span>'+
'<span class="fd-slot-marker" data-for-paint="'+i+'" title="'+_escAttr(defaultSlot?_slotProfileLabel(defaultSlot):'')+'" style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:5px;background:'+slotColor+';color:'+slotTxt+';font-weight:700;font-size:12px;border:1px solid var(--border);flex-shrink:0">'+(defaultSlot?defaultSlot.slot_index+1:'?')+'</span>'+
'<span class="fd-slot-marker" data-for-paint="'+i+'" style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:5px;background:'+slotColor+';color:'+slotTxt+';font-weight:700;font-size:12px;border:1px solid var(--border);flex-shrink:0">'+(defaultSlot?defaultSlot.slot_index+1:'?')+'</span>'+
'<select data-paint="'+i+'" data-paint-color="'+gc.color_hex+'" data-is-used="'+(isUsed?'1':'0')+'" data-has-compatible="'+(compatible.length?'1':'0')+'" '+(compatible.length?'':'disabled')+' onchange="_updateSlotMarker(this)" style="flex:1;min-width:0;padding:4px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+
opts+'</select>'+
'</div>';
@@ -2482,14 +2334,38 @@ function openFilamentDialog(slots){
function closeFilamentDialog(){
var dlg=document.getElementById('filament-dialog');
if(dlg)dlg.classList.remove('open');
_fdDialogOpen=false;
if(_fdAutoOpenedFile) _fdUserCancelled=true;
}
function syncPrintOptionsDialog(){
var autoLeveling=document.getElementById('fd-auto-leveling');
if(autoLeveling)autoLeveling.checked=!!_printOptionsAutoLeveling;
}
function openPrintOptionsDialog(){
_printOptionsAutoLevelingSnapshot=_printOptionsAutoLeveling;
syncPrintOptionsDialog();
var autoLeveling=document.getElementById('fd-auto-leveling');
if(autoLeveling)autoLeveling.focus();
}
function closePrintOptionsDialog(restore){
if(restore){
_printOptionsAutoLeveling=_printOptionsAutoLevelingSnapshot;
syncPrintOptionsDialog();
}
}
function confirmPrintOptionsDialog(){
_printOptionsAutoLeveling=(document.getElementById('fd-auto-leveling')||{}).checked?1:0;
_printOptionsAutoLevelingSnapshot=_printOptionsAutoLeveling;
closePrintOptionsDialog(false);
}
function confirmFilamentPrint(){
var selects=document.querySelectorAll('#fd-slots select');
var assignments=[];
var missingCompatible=0;
var autoLeveling=_printOptionsAutoLeveling;
selects.forEach(function(sel){
var paintIdx=parseInt(sel.dataset.paint);
var paintColor=sel.dataset.paintColor;
@@ -2523,14 +2399,12 @@ function confirmFilamentPrint(){
}
// Pre-Print Skip: Namen der abgehakten Objekte sammeln
var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;});
var fdAlEl=document.getElementById('fd-auto-leveling');
var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0);
closeFilamentDialog();
if(_filamentDialogMode==='banner'){
// Banner-Modus: normaler print/start mit Slot-Override
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})
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:autoLeveling})
.then(function(r){return r.json();})
.then(function(){
document.getElementById('file-ready-banner').style.display='none';
@@ -2545,7 +2419,7 @@ function confirmFilamentPrint(){
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})
body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:autoLeveling})
}).then(function(r){return r.json()}).then(function(d){
if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');}
else{clog('Druckfehler: '+(d.error||'?'),'msg-err');}
@@ -2571,15 +2445,6 @@ function _toggleObjectSkip(idx,val){
if(_printObjects[idx])_printObjects[idx].skip=!!val;
renderObjectSvg();
}
// Issue #57 Punkt 3: Skip-Objekte-Bereich ein-/ausklappen
function toggleFdObjects(){
var body=document.getElementById('fd-objects-body');
var arrow=document.getElementById('fd-objects-arrow');
if(!body)return;
var open=body.style.display!=='none';
body.style.display=open?'none':'block';
if(arrow)arrow.style.transform=open?'':'rotate(90deg)';
}
function renderObjectSvg(){
var box=document.getElementById('fd-objects-svg');
if(!box)return;

View File

@@ -42,12 +42,122 @@
<option value="zh-cn">中文(简体)</option>
</select>
</div>
<button class="theme-btn" onclick="showPanel('settings')" id="settings-btn" title="Einstellungen"></button>
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title=""></button>
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
</header>
<!-- ═══ SETTINGS MODAL ═══ -->
<!-- Settings-Modal entfernt — jetzt #panel-settings (Master-Detail im Main-Bereich) -->
<div class="modal-overlay" id="settings-modal" onclick="if(event.target===this)closeSettings()">
<div class="modal-box">
<div class="modal-header">
<span class="modal-title" id="modal-title-settings">Einstellungen</span>
<button class="modal-close" onclick="closeSettings()"></button>
</div>
<div>
<div class="modal-field" style="margin-bottom:12px">
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
</div>
<div class="modal-section" id="modal-sec-connection">Verbindung</div>
<div class="modal-field">
<label id="lbl-printer-ip">Drucker-IP</label>
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
</div>
<div class="modal-field">
<label id="lbl-mqtt-port">MQTT-Port</label>
<input type="number" id="s-mqtt-port" placeholder="9883">
</div>
<div class="modal-field">
<label id="lbl-username">MQTT-Benutzername</label>
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-password">MQTT-Passwort</label>
<input type="password" id="s-password" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-device-id">Device-ID</label>
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
</div>
<div class="modal-field">
<label id="lbl-mode-id">Mode-ID</label>
<input type="text" id="s-mode-id" placeholder="20030">
</div>
</div>
<div>
<div class="modal-section" id="modal-sec-print">Druckeinstellungen</div>
<div class="modal-field">
<label id="lbl-file-ready-mode"></label>
<select id="s-file-ready-mode">
<option value="dialog" id="opt-file-ready-dialog"></option>
<option value="banner" id="opt-file-ready-banner"></option>
</select>
</div>
<div class="modal-field">
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
<select id="s-default-slot">
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
<option value="0" id="opt-slot-0">Slot 1</option>
<option value="1" id="opt-slot-1">Slot 2</option>
<option value="2" id="opt-slot-2">Slot 3</option>
<option value="3" id="opt-slot-3">Slot 4</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
</div>
</div>
<div>
<div class="modal-section" id="modal-sec-poll">Poll-Intervall</div>
<div class="poll-btns">
<button class="poll-btn" onclick="setPoll(1000)" id="poll-1">1s</button>
<button class="poll-btn active" onclick="setPoll(2000)" id="poll-2">2s</button>
<button class="poll-btn" onclick="setPoll(5000)" id="poll-5">5s</button>
</div>
</div>
<!-- OrcaSlicer-Profile (Custom-Profile-Import, Issue #41) -->
<div>
<div class="modal-section" id="modal-sec-orca-profiles">OrcaSlicer-Profile</div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
</div>
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
style="background:var(--raised);color:var(--txt)">
<span id="lbl-orca-profiles-import">Profile importieren</span>
</button>
</div>
<div>
<div class="modal-section" id="modal-sec-version">Version</div>
<div class="update-row">
<span id="s-version-label" style="font-size:13px;color:var(--txt)"></span>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
</div>
<div class="update-status" id="update-status" style="margin-top:6px"></div>
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
<span id="lbl-update-apply">Jetzt installieren</span>
</button>
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
</div>
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings">Speichern &amp; Neustart</button>
</div>
</div>
<!-- ═══ AMS SLOT EDIT DIALOG ═══ -->
<div class="modal-overlay" id="slot-edit-modal" onclick="if(event.target===this)closeSlotEdit()">
@@ -127,8 +237,6 @@
<span class="nav-icon">🗂</span><span class="nav-text">Browser</span></button>
<button class="nav-btn" onclick="showPanel('console');clearLogBadge()" id="nb-console">
<span class="nav-icon"></span><span class="nav-text">Konsole</span><span id="log-badge" style="display:none;margin-left:4px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 5px;font-weight:700"></span></button>
<button class="nav-btn" onclick="showPanel('settings')" id="nb-settings">
<span class="nav-icon"></span><span class="nav-text" id="nav-settings">Einstellungen</span></button>
</nav>
<main>
@@ -192,17 +300,15 @@
</div>
</div>
<div class="fname" id="d-fname" title="" style="margin-top:6px"></div>
<div class="ctrl-btns" id="d-ready-btns" style="margin-top:12px;display:none">
<button class="btn btn-accent btn-sm" id="d-btn-ready-print" onclick="startProgressReadyFile()">▶ Print</button>
<button class="btn btn-cancel btn-sm" id="d-btn-ready-clear" onclick="clearProgressReadyFile()">✕ Clear</button>
</div>
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none"><span id="d-btn-skip-label">Objekte</span></button>
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
</div>
<!-- Aktionen für eine geladene, aber nicht laufende Datei (Issue #55) -->
<div class="ctrl-btns" id="d-idle-btns" style="margin-top:12px;display:none">
<button class="btn btn-accent btn-sm" id="d-idle-print" onclick="startIdleFile()"><span id="d-idle-print-lbl">Drucken</span></button>
<button class="btn btn-sm" id="d-idle-slots" onclick="startIdleFileWithSlots()" style="background:var(--raised);color:var(--txt)"><span id="d-idle-slots-lbl">Slots zuordnen</span></button>
<button class="btn btn-cancel btn-sm" id="d-idle-clear" onclick="clearIdleFile()"><span id="d-idle-clear-lbl">Leeren</span></button>
</div>
</div>
<!-- Temperatursteuerung + Verlauf -->
@@ -395,199 +501,36 @@
<div class="card">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
<span><span></span> <span id="ptitle-console">Ereignis-Log</span></span>
<a id="btn-log-dl" href="/api/log/download" download
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none">⬇ Download</a>
<a id="btn-log-dl" href="/api/log/download" download
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none"></a>
</div>
<div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;align-items:center">
<input id="log-filter" type="text" placeholder="Filter…"
<input id="log-filter" type="text" placeholder=""
oninput="renderLog()"
style="flex:1;min-width:120px;padding:5px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;font-family:var(--mono)">
<button id="btn-autoscroll" onclick="toggleAutoScroll()"
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap">⬇ Auto</button>
<button onclick="consoleLogs=[];renderLog()"
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">✕ Clear</button>
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap"></button>
<button id="btn-log-clear" onclick="consoleLogs=[];renderLog()"
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
</div>
<div style="display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap">
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px">Dir:</span>
<span id="log-lbl-dir" style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px"></span>
<button class="log-dir-btn active" id="logdir-all" onclick="setLogDir('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">RX</button>
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">TX</button>
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level">Level:</span>
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level"></span>
<button class="log-lvl-btn active" id="loglvl-all" onclick="setLogLevel('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⛔ Errors</button>
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⚠ Warn</button>
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px">Topic:</span>
<button class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">AMS</button>
<button class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">print</button>
<button class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">info</button>
<button class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">status</button>
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<span id="log-lbl-topic" style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px"></span>
<button id="log-topic-ams" class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button id="log-topic-print" class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button id="log-topic-info" class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button id="log-topic-status" class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
</div>
<div class="console" id="console-log" style="height:calc(100vh - 260px);min-height:160px" onscroll="onLogScroll()"></div>
</div>
</div>
<!-- ═══ EINSTELLUNGEN ═══ -->
<div class="panel" id="panel-settings">
<div class="settings-wrap">
<div class="settings-cats">
<button class="set-cat active" id="setcat-connection" onclick="showSettingsCat('connection')"><span>🔌</span> <span id="setcat-lbl-connection">Verbindung</span></button>
<button class="set-cat" id="setcat-printer" onclick="showSettingsCat('printer')"><span>🖨</span> <span id="setcat-lbl-printer">Drucker</span></button>
<button class="set-cat" id="setcat-display" onclick="showSettingsCat('display')"><span>🎨</span> <span id="setcat-lbl-display">Darstellung</span></button>
<button class="set-cat" id="setcat-filament" onclick="showSettingsCat('filament')"><span>🧵</span> <span id="setcat-lbl-filament">Filament</span></button>
<button class="set-cat" id="setcat-system" onclick="showSettingsCat('system')"><span></span> <span id="setcat-lbl-system">System</span></button>
</div>
<div class="settings-content">
<!-- Verbindung -->
<div class="set-group active" id="setgrp-connection">
<div class="card">
<div class="card-title"><span>🔌</span> <span id="modal-sec-connection">Verbindung</span></div>
<div class="modal-field" style="margin-bottom:12px">
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
</div>
<div class="modal-field">
<label id="lbl-printer-ip">Drucker-IP</label>
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
</div>
<div class="modal-field">
<label id="lbl-mqtt-port">MQTT-Port</label>
<input type="number" id="s-mqtt-port" placeholder="9883">
</div>
<div class="modal-field">
<label id="lbl-username">MQTT-Benutzername</label>
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-password">MQTT-Passwort</label>
<input type="password" id="s-password" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-device-id">Device-ID</label>
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
</div>
<div class="modal-field">
<label id="lbl-mode-id">Mode-ID</label>
<input type="text" id="s-mode-id" placeholder="20030">
</div>
</div>
</div>
<!-- Drucker -->
<div class="set-group" id="setgrp-printer">
<div class="card">
<div class="card-title"><span>🖨</span> <span id="modal-sec-print">Druckeinstellungen</span></div>
<div class="modal-field">
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
<select id="s-default-slot">
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
<option value="0" id="opt-slot-0">Slot 1</option>
<option value="1" id="opt-slot-1">Slot 2</option>
<option value="2" id="opt-slot-2">Slot 3</option>
<option value="3" id="opt-slot-3">Slot 4</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
</div>
<div class="modal-field">
<label id="lbl-file-ready-mode">Nach Upload: Druckstart-Verhalten</label>
<select id="s-file-ready-mode">
<option value="1" id="opt-file-ready-dialog">Print-Dialog</option>
<option value="0" id="opt-file-ready-banner">Print-Leiste</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
</div>
</div>
</div>
<!-- Darstellung -->
<div class="set-group" id="setgrp-display">
<div class="card">
<div class="card-title"><span>🎨</span> <span id="setcat-lbl-display2">Darstellung</span></div>
<div class="modal-field">
<label id="lbl-set-lang">Sprache</label>
<select id="s-lang-select" onchange="setLanguage(this.value)">
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="es">Espanol</option>
<option value="fr">Français</option>
<option value="zh-cn">中文(简体)</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="toggleTheme()"><span id="lbl-set-theme">Hell / Dunkel umschalten</span></button>
</div>
<div class="modal-field">
<label id="lbl-poll-interval">Poll-Intervall (Sekunden)</label>
<input type="number" id="s-poll-interval" min="1" max="60" step="1" placeholder="3" oninput="onPollIntervalInput()">
<small style="color:var(--txt2)" id="lbl-poll-hint">Wie oft die Bridge den Drucker-Status abfragt</small>
</div>
</div>
</div>
<!-- Filament -->
<div class="set-group" id="setgrp-filament">
<div class="card">
<div class="card-title"><span>🧵</span> <span id="modal-sec-orca-profiles">OrcaSlicer-Profile</span></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
</div>
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
style="background:var(--raised);color:var(--txt)">
<span id="lbl-orca-profiles-import">Profile importieren</span>
</button>
</div>
<div class="card">
<div class="card-title"><span>🎯</span> <span id="lbl-filament-mapping">Filament-Profil-Mapping (pro Slot)</span></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="filament-mapping-hint">
Festes Orca-Profil pro AMS-Slot. Beim Slicer-Sync sendet die Bridge dieses Profil statt „Generic".
</div>
<div id="filament-mapping-list"></div>
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveFilamentMapping()"><span id="lbl-filament-mapping-save">Mapping speichern</span></button>
</div>
<div class="card">
<div class="card-title"><span>👁</span> <span id="lbl-visible-vendors">Sichtbare Hersteller (Profil-Dropdown)</span></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="visible-vendors-hint">
Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.
</div>
<input type="text" id="vendor-filter-search" placeholder="Hersteller suchen…" oninput="renderVendorChecklist()"
style="width:100%;padding:6px 10px;margin-bottom:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px">
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
</div>
</div>
<!-- System -->
<div class="set-group" id="setgrp-system">
<div class="card">
<div class="card-title"><span></span> <span id="modal-sec-version">Version</span></div>
<div class="update-row">
<span id="s-version-label" style="font-size:13px;color:var(--txt)"></span>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
</div>
<div class="update-status" id="update-status" style="margin-top:6px"></div>
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
<span id="lbl-update-apply">Jetzt installieren</span>
</button>
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
</div>
</div>
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings" style="margin-top:14px">Speichern &amp; Neustart</button>
</div>
</div>
</div>
</main>
</div>
@@ -596,14 +539,13 @@
<button class="bnav-btn" onclick="showPanel('printers');loadPrinterTab()" id="bnb-printers"><span class="bnav-icon">🖨</span>Drucker</button>
<button class="bnav-btn" onclick="showPanel('store');loadStore()" id="bnb-store"><span class="bnav-icon">🗂</span>Browser</button>
<button class="bnav-btn" onclick="showPanel('console');clearLogBadge()" id="bnb-console"><span class="bnav-icon"></span>Log<span id="log-badge-bot" style="display:none;margin-left:3px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 4px;font-weight:700"></span></button>
<button class="bnav-btn" onclick="showPanel('settings')" id="bnb-settings"><span class="bnav-icon"></span>Setup</button>
</nav>
<!-- Web-Upload-Verify-Dialog -->
<div class="modal-overlay" id="store-web-verify-dialog" onclick="if(event.target===this)closeStoreWebVerifyDialog()">
<div class="modal-box" style="max-width:420px;width:100%">
<div class="modal-header" style="margin-bottom:14px">
<div class="modal-header" style="margin-bottom:12px">
<span class="modal-title" id="store-web-verify-title">Datei verifizieren</span>
<button onclick="closeStoreWebVerifyDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
@@ -618,31 +560,24 @@
<!-- Filament-Slot-Dialog -->
<div class="modal-overlay" id="filament-dialog" onclick="if(event.target===this)closeFilamentDialog()">
<div class="modal-box" style="max-width:380px;width:100%">
<div class="modal-box" style="max-width:380px;width:100%;gap:0">
<div class="modal-header" style="margin-bottom:14px">
<span class="modal-title" id="fd-title" style="font-size:14px;word-break:break-all"></span>
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
<button type="button" id="fd-objects-toggle" onclick="toggleFdObjects()"
style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer;font-size:12px;text-align:left">
<span id="fd-objects-arrow" style="font-size:10px;transition:transform .15s"></span>
<span><span id="fd-objects-toggle-lbl">Objekte überspringen</span></span>
<span id="fd-objects-count" style="margin-left:auto;color:var(--txt2);font-weight:600"></span>
</button>
<div id="fd-objects-body" style="display:none;margin-top:8px">
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
<div id="fd-options-section" style="margin-bottom:6px">
<p id="fd-options-title" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0"></p>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px;padding:8px 10px;background:var(--raised);border:1px solid var(--border);border-radius:8px">
<input type="checkbox" id="fd-auto-leveling" style="width:auto;margin:0" onchange="_printOptionsAutoLeveling=this.checked?1:0">
<label id="lbl-fd-auto-leveling" style="margin:0;cursor:pointer;line-height:1.2" for="fd-auto-leveling"></label>
</div>
</div>
<div style="margin-bottom:14px;padding:10px 12px;background:var(--raised);border-radius:8px;border:1px solid var(--border)">
<div style="font-size:11px;font-weight:600;color:var(--txt2);margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em" id="fd-options-title">Druckoptionen</div>
<div style="display:flex;align-items:center;gap:8px">
<input type="checkbox" id="fd-auto-leveling" style="width:auto;margin:0">
<label for="fd-auto-leveling" style="margin:0;cursor:pointer;font-size:13px" id="fd-lbl-auto-leveling">Auto-Leveling</label>
</div>
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0">GCode-Kanal → AMS-Slot zuweisen:</p>
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
<div id="fd-objects-section" style="display:none;margin-bottom:14px">
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0">Objekte überspringen (optional):</p>
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="fd-cancel" onclick="closeFilamentDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
@@ -680,7 +615,7 @@
<div id="skip-list" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;margin-bottom:12px"></div>
<div id="skip-status" style="font-size:12px;color:var(--txt2);min-height:16px;margin-bottom:8px"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
<button id="skip-cancel" onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer"></button>
<button id="skip-confirm" onclick="confirmSkip()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Überspringen</button>
</div>
</div>
@@ -716,15 +651,15 @@
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:8px">
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA</button>
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA+</button>
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PETG</button>
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">TPU</button>
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">ABS / ASA</button>
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PA / PC</button>
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 1</button>
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 2</button>
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 3</button>
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
</div>
<div id="ace-dry-dialog-custom-name-row" style="display:none;align-items:center;gap:12px;margin-bottom:14px">
<label id="ace-dry-dialog-custom-name-label" style="min-width:190px;font-size:12px;color:var(--txt)">Custom Name</label>

View File

@@ -212,24 +212,6 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
.panel{display:none}
.panel.active{display:block}
/* ── SETTINGS (Master-Detail) ── */
.settings-wrap{display:grid;grid-template-columns:200px 1fr;gap:16px;align-items:start}
.settings-cats{display:flex;flex-direction:column;gap:4px;position:sticky;top:12px}
.set-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;
border:1px solid transparent;background:var(--raised);color:var(--txt2);cursor:pointer;
font-size:13px;text-align:left;transition:background .15s,color .15s}
.set-cat:hover{color:var(--txt)}
.set-cat.active{background:var(--accent);color:#fff;border-color:var(--accent)}
.set-group{display:none}
.set-group.active{display:block}
.set-group .card{margin-bottom:14px}
@media(max-width:768px){
.settings-wrap{grid-template-columns:1fr}
.settings-cats{flex-direction:row;flex-wrap:wrap;position:static;overflow-x:auto}
.set-cat{flex:1;min-width:auto;justify-content:center;padding:8px 6px;font-size:12px}
.set-cat .nav-text{display:inline}
}
/* ── FILE BROWSER UPLOAD ZONE ── */
#store-upload-zone{
display:flex;flex-direction:column;align-items:center;justify-content:center;

View File

@@ -69,6 +69,13 @@
"ace_dry_dialog_save_restart": "Speichern & Neustart",
"ace_dry_dialog_custom_name": "Eigener Name",
"ace_dry_dialog_reset_default": "Auf Standard zurücksetzen",
"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": "Custom",
"cam_placeholder": "📷 Kamera nicht gestartet",
"cam_stream_unavailable": "Stream nicht verfügbar",
"btn_cam_start": "▶ Kamera",
@@ -127,21 +134,8 @@
"settings_title": "Einstellungen",
"settings_connection": "Verbindung",
"settings_print": "Druckeinstellungen",
"settings_poll": "Poll-Intervall (Sekunden)",
"settings_poll": "Poll-Intervall",
"settings_version": "Version",
"nav_settings": "Einstellungen",
"settings_cat_display": "Darstellung",
"settings_cat_filament": "Filament",
"settings_cat_language": "Sprache",
"settings_cat_theme": "Hell / Dunkel umschalten",
"settings_filament_mapping": "Filament-Profil-Mapping (pro Slot)",
"settings_filament_mapping_save": "Mapping speichern",
"settings_visible_vendors": "Sichtbare Hersteller (Profil-Dropdown)",
"settings_visible_vendors_hint": "Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic\" und eigene Profile sind immer sichtbar.",
"settings_visible_vendors_save": "Auswahl speichern",
"progress_action_print": "Drucken",
"progress_action_slots": "Slots zuordnen",
"progress_action_clear": "Leeren",
"settings_save": "Speichern & Neustart",
"settings_printer_name": "Drucker-Name",
"settings_printer_ip": "Drucker-IP",
@@ -154,7 +148,12 @@
"settings_default_slot": "Standard-Slot (Einfarbdruck)",
"settings_slot_auto": "Auto (alle belegten Slots)",
"settings_auto_leveling": "Auto-Leveling vor Druck",
"fd_options_title": "Optionen",
"print_auto_leveling": "Auto-Leveling für diesen Druck",
"settings_camera_on_print": "Kamera bei Druckstart einschalten",
"settings_file_ready_mode": "Druckdialog starten",
"settings_file_ready_banner": "Druckleiste",
"settings_file_ready_dialog": "Druckdialog",
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
"update_check": "Auf Updates prüfen",
"update_checking": "Prüfe...",
@@ -192,7 +191,21 @@
"orca_profile_done": "Importiert",
"orca_profile_skipped": "übersprungen",
"log_dir_all": "Alle",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "Richtung:",
"log_lvl_label": "Level:",
"log_lvl_err": "⛔ Fehler",
"log_lvl_warn": "⚠ Warnung",
"log_topic_label": "Thema:",
"log_topic_ams": "AMS",
"log_topic_print": "Druck",
"log_topic_info": "Info",
"log_topic_status": "Status",
"log_download": "⬇ Download",
"log_auto": "⬇ Auto",
"log_clear": "✕ Leeren",
"log_filter_placeholder": "Filtern…",
"file_ready_btn": "▶ Druck starten",
"file_slots_btn": "🎨 Slots wählen",
"file_cancel_btn": "✕ Abbrechen",
@@ -205,8 +218,9 @@
"skip_select_at_least_one": "Bitte mindestens ein Objekt wählen.",
"skip_sending": "Sende …",
"skip_success": "Objekte werden übersprungen.",
"skip_cancel": "Abbrechen",
"skip_confirm": "Überspringen",
"fd_objects_hint": "Objekte überspringen (optional):",
"fd_objects_toggle": "Objekte überspringen",
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
"fd_cancel": "Abbrechen",
"fd_print": "▶ Drucken",
@@ -258,33 +272,5 @@
"sf_new": "Neu",
"ss_date": "↓ Datum",
"ss_name": "AZ Name",
"ss_dur": "⏱ Druckzeit",
"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": "Custom",
"fd_options_title": "Optionen",
"print_auto_leveling": "Auto-Leveling für diesen Druck",
"settings_file_ready_mode": "Druckdialog starten",
"settings_file_ready_banner": "Druckleiste",
"settings_file_ready_dialog": "Druckdialog",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "Richtung:",
"log_lvl_err": "⛔ Fehler",
"log_lvl_warn": "⚠ Warnung",
"log_topic_label": "Thema:",
"log_topic_ams": "AMS",
"log_topic_print": "Druck",
"log_topic_info": "Info",
"log_topic_status": "Status",
"log_download": "⬇ Download",
"log_auto": "⬇ Auto",
"log_clear": "✕ Leeren",
"log_filter_placeholder": "Filtern…",
"skip_cancel": "Abbrechen",
"skip_confirm": "Überspringen"
"ss_dur": "⏱ Druckzeit"
}

View File

@@ -134,20 +134,7 @@
"settings_title": "Settings",
"settings_connection": "Connection",
"settings_print": "Print Settings",
"settings_poll": "Poll Interval (seconds)",
"nav_settings": "Settings",
"settings_cat_display": "Appearance",
"settings_cat_filament": "Filament",
"settings_cat_language": "Language",
"settings_cat_theme": "Toggle light / dark",
"settings_filament_mapping": "Filament profile mapping (per slot)",
"settings_filament_mapping_save": "Save mapping",
"settings_visible_vendors": "Visible vendors (profile dropdown)",
"settings_visible_vendors_hint": "Only these vendors appear in the slot profile dropdown. Nothing selected = show all. \"Generic\" and your own profiles are always visible.",
"settings_visible_vendors_save": "Save selection",
"progress_action_print": "Print",
"progress_action_slots": "Map slots",
"progress_action_clear": "Clear",
"settings_poll": "Poll Interval",
"settings_version": "Version",
"settings_save": "Save & Restart",
"settings_printer_name": "Printer Name",
@@ -163,10 +150,10 @@
"settings_auto_leveling": "Auto-Leveling Default",
"fd_options_title": "Print Options",
"print_auto_leveling": "Auto-Leveling",
"settings_camera_on_print": "Turn camera on at print start",
"settings_file_ready_mode": "Start Print Behavior",
"settings_file_ready_banner": "Print Bar",
"settings_file_ready_dialog": "Print Dialog",
"settings_camera_on_print": "Turn camera on at print start",
"settings_web_upload_warning": "Show warning when printing web uploads",
"update_check": "Check for Updates",
"update_checking": "Checking...",
@@ -228,13 +215,12 @@
"skip_btn_label": "Objects",
"skip_no_objects": "No objects in this print.",
"skip_already": "skipped",
"skip_cancel": "Cancel",
"skip_confirm": "Skip",
"skip_select_at_least_one": "Please pick at least one object.",
"skip_sending": "Sending …",
"skip_success": "Objects will be skipped.",
"skip_cancel": "Cancel",
"skip_confirm": "Skip",
"fd_objects_hint": "Skip objects (optional):",
"fd_objects_toggle": "Skip objects",
"fd_slots_hint": "Assign GCode channel to AMS slot:",
"fd_cancel": "Cancel",
"fd_print": "▶ Print",

View File

@@ -69,6 +69,13 @@
"ace_dry_dialog_save_restart": "Guardar y reiniciar",
"ace_dry_dialog_custom_name": "Nombre personalizado",
"ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
"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": "Personalizado",
"cam_placeholder": "📷 Cámara no iniciada",
"cam_stream_unavailable": "Stream no disponible",
"btn_cam_start": "▶ Cámara",
@@ -127,20 +134,7 @@
"settings_title": "Configuración",
"settings_connection": "Conexión",
"settings_print": "Ajustes de impresión",
"settings_poll": "Intervalo de sondeo (segundos)",
"nav_settings": "Ajustes",
"settings_cat_display": "Apariencia",
"settings_cat_filament": "Filamento",
"settings_cat_language": "Idioma",
"settings_cat_theme": "Alternar claro / oscuro",
"settings_filament_mapping": "Asignación de perfil de filamento (por ranura)",
"settings_filament_mapping_save": "Guardar asignación",
"settings_visible_vendors": "Fabricantes visibles (lista de perfiles)",
"settings_visible_vendors_hint": "Solo estos fabricantes aparecen en la lista de perfiles de ranura. Nada seleccionado = mostrar todos. «Generic» y tus propios perfiles siempre son visibles.",
"settings_visible_vendors_save": "Guardar selección",
"progress_action_print": "Imprimir",
"progress_action_slots": "Asignar ranuras",
"progress_action_clear": "Vaciar",
"settings_poll": "Intervalo de sondeo",
"settings_version": "Versión",
"settings_save": "Guardar y reiniciar",
"settings_printer_name": "Nombre de impresora",
@@ -154,7 +148,12 @@
"settings_default_slot": "Ranura predeterminada (un color)",
"settings_slot_auto": "Auto (todos los slots cargados)",
"settings_auto_leveling": "Autonivelado antes de imprimir",
"fd_options_title": "Opciones",
"print_auto_leveling": "Autonivelado para esta impresión",
"settings_camera_on_print": "Encender cámara al iniciar impresión",
"settings_file_ready_mode": "Iniciar diálogo de impresión",
"settings_file_ready_banner": "Barra de impresión",
"settings_file_ready_dialog": "Diálogo de impresión",
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
"update_check": "Buscar actualizaciones",
"update_checking": "Comprobando...",
@@ -192,7 +191,21 @@
"orca_profile_done": "Importado",
"orca_profile_skipped": "omitido",
"log_dir_all": "Todos",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "Dirección:",
"log_lvl_label": "Nivel:",
"log_lvl_err": "⛔ Errores",
"log_lvl_warn": "⚠ Avisos",
"log_topic_label": "Tema:",
"log_topic_ams": "AMS",
"log_topic_print": "Impresión",
"log_topic_info": "Info",
"log_topic_status": "Estado",
"log_download": "⬇ Descargar",
"log_auto": "⬇ Auto",
"log_clear": "✕ Limpiar",
"log_filter_placeholder": "Filtrar…",
"file_ready_btn": "▶ Iniciar impresión",
"file_slots_btn": "🎨 Seleccionar ranuras",
"file_cancel_btn": "✕ Cancelar",
@@ -205,8 +218,9 @@
"skip_select_at_least_one": "Selecciona al menos un objeto.",
"skip_sending": "Enviando …",
"skip_success": "Se omitirán los objetos.",
"skip_cancel": "Cancelar",
"skip_confirm": "Omitir",
"fd_objects_hint": "Omitir objetos (opcional):",
"fd_objects_toggle": "Omitir objetos",
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
"fd_cancel": "Cancelar",
"fd_print": "▶ Imprimir",
@@ -258,33 +272,5 @@
"sf_new": "Nuevo",
"ss_date": "↓ Fecha",
"ss_name": "AZ Nombre",
"ss_dur": "⏱ Tiempo de impresión",
"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": "Personalizado",
"fd_options_title": "Opciones",
"print_auto_leveling": "Autonivelado para esta impresión",
"settings_file_ready_mode": "Iniciar diálogo de impresión",
"settings_file_ready_banner": "Barra de impresión",
"settings_file_ready_dialog": "Diálogo de impresión",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "Dirección:",
"log_lvl_err": "⛔ Errores",
"log_lvl_warn": "⚠ Avisos",
"log_topic_label": "Tema:",
"log_topic_ams": "AMS",
"log_topic_print": "Impresión",
"log_topic_info": "Info",
"log_topic_status": "Estado",
"log_download": "⬇ Descargar",
"log_auto": "⬇ Auto",
"log_clear": "✕ Limpiar",
"log_filter_placeholder": "Filtrar…",
"skip_cancel": "Cancelar",
"skip_confirm": "Omitir"
"ss_dur": "⏱ Tiempo de impresión"
}

View File

@@ -69,6 +69,13 @@
"ace_dry_dialog_save_restart": "Enregistrer et redémarrer",
"ace_dry_dialog_custom_name": "Nom personnalisé",
"ace_dry_dialog_reset_default": "Réinitialiser",
"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": "Personnalisé",
"cam_placeholder": "📷 Caméra non démarrée",
"cam_stream_unavailable": "Flux indisponible",
"btn_cam_start": "▶ Caméra",
@@ -127,20 +134,7 @@
"settings_title": "Paramètres",
"settings_connection": "Connexion",
"settings_print": "Paramètres d'impression",
"settings_poll": "Intervalle de sondage (secondes)",
"nav_settings": "Paramètres",
"settings_cat_display": "Apparence",
"settings_cat_filament": "Filament",
"settings_cat_language": "Langue",
"settings_cat_theme": "Basculer clair / sombre",
"settings_filament_mapping": "Mappage du profil de filament (par emplacement)",
"settings_filament_mapping_save": "Enregistrer le mappage",
"settings_visible_vendors": "Fabricants visibles (liste des profils)",
"settings_visible_vendors_hint": "Seuls ces fabricants apparaissent dans la liste des profils d'emplacement. Rien de sélectionné = tout afficher. « Generic » et vos propres profils sont toujours visibles.",
"settings_visible_vendors_save": "Enregistrer la sélection",
"progress_action_print": "Imprimer",
"progress_action_slots": "Affecter les emplacements",
"progress_action_clear": "Vider",
"settings_poll": "Intervalle de sondage",
"settings_version": "Version",
"settings_save": "Enregistrer et redémarrer",
"settings_printer_name": "Nom de l'imprimante",
@@ -154,7 +148,12 @@
"settings_default_slot": "Slot par défaut (couleur unique)",
"settings_slot_auto": "Auto (tous les slots chargés)",
"settings_auto_leveling": "Mise à niveau auto avant impression",
"fd_options_title": "Options",
"print_auto_leveling": "Mise à niveau auto pour cette impression",
"settings_camera_on_print": "Activer la caméra au démarrage de l'impression",
"settings_file_ready_mode": "Démarrer le dialogue d'impression",
"settings_file_ready_banner": "Barre d'impression",
"settings_file_ready_dialog": "Dialogue d'impression",
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
"update_check": "Vérifier les mises à jour",
"update_checking": "Vérification…",
@@ -192,7 +191,21 @@
"orca_profile_done": "Importé",
"orca_profile_skipped": "ignoré",
"log_dir_all": "Tout",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "Sens :",
"log_lvl_label": "Niveau :",
"log_lvl_err": "⛔ Erreurs",
"log_lvl_warn": "⚠ Avert.",
"log_topic_label": "Sujet :",
"log_topic_ams": "AMS",
"log_topic_print": "Impression",
"log_topic_info": "Info",
"log_topic_status": "Statut",
"log_download": "⬇ Télécharger",
"log_auto": "⬇ Auto",
"log_clear": "✕ Effacer",
"log_filter_placeholder": "Filtrer…",
"file_ready_btn": "▶ Lancer l'impression",
"file_slots_btn": "🎨 Choisir les slots",
"file_cancel_btn": "✕ Annuler",
@@ -205,8 +218,9 @@
"skip_select_at_least_one": "Veuillez sélectionner au moins un objet.",
"skip_sending": "Envoi …",
"skip_success": "Les objets seront ignorés.",
"skip_cancel": "Annuler",
"skip_confirm": "Ignorer",
"fd_objects_hint": "Ignorer des objets (optionnel) :",
"fd_objects_toggle": "Ignorer des objets",
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
"fd_cancel": "Annuler",
"fd_print": "▶ Imprimer",
@@ -258,33 +272,6 @@
"sf_new": "Nouveau",
"ss_date": "↓ Date",
"ss_name": "AZ Nom",
"ss_dur": "⏱ Durée d'impression",
"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": "Personnalisé",
"fd_options_title": "Options",
"print_auto_leveling": "Mise à niveau auto pour cette impression",
"settings_file_ready_mode": "Démarrer le dialogue d'impression",
"settings_file_ready_banner": "Barre d'impression",
"settings_file_ready_dialog": "Dialogue d'impression",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "Sens :",
"log_lvl_err": "⛔ Erreurs",
"log_lvl_warn": "⚠ Avert.",
"log_topic_label": "Sujet :",
"log_topic_ams": "AMS",
"log_topic_print": "Impression",
"log_topic_info": "Info",
"log_topic_status": "Statut",
"log_download": "⬇ Télécharger",
"log_auto": "⬇ Auto",
"log_clear": "✕ Effacer",
"log_filter_placeholder": "Filtrer…",
"skip_cancel": "Annuler",
"skip_confirm": "Ignorer"
"ss_dur": "⏱ Durée d'impression"
}

View File

@@ -69,6 +69,13 @@
"ace_dry_dialog_save_restart": "保存并重启",
"ace_dry_dialog_custom_name": "自定义名称",
"ace_dry_dialog_reset_default": "恢复默认",
"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": "自定义",
"cam_placeholder": "📷 相机未启动",
"cam_stream_unavailable": "视频流不可用",
"btn_cam_start": "▶ 相机",
@@ -127,20 +134,7 @@
"settings_title": "设置",
"settings_connection": "连接",
"settings_print": "打印设置",
"settings_poll": "轮询间隔(秒)",
"nav_settings": "设置",
"settings_cat_display": "外观",
"settings_cat_filament": "耗材",
"settings_cat_language": "语言",
"settings_cat_theme": "切换浅色 / 深色",
"settings_filament_mapping": "耗材配置映射(每槽位)",
"settings_filament_mapping_save": "保存映射",
"settings_visible_vendors": "可见厂商(配置下拉框)",
"settings_visible_vendors_hint": "仅这些厂商会出现在槽位配置下拉框中。未选择 = 显示全部。“Generic”和您自己的配置始终可见。",
"settings_visible_vendors_save": "保存选择",
"progress_action_print": "打印",
"progress_action_slots": "分配槽位",
"progress_action_clear": "清除",
"settings_poll": "轮询间隔",
"settings_version": "版本",
"settings_save": "保存并重启",
"settings_printer_name": "打印机名称",
@@ -154,7 +148,12 @@
"settings_default_slot": "默认槽位 (单色)",
"settings_slot_auto": "自动 (所有已装载槽位)",
"settings_auto_leveling": "打印前自动调平",
"fd_options_title": "选项",
"print_auto_leveling": "本次打印自动调平",
"settings_camera_on_print": "打印开始时开启相机",
"settings_file_ready_mode": "开始打印对话框",
"settings_file_ready_banner": "打印栏",
"settings_file_ready_dialog": "打印对话框",
"settings_web_upload_warning": "打印网页上传文件时显示警告",
"update_check": "检查更新",
"update_checking": "检查中...",
@@ -192,7 +191,21 @@
"orca_profile_done": "已导入",
"orca_profile_skipped": "跳过",
"log_dir_all": "全部",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "方向:",
"log_lvl_label": "级别:",
"log_lvl_err": "⛔ 错误",
"log_lvl_warn": "⚠ 警告",
"log_topic_label": "主题:",
"log_topic_ams": "AMS",
"log_topic_print": "打印",
"log_topic_info": "信息",
"log_topic_status": "状态",
"log_download": "⬇ 下载",
"log_auto": "⬇ 自动",
"log_clear": "✕ 清空",
"log_filter_placeholder": "筛选…",
"file_ready_btn": "▶ 开始打印",
"file_slots_btn": "🎨 选择槽位",
"file_cancel_btn": "✕ 取消",
@@ -205,8 +218,9 @@
"skip_select_at_least_one": "请至少选择一个对象。",
"skip_sending": "发送中 …",
"skip_success": "对象将被跳过。",
"skip_cancel": "取消",
"skip_confirm": "跳过",
"fd_objects_hint": "跳过对象 (可选):",
"fd_objects_toggle": "跳过对象",
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
"fd_cancel": "取消",
"fd_print": "▶ 打印",
@@ -258,33 +272,5 @@
"sf_new": "新",
"ss_date": "↓ 日期",
"ss_name": "AZ 名称",
"ss_dur": "⏱ 打印时间",
"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": "自定义",
"fd_options_title": "选项",
"print_auto_leveling": "本次打印自动调平",
"settings_file_ready_mode": "开始打印对话框",
"settings_file_ready_banner": "打印栏",
"settings_file_ready_dialog": "打印对话框",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "方向:",
"log_lvl_err": "⛔ 错误",
"log_lvl_warn": "⚠ 警告",
"log_topic_label": "主题:",
"log_topic_ams": "AMS",
"log_topic_print": "打印",
"log_topic_info": "信息",
"log_topic_status": "状态",
"log_download": "⬇ 下载",
"log_auto": "⬇ 自动",
"log_clear": "✕ 清空",
"log_filter_placeholder": "筛选…",
"skip_cancel": "取消",
"skip_confirm": "跳过"
"ss_dur": "⏱ 打印时间"
}