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
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,5 +1,15 @@
|
||||||
# Changelog
|
# 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
|
## 0.1.2 - 2026-02-03
|
||||||
### Added
|
### Added
|
||||||
- Persistente Einstellungen (config.json) im User-Config-Verzeichnis (XDG_CONFIG_HOME / ~/.config, macOS Application Support, Windows APPDATA).
|
- Persistente Einstellungen (config.json) im User-Config-Verzeichnis (XDG_CONFIG_HOME / ~/.config, macOS Application Support, Windows APPDATA).
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "paperless-asn-label-generator"
|
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."
|
description = "GUI tool to generate ASN barcode/QR labels for paperless-ngx (A4/Avery L4731) with calibration."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
@ -31,4 +31,3 @@ package-dir = {"" = "src"}
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
__all__ = ["__version__"]
|
__all__ = ["__version__"]
|
||||||
__version__ = "0.1.2"
|
__version__ = "0.1.3"
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ from reportlab.graphics.barcode import code128
|
||||||
import qrcode
|
import qrcode
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
|
||||||
|
|
||||||
BarcodeKind = Literal["QR", "CODE128"]
|
BarcodeKind = Literal["QR", "CODE128"]
|
||||||
|
|
||||||
|
|
@ -208,11 +210,19 @@ DEFAULT_SETTINGS = {
|
||||||
"zeros": "7",
|
"zeros": "7",
|
||||||
"kind": "QR",
|
"kind": "QR",
|
||||||
"border": False,
|
"border": False,
|
||||||
# deine aktuellen Preset-Kalibrierwerte:
|
# neutrale Kalibrierung für frische Installationen
|
||||||
"off_x": "-4.5",
|
"off_x": "0.0",
|
||||||
"off_y": "7.0",
|
"off_y": "0.0",
|
||||||
"pitch_dx": "1.2",
|
"pitch_dx": "0.0",
|
||||||
"pitch_dy": "0.42",
|
"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):
|
class App(ttk.Frame):
|
||||||
|
|
@ -239,22 +249,20 @@ class App(ttk.Frame):
|
||||||
|
|
||||||
self._loading_settings = False
|
self._loading_settings = False
|
||||||
self._save_job = None
|
self._save_job = None
|
||||||
|
self._pending_config_status: str | None = None
|
||||||
|
|
||||||
# 1) Laden (überschreibt Defaults, falls config existiert)
|
# 1) Laden (überschreibt Defaults, falls config existiert)
|
||||||
self._load_settings()
|
self._load_settings()
|
||||||
|
|
||||||
# UI bauen
|
# UI einmal bauen
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
self._flush_pending_config_status()
|
||||||
self._apply_mode()
|
self._apply_mode()
|
||||||
self._update_preview()
|
self._update_preview()
|
||||||
|
|
||||||
# 2) Autosave aktivieren
|
# 2) Autosave aktivieren
|
||||||
self._install_autosave_traces()
|
self._install_autosave_traces()
|
||||||
|
|
||||||
self._build_ui()
|
|
||||||
self._apply_mode()
|
|
||||||
self._update_preview()
|
|
||||||
|
|
||||||
|
|
||||||
def _config_path(self) -> Path:
|
def _config_path(self) -> Path:
|
||||||
"""
|
"""
|
||||||
|
|
@ -288,12 +296,31 @@ class App(ttk.Frame):
|
||||||
"off_y": self.var_off_y.get(),
|
"off_y": self.var_off_y.get(),
|
||||||
"pitch_dx": self.var_pitch_dx.get(),
|
"pitch_dx": self.var_pitch_dx.get(),
|
||||||
"pitch_dy": self.var_pitch_dy.get(),
|
"pitch_dy": self.var_pitch_dy.get(),
|
||||||
|
"calibration_mode": "delta",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _apply_settings_from_dict(self, d: dict) -> None:
|
def _apply_settings_from_dict(self, d: dict) -> None:
|
||||||
# Beim Laden sollen keine Autosaves getriggert werden
|
# Beim Laden sollen keine Autosaves getriggert werden
|
||||||
self._loading_settings = True
|
self._loading_settings = True
|
||||||
try:
|
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 "start" in d: self.var_start.set(str(d["start"]))
|
||||||
if "mode" in d: self.var_mode.set(str(d["mode"]))
|
if "mode" in d: self.var_mode.set(str(d["mode"]))
|
||||||
if "count" in d: self.var_count.set(str(d["count"]))
|
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 "zeros" in d: self.var_zeros.set(str(d["zeros"]))
|
||||||
if "kind" in d: self.var_kind.set(str(d["kind"]))
|
if "kind" in d: self.var_kind.set(str(d["kind"]))
|
||||||
if "border" in d: self.var_border.set(bool(d["border"]))
|
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"]))
|
set_calibration_var("off_x", self.var_off_x)
|
||||||
if "off_y" in d: self.var_off_y.set(str(d["off_y"]))
|
set_calibration_var("off_y", self.var_off_y)
|
||||||
if "pitch_dx" in d: self.var_pitch_dx.set(str(d["pitch_dx"]))
|
set_calibration_var("pitch_dx", self.var_pitch_dx)
|
||||||
if "pitch_dy" in d: self.var_pitch_dy.set(str(d["pitch_dy"]))
|
set_calibration_var("pitch_dy", self.var_pitch_dy)
|
||||||
finally:
|
finally:
|
||||||
self._loading_settings = False
|
self._loading_settings = False
|
||||||
|
|
||||||
|
|
@ -316,9 +343,11 @@ class App(ttk.Frame):
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
self._apply_settings_from_dict(data)
|
self._apply_settings_from_dict(data)
|
||||||
except Exception:
|
else:
|
||||||
# bewusst still: fehlerhafte config soll GUI nicht killen
|
self._set_config_status(f"⚠️ Config hat falsches Format, verwende Defaults: {path}")
|
||||||
pass
|
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:
|
def _save_settings_now(self) -> None:
|
||||||
if getattr(self, "_loading_settings", False):
|
if getattr(self, "_loading_settings", False):
|
||||||
|
|
@ -329,9 +358,9 @@ class App(ttk.Frame):
|
||||||
tmp = path.with_suffix(".tmp")
|
tmp = path.with_suffix(".tmp")
|
||||||
tmp.write_text(json.dumps(self._settings_to_dict(), indent=2, ensure_ascii=False), encoding="utf-8")
|
tmp.write_text(json.dumps(self._settings_to_dict(), indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
tmp.replace(path)
|
tmp.replace(path)
|
||||||
except Exception:
|
self._set_config_status("")
|
||||||
# still ignorieren oder optional status zeigen
|
except Exception as e:
|
||||||
pass
|
self._set_config_status(f"⚠️ Config konnte nicht gespeichert werden ({path}): {e}")
|
||||||
|
|
||||||
def _schedule_save(self) -> None:
|
def _schedule_save(self) -> None:
|
||||||
# Debounce: erst nach kurzer Pause speichern
|
# Debounce: erst nach kurzer Pause speichern
|
||||||
|
|
@ -368,7 +397,7 @@ class App(ttk.Frame):
|
||||||
v.trace("w", lambda *_: self._schedule_save())
|
v.trace("w", lambda *_: self._schedule_save())
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
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.grid(sticky="nsew")
|
||||||
self.master.columnconfigure(0, weight=1)
|
self.master.columnconfigure(0, weight=1)
|
||||||
self.master.rowconfigure(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 = ttk.Label(self, text="", foreground="#333")
|
||||||
self.lbl_status.grid(row=1, column=0, columnspan=2, sticky="we", pady=(10, 0))
|
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:
|
def _apply_mode(self) -> None:
|
||||||
mode = self.var_mode.get()
|
mode = self.var_mode.get()
|
||||||
|
|
@ -484,6 +527,12 @@ class App(ttk.Frame):
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ValueError(f"{field} ist keine gültige Zahl (z.B. 0.2).")
|
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]:
|
def _effective_count_and_pages(self) -> tuple[int, int]:
|
||||||
mode = self.var_mode.get()
|
mode = self.var_mode.get()
|
||||||
if mode == "pages":
|
if mode == "pages":
|
||||||
|
|
@ -511,13 +560,22 @@ class App(ttk.Frame):
|
||||||
raise ValueError("Führende Nullen muss >= 0 sein.")
|
raise ValueError("Führende Nullen muss >= 0 sein.")
|
||||||
return make_asn_text(prefix, start, zeros)
|
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_x = self._parse_float(self.var_off_x.get(), "Offset X")
|
||||||
off_y = self._parse_float(self.var_off_y.get(), "Offset Y")
|
off_y = self._parse_float(self.var_off_y.get(), "Offset Y")
|
||||||
pdx = self._parse_float(self.var_pitch_dx.get(), "Spalten-Pitch Δ")
|
pdx = self._parse_float(self.var_pitch_dx.get(), "Spalten-Pitch Δ")
|
||||||
pdy = self._parse_float(self.var_pitch_dy.get(), "Zeilen-Pitch Δ")
|
pdy = self._parse_float(self.var_pitch_dy.get(), "Zeilen-Pitch Δ")
|
||||||
return off_x, off_y, pdx, pdy
|
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:
|
def _update_preview(self, *_args) -> None:
|
||||||
try:
|
try:
|
||||||
text = self._current_text()
|
text = self._current_text()
|
||||||
|
|
@ -527,9 +585,14 @@ class App(ttk.Frame):
|
||||||
self.preview.configure(image=self._preview_imgtk)
|
self.preview.configure(image=self._preview_imgtk)
|
||||||
|
|
||||||
count, pages = self._effective_count_and_pages()
|
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()
|
off_x, off_y, pdx, pdy = self._calibration()
|
||||||
self.lbl_status.configure(
|
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:
|
except Exception as e:
|
||||||
self.preview.configure(image="")
|
self.preview.configure(image="")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue