mirror of
https://github.com/AxonByteDev/paperless-asn-label-generator.git
synced 2026-08-06 08:42:03 +00:00
fix(ui): dedupe init, bump 0.1.3
This commit is contained in:
parent
c448e8bb75
commit
6f7476f137
4 changed files with 99 additions and 27 deletions
12
CHANGELOG.md
12
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).
|
||||
- Stabileres Laden/Speichern (fehlerhafte config bricht GUI nicht ab).
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
__all__ = ["__version__"]
|
||||
__version__ = "0.1.2"
|
||||
__version__ = "0.1.3"
|
||||
|
|
|
|||
|
|
@ -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="")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue