From 6f7476f1373f518d4131c8f43099f501167c351b Mon Sep 17 00:00:00 2001 From: MrLetsPlay4Fun Date: Sun, 8 Feb 2026 08:29:23 +0100 Subject: [PATCH] fix(ui): dedupe init, bump 0.1.3 --- CHANGELOG.md | 12 +- pyproject.toml | 3 +- src/paperless_asn_label_generator/__init__.py | 2 +- src/paperless_asn_label_generator/app.py | 109 ++++++++++++++---- 4 files changed, 99 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7b639..23e1a5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.1.3 - 2026-02-08 +### Changed +- Kalibrierungs-Eingaben sind jetzt als sichtbare Delta-Werte ausgelegt (Default `0.0` in der UI), während intern weiterhin die Basiswerte genutzt werden. +- Persistierte Kalibrierung speichert den Modus explizit als `calibration_mode = "delta"`; bestehende Legacy-Configs ohne Marker werden kompatibel übernommen. +- GUI-Titel zeigt jetzt die aktuelle App-Version an. + +### Fixed +- Doppelten `_build_ui()`-Aufruf entfernt, damit beim Start keine doppelten Widgets oder Event-Bindings entstehen. +- Fehler beim Laden/Speichern der `config.json` werden in der Oberfläche sichtbar angezeigt statt still ignoriert. + ## 0.1.2 - 2026-02-03 ### Added - Persistente Einstellungen (config.json) im User-Config-Verzeichnis (XDG_CONFIG_HOME / ~/.config, macOS Application Support, Windows APPDATA). @@ -7,4 +17,4 @@ - Button "Config löschen": löscht config.json und setzt Standard-Preset-Werte zurück. ### Fixed -- Stabileres Laden/Speichern (fehlerhafte config bricht GUI nicht ab). \ No newline at end of file +- Stabileres Laden/Speichern (fehlerhafte config bricht GUI nicht ab). diff --git a/pyproject.toml b/pyproject.toml index 6807365..99cc277 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "paperless-asn-label-generator" -version = "0.1.2" +version = "0.1.3" description = "GUI tool to generate ASN barcode/QR labels for paperless-ngx (A4/Avery L4731) with calibration." readme = "README.md" requires-python = ">=3.10" @@ -31,4 +31,3 @@ package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] - diff --git a/src/paperless_asn_label_generator/__init__.py b/src/paperless_asn_label_generator/__init__.py index 834cdae..0c08b61 100644 --- a/src/paperless_asn_label_generator/__init__.py +++ b/src/paperless_asn_label_generator/__init__.py @@ -1,2 +1,2 @@ __all__ = ["__version__"] -__version__ = "0.1.2" \ No newline at end of file +__version__ = "0.1.3" diff --git a/src/paperless_asn_label_generator/app.py b/src/paperless_asn_label_generator/app.py index a2a1ef8..6090ce2 100644 --- a/src/paperless_asn_label_generator/app.py +++ b/src/paperless_asn_label_generator/app.py @@ -35,6 +35,8 @@ from reportlab.graphics.barcode import code128 import qrcode from PIL import Image, ImageTk +from . import __version__ + BarcodeKind = Literal["QR", "CODE128"] @@ -208,11 +210,19 @@ DEFAULT_SETTINGS = { "zeros": "7", "kind": "QR", "border": False, - # deine aktuellen Preset-Kalibrierwerte: - "off_x": "-4.5", - "off_y": "7.0", - "pitch_dx": "1.2", - "pitch_dy": "0.42", + # neutrale Kalibrierung für frische Installationen + "off_x": "0.0", + "off_y": "0.0", + "pitch_dx": "0.0", + "pitch_dy": "0.0", +} + +# Effektive Basiswerte, die beim Drucken intern immer addiert werden. +BASE_CALIBRATION_MM = { + "off_x": -4.5, + "off_y": 7.0, + "pitch_dx": 1.2, + "pitch_dy": 0.42, } class App(ttk.Frame): @@ -239,22 +249,20 @@ class App(ttk.Frame): self._loading_settings = False self._save_job = None + self._pending_config_status: str | None = None # 1) Laden (überschreibt Defaults, falls config existiert) self._load_settings() - # UI bauen + # UI einmal bauen self._build_ui() + self._flush_pending_config_status() self._apply_mode() self._update_preview() # 2) Autosave aktivieren self._install_autosave_traces() - self._build_ui() - self._apply_mode() - self._update_preview() - def _config_path(self) -> Path: """ @@ -288,12 +296,31 @@ class App(ttk.Frame): "off_y": self.var_off_y.get(), "pitch_dx": self.var_pitch_dx.get(), "pitch_dy": self.var_pitch_dy.get(), + "calibration_mode": "delta", } def _apply_settings_from_dict(self, d: dict) -> None: # Beim Laden sollen keine Autosaves getriggert werden self._loading_settings = True try: + calibration_mode = str(d.get("calibration_mode", "")).strip().lower() + legacy_absolute = calibration_mode != "delta" + if calibration_mode == "": + # Übergangsfall: Wenn alle Werte 0 sind (vorherige Zwischenversion), als Delta interpretieren. + parsed = [self._coerce_float(d.get(k)) for k in ("off_x", "off_y", "pitch_dx", "pitch_dy") if k in d] + if parsed and all(v is not None and abs(v) < 1e-9 for v in parsed): + legacy_absolute = False + + def set_calibration_var(key: str, var: tk.StringVar) -> None: + if key not in d: + return + if legacy_absolute: + absolute = self._coerce_float(d.get(key)) + if absolute is not None: + var.set(str(absolute - BASE_CALIBRATION_MM[key])) + return + var.set(str(d[key])) + if "start" in d: self.var_start.set(str(d["start"])) if "mode" in d: self.var_mode.set(str(d["mode"])) if "count" in d: self.var_count.set(str(d["count"])) @@ -302,10 +329,10 @@ class App(ttk.Frame): if "zeros" in d: self.var_zeros.set(str(d["zeros"])) if "kind" in d: self.var_kind.set(str(d["kind"])) if "border" in d: self.var_border.set(bool(d["border"])) - if "off_x" in d: self.var_off_x.set(str(d["off_x"])) - if "off_y" in d: self.var_off_y.set(str(d["off_y"])) - if "pitch_dx" in d: self.var_pitch_dx.set(str(d["pitch_dx"])) - if "pitch_dy" in d: self.var_pitch_dy.set(str(d["pitch_dy"])) + set_calibration_var("off_x", self.var_off_x) + set_calibration_var("off_y", self.var_off_y) + set_calibration_var("pitch_dx", self.var_pitch_dx) + set_calibration_var("pitch_dy", self.var_pitch_dy) finally: self._loading_settings = False @@ -316,9 +343,11 @@ class App(ttk.Frame): data = json.loads(path.read_text(encoding="utf-8")) if isinstance(data, dict): self._apply_settings_from_dict(data) - except Exception: - # bewusst still: fehlerhafte config soll GUI nicht killen - pass + else: + self._set_config_status(f"⚠️ Config hat falsches Format, verwende Defaults: {path}") + except Exception as e: + # GUI bleibt benutzbar, Fehler wird aber sichtbar gemacht + self._set_config_status(f"⚠️ Config konnte nicht geladen werden ({path}): {e}") def _save_settings_now(self) -> None: if getattr(self, "_loading_settings", False): @@ -329,9 +358,9 @@ class App(ttk.Frame): tmp = path.with_suffix(".tmp") tmp.write_text(json.dumps(self._settings_to_dict(), indent=2, ensure_ascii=False), encoding="utf-8") tmp.replace(path) - except Exception: - # still ignorieren oder optional status zeigen - pass + self._set_config_status("") + except Exception as e: + self._set_config_status(f"⚠️ Config konnte nicht gespeichert werden ({path}): {e}") def _schedule_save(self) -> None: # Debounce: erst nach kurzer Pause speichern @@ -368,7 +397,7 @@ class App(ttk.Frame): v.trace("w", lambda *_: self._schedule_save()) def _build_ui(self) -> None: - self.master.title("paperless-ngx ASN Label Generator (A4 / Avery L4731) – mit Kalibrierung") + self.master.title(f"paperless-ngx ASN Label Generator v{__version__} (A4 / Avery L4731) - mit Kalibrierung") self.grid(sticky="nsew") self.master.columnconfigure(0, weight=1) self.master.rowconfigure(0, weight=1) @@ -462,6 +491,20 @@ class App(ttk.Frame): self.lbl_status = ttk.Label(self, text="", foreground="#333") self.lbl_status.grid(row=1, column=0, columnspan=2, sticky="we", pady=(10, 0)) + self.lbl_config_status = ttk.Label(self, text="", foreground="#b00020") + self.lbl_config_status.grid(row=2, column=0, columnspan=2, sticky="we", pady=(4, 0)) + + def _set_config_status(self, message: str) -> None: + lbl = getattr(self, "lbl_config_status", None) + if lbl is None: + self._pending_config_status = message + return + lbl.configure(text=message) + self._pending_config_status = message + + def _flush_pending_config_status(self) -> None: + if self._pending_config_status is not None: + self._set_config_status(self._pending_config_status) def _apply_mode(self) -> None: mode = self.var_mode.get() @@ -484,6 +527,12 @@ class App(ttk.Frame): except Exception: raise ValueError(f"{field} ist keine gültige Zahl (z.B. 0.2).") + def _coerce_float(self, value: object) -> float | None: + try: + return float(str(value).strip().replace(",", ".")) + except Exception: + return None + def _effective_count_and_pages(self) -> tuple[int, int]: mode = self.var_mode.get() if mode == "pages": @@ -511,13 +560,22 @@ class App(ttk.Frame): raise ValueError("Führende Nullen muss >= 0 sein.") return make_asn_text(prefix, start, zeros) - def _calibration(self) -> tuple[float, float, float, float]: + def _calibration_delta(self) -> tuple[float, float, float, float]: off_x = self._parse_float(self.var_off_x.get(), "Offset X") off_y = self._parse_float(self.var_off_y.get(), "Offset Y") pdx = self._parse_float(self.var_pitch_dx.get(), "Spalten-Pitch Δ") pdy = self._parse_float(self.var_pitch_dy.get(), "Zeilen-Pitch Δ") return off_x, off_y, pdx, pdy + def _calibration(self) -> tuple[float, float, float, float]: + off_x_delta, off_y_delta, pdx_delta, pdy_delta = self._calibration_delta() + return ( + BASE_CALIBRATION_MM["off_x"] + off_x_delta, + BASE_CALIBRATION_MM["off_y"] + off_y_delta, + BASE_CALIBRATION_MM["pitch_dx"] + pdx_delta, + BASE_CALIBRATION_MM["pitch_dy"] + pdy_delta, + ) + def _update_preview(self, *_args) -> None: try: text = self._current_text() @@ -527,9 +585,14 @@ class App(ttk.Frame): self.preview.configure(image=self._preview_imgtk) count, pages = self._effective_count_and_pages() + off_x_delta, off_y_delta, pdx_delta, pdy_delta = self._calibration_delta() off_x, off_y, pdx, pdy = self._calibration() self.lbl_status.configure( - text=f"Beispiel: {text} | {count} Labels ≈ {pages} Seiten | Offset({off_x},{off_y})mm PitchΔ({pdx},{pdy})mm" + text=( + f"Beispiel: {text} | {count} Labels ≈ {pages} Seiten" + f" | Eingabe Offset({off_x_delta},{off_y_delta})mm PitchΔ({pdx_delta},{pdy_delta})mm" + f" | Effektiv Offset({off_x},{off_y})mm PitchΔ({pdx},{pdy})mm" + ) ) except Exception as e: self.preview.configure(image="")