diff --git a/config.ini.example b/config.ini.example index f0fa3b3..21a7f72 100644 --- a/config.ini.example +++ b/config.ini.example @@ -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 diff --git a/config_loader.py b/config_loader.py index 807f911..c64de21 100644 --- a/config_loader.py +++ b/config_loader.py @@ -60,6 +60,7 @@ 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"), + "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"), } @@ -72,6 +73,17 @@ def _load_config_file(path: pathlib.Path): except (configparser.NoSectionError, configparser.NoOptionError): pass + # 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") + if legacy: + os.environ["PRINT_START_DIALOG"] = legacy + except (configparser.NoSectionError, configparser.NoOptionError): + pass + if "PRINT_START_DIALOG" not in os.environ and "FILE_READY_DIALOG" in os.environ: + os.environ["PRINT_START_DIALOG"] = os.environ["FILE_READY_DIALOG"] + def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path): """Einmalige Migration: .env → config.ini anlegen.""" @@ -98,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] = { @@ -258,4 +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")) diff --git a/env_loader.py b/env_loader.py index 342df7b..5ed9379 100644 --- a/env_loader.py +++ b/env_loader.py @@ -48,3 +48,4 @@ MODE_ID = get("MODE_ID", "") DEVICE_ID = get("DEVICE_ID", "") DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto") AUTO_LEVELING = int(get("AUTO_LEVELING", "1")) +PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1"))) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 8caf876..555608d 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -783,6 +783,7 @@ class KobraXBridge: "print_speed_mode": 2, "connection_error": "", "file_ready": "", + "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}, } @@ -806,6 +807,9 @@ 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 confirmed/applied via skip/start + self._pending_preprint_skip: list[str] = [] + self._pending_preprint_skip_deadline: float = 0.0 # Theme-Name prüfen (keine Sonderzeichen oder Umlaute) raw_theme = (getattr(args, "ui_theme", None) or "default").strip() @@ -1059,26 +1063,41 @@ class KobraXBridge: """ d = payload.get("data") or {} skipped = d.get("objects_skip_parts") or d.get("skipped") or d.get("skipped_parts") or [] - # Liste immer (auch leer) übernehmen – sonst bleibt sie auf alten Stand + # 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 + + # 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 {} + 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 "" - if thumb: + # 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)): self._thumbnail_b64 = thumb log.info(f"Vorschaubild empfangen: {len(thumb)} Zeichen base64") # Part-Skip: Objekt-Liste + optionales SVG (v0.9.10) objs = details.get("objects_skip_parts") or [] svg = details.get("svg_image") or "" if objs: - filename = d.get("filename") or details.get("filename") or self._last_uploaded_file + filename = file_name if filename: try: self._store.update_file_objects(filename, objs, svg) @@ -2433,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) @@ -2447,7 +2475,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))) filename = gcode_file["filename"] file_path = gcode_file["path"] @@ -2475,6 +2503,9 @@ 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, }, } @@ -2487,6 +2518,9 @@ class KobraXBridge: if result is None: return self._json_cors({"error": "Keine Antwort vom Drucker"}, status=504) + if excluded_objects: + loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects)) + # Job in History starten self._current_job_id = self._store.start_job( gcode_file_id=gcode_file["id"], @@ -2741,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() @@ -2773,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( @@ -2819,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 @@ -2859,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 @@ -2882,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, @@ -2914,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") @@ -2957,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: @@ -2970,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", "") @@ -2996,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, }, } @@ -3012,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): @@ -3034,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"}) @@ -3646,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"], @@ -3659,6 +3760,7 @@ class KobraXBridge: "thumbnail": thumbnail, "connection_error": s["connection_error"], "file_ready": s["file_ready"], + "print_start_dialog": getattr(self._args, "print_start_dialog", 1), "version": self._read_version(), }) @@ -3796,6 +3898,7 @@ 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), + "print_start_dialog": getattr(self._args, "print_start_dialog", 1), "web_upload_warning": getattr(self._args, "web_upload_warning", 1), "ace_dry_presets": self._ace_dry_presets, }) @@ -3826,6 +3929,8 @@ 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)))))) if not cfg.has_option("bridge", "poll_interval"): cfg.set("bridge", "poll_interval", "3") @@ -3835,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(): @@ -3992,8 +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", "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") @@ -4885,6 +4993,8 @@ 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("--host", default="0.0.0.0", diff --git a/web/themes/default/app.js b/web/themes/default/app.js index a84ba72..31c359a 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -3,6 +3,7 @@ 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; @@ -10,6 +11,8 @@ var camUserStopped=false; // user stopped camera manually — suppress auto-rest var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support) var currentStep=1; var currentPanel='dashboard'; +var _printOptionsAutoLeveling=1; +var _printOptionsAutoLevelingSnapshot=1; var aceAutoRefillPrefs=(function(){ try{return JSON.parse(localStorage.getItem('aceAutoRefillPrefs')||'{}')||{};}catch(_){return {};} })(); @@ -260,6 +263,8 @@ function applyLang(){ 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); @@ -338,7 +343,12 @@ 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('fd-options-title',T.fd_options_title); + 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); @@ -385,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); @@ -620,12 +645,25 @@ function applyState(){ 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 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){ - if(s.file_ready&&s.print_state==='standby'){ - document.getElementById('file-ready-name').textContent=s.file_ready; + if(readyStandby&&!dialogMode){ + document.getElementById('file-ready-name').textContent=readyFile; frb.style.display='flex'; - }else{frb.style.display='none';} + }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'); @@ -634,6 +672,10 @@ 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(); // header @@ -674,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:''; @@ -910,7 +952,9 @@ 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=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); }); var v=localStorage.getItem('pollInterval')||'2000'; @@ -1170,6 +1214,26 @@ function cancelReadyFile(){ post('/api/file_ready/clear',{}) .then(function(){document.getElementById('file-ready-banner').style.display='none';}); } +function startProgressReadyFile(){ + var target=((S.file_ready||'')||(S.filename||'')).trim(); + if(!target)return; + S.file_ready=target; + startReadyFileWithSlots(); +} +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; highlightMatBtn(m); @@ -1269,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, @@ -1280,6 +1347,7 @@ 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:printStartDialog, web_upload_warning:webUploadWarning, }).then(function(){ btn.textContent=T.update_restarting; @@ -1615,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)); }); } @@ -1681,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(); @@ -1927,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; @@ -1960,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); @@ -2069,37 +2147,36 @@ function startReadyFileWithSlots(){ maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(); }); return; } - _filamentDialogMode='banner'; - _storeFilename=S.file_ready||''; - // Banner must never reuse stale store-file context. - _storeFileId=null; - _gcodeFilaments=[]; - - function openWithSlots(){ - fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ - openFilamentDialog(d.result||[]); - }).catch(function(){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([]);}); }); } @@ -2166,6 +2243,8 @@ function openFilamentDialog(slots){ } }).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){ @@ -2257,10 +2336,36 @@ function closeFilamentDialog(){ if(dlg)dlg.classList.remove('open'); } +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; @@ -2299,7 +2404,7 @@ function confirmFilamentPrint(){ // 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}) + 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'; @@ -2314,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}) + 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');} diff --git a/web/themes/default/index.html b/web/themes/default/index.html index d662a58..154e53e 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -42,7 +42,7 @@ - + @@ -89,6 +89,13 @@