Persistente Settings + Reset-Button + Changelog

This commit is contained in:
MrLetsPlay4Fun 2026-02-03 17:29:35 +01:00
parent e1e60cc502
commit ed0e0d40a7
4 changed files with 200 additions and 19 deletions

10
CHANGELOG.md Normal file
View file

@ -0,0 +1,10 @@
# Changelog
## 0.1.2 - 2026-02-03
### Added
- Persistente Einstellungen (config.json) im User-Config-Verzeichnis (XDG_CONFIG_HOME / ~/.config, macOS Application Support, Windows APPDATA).
- Autosave der GUI-Einstellungen (debounced), damit Drucker-Kalibrierwerte erhalten bleiben.
- 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).

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "paperless-asn-label-generator" name = "paperless-asn-label-generator"
version = "0.1.1" version = "0.1.2"
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"

View file

@ -1,2 +1,2 @@
__all__ = ["__version__"] __all__ = ["__version__"]
__version__ = "0.1.1" __version__ = "0.1.2"

View file

@ -18,6 +18,8 @@ from __future__ import annotations
import math import math
import os import os
import sys import sys
import json
from pathlib import Path
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal, Tuple from typing import Literal, Tuple
@ -197,38 +199,174 @@ def generate_pdf(
c.save() c.save()
return pages, current return pages, current
DEFAULT_SETTINGS = {
"start": "1",
"mode": "labels",
"count": str(AVERY_L4731.labels_per_page),
"pages": "1",
"prefix": "ASN",
"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",
}
class App(ttk.Frame): class App(ttk.Frame):
def __init__(self, master: tk.Tk) -> None: def __init__(self, master: tk.Tk) -> None:
super().__init__(master, padding=12) super().__init__(master, padding=12)
self.master = master self.master = master
self.var_start = tk.StringVar(value="1") self.var_start = tk.StringVar(value=DEFAULT_SETTINGS["start"])
self.var_mode = tk.StringVar(value=DEFAULT_SETTINGS["mode"])
# Quantity mode self.var_count = tk.StringVar(value=DEFAULT_SETTINGS["count"])
self.var_mode = tk.StringVar(value="labels") # "labels" or "pages" self.var_pages = tk.StringVar(value=DEFAULT_SETTINGS["pages"])
self.var_count = tk.StringVar(value=str(AVERY_L4731.labels_per_page)) self.var_prefix = tk.StringVar(value=DEFAULT_SETTINGS["prefix"])
self.var_pages = tk.StringVar(value="1") self.var_zeros = tk.StringVar(value=DEFAULT_SETTINGS["zeros"])
self.var_kind = tk.StringVar(value=DEFAULT_SETTINGS["kind"])
self.var_prefix = tk.StringVar(value="ASN") self.var_border = tk.BooleanVar(value=DEFAULT_SETTINGS["border"])
self.var_zeros = tk.StringVar(value="7") self.var_off_x = tk.StringVar(value=DEFAULT_SETTINGS["off_x"])
self.var_kind = tk.StringVar(value="QR") self.var_off_y = tk.StringVar(value=DEFAULT_SETTINGS["off_y"])
self.var_border = tk.BooleanVar(value=False) self.var_pitch_dx = tk.StringVar(value=DEFAULT_SETTINGS["pitch_dx"])
self.var_pitch_dy = tk.StringVar(value=DEFAULT_SETTINGS["pitch_dy"])
# NEW calibration vars (mm)
self.var_off_x = tk.StringVar(value="-4.5")
self.var_off_y = tk.StringVar(value="7.0")
self.var_pitch_dx = tk.StringVar(value="1.2")
self.var_pitch_dy = tk.StringVar(value="0.42")
self._preview_imgtk: ImageTk.PhotoImage | None = None self._preview_imgtk: ImageTk.PhotoImage | None = None
self._count_entry: ttk.Entry | None = None self._count_entry: ttk.Entry | None = None
self._pages_entry: ttk.Entry | None = None self._pages_entry: ttk.Entry | None = None
self._loading_settings = False
self._save_job = None
# 1) Laden (überschreibt Defaults, falls config existiert)
self._load_settings()
# UI bauen
self._build_ui() self._build_ui()
self._apply_mode() self._apply_mode()
self._update_preview() self._update_preview()
# 2) Autosave aktivieren
self._install_autosave_traces()
self._build_ui()
self._apply_mode()
self._update_preview()
def _config_path(self) -> Path:
"""
Cross-platform config path:
- Windows: %APPDATA%/paperless-ngx-asn-labels/config.json
- macOS: ~/Library/Application Support/paperless-ngx-asn-labels/config.json
- Linux: $XDG_CONFIG_HOME/paperless-ngx-asn-labels/config.json (fallback ~/.config/...)
"""
home = Path.home()
if os.name == "nt":
base = Path(os.environ.get("APPDATA", home / "AppData" / "Roaming"))
elif sys.platform == "darwin":
base = home / "Library" / "Application Support"
else:
base = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config"))
return base / "paperless-ngx-asn-labels" / "config.json"
def _settings_to_dict(self) -> dict:
return {
"start": self.var_start.get(),
"mode": self.var_mode.get(),
"count": self.var_count.get(),
"pages": self.var_pages.get(),
"prefix": self.var_prefix.get(),
"zeros": self.var_zeros.get(),
"kind": self.var_kind.get(),
"border": bool(self.var_border.get()),
"off_x": self.var_off_x.get(),
"off_y": self.var_off_y.get(),
"pitch_dx": self.var_pitch_dx.get(),
"pitch_dy": self.var_pitch_dy.get(),
}
def _apply_settings_from_dict(self, d: dict) -> None:
# Beim Laden sollen keine Autosaves getriggert werden
self._loading_settings = True
try:
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"]))
if "pages" in d: self.var_pages.set(str(d["pages"]))
if "prefix" in d: self.var_prefix.set(str(d["prefix"]))
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"]))
finally:
self._loading_settings = False
def _load_settings(self) -> None:
path = self._config_path()
try:
if path.exists():
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
def _save_settings_now(self) -> None:
if getattr(self, "_loading_settings", False):
return
path = self._config_path()
try:
path.parent.mkdir(parents=True, exist_ok=True)
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
def _schedule_save(self) -> None:
# Debounce: erst nach kurzer Pause speichern
if getattr(self, "_loading_settings", False):
return
if getattr(self, "_save_job", None) is not None:
try:
self.after_cancel(self._save_job)
except Exception:
pass
self._save_job = self.after(500, self._save_settings_now)
def _install_autosave_traces(self) -> None:
# alle Variablen bei Änderung -> save schedulen
vars_to_watch = [
self.var_start,
self.var_mode,
self.var_count,
self.var_pages,
self.var_prefix,
self.var_zeros,
self.var_kind,
self.var_border,
self.var_off_x,
self.var_off_y,
self.var_pitch_dx,
self.var_pitch_dy,
]
for v in vars_to_watch:
try:
v.trace_add("write", lambda *_: self._schedule_save())
except Exception:
# fallback (sehr alte Tk-Versionen)
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("paperless-ngx ASN Label Generator (A4 / Avery L4731) mit Kalibrierung")
self.grid(sticky="nsew") self.grid(sticky="nsew")
@ -318,6 +456,10 @@ class App(ttk.Frame):
ttk.Button(btns, text="PDF erzeugen…", command=self.on_generate_pdf).grid(row=0, column=0, sticky="we", padx=(0, 6)) ttk.Button(btns, text="PDF erzeugen…", command=self.on_generate_pdf).grid(row=0, column=0, sticky="we", padx=(0, 6))
ttk.Button(btns, text="Ordner öffnen", command=self.on_open_folder).grid(row=0, column=1, sticky="we", padx=(6, 0)) ttk.Button(btns, text="Ordner öffnen", command=self.on_open_folder).grid(row=0, column=1, sticky="we", padx=(6, 0))
ttk.Button(btns, text="Config löschen", command=self.on_reset_config).grid(
row=1, column=0, columnspan=2, sticky="we", pady=(6, 0)
)
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))
@ -476,6 +618,35 @@ class App(ttk.Frame):
except Exception: except Exception:
messagebox.showinfo("Info", f"Ordner: {folder}") messagebox.showinfo("Info", f"Ordner: {folder}")
def on_reset_config(self) -> None:
# Nachfrage, damit niemand aus Versehen alles wegklickt
if not messagebox.askyesno(
"Einstellungen zurücksetzen",
"Gespeicherte Einstellungen (config.json) löschen und auf Standardwerte zurücksetzen?"
):
return
path = self._config_path()
# 1) config.json löschen (wenn vorhanden)
try:
if path.exists():
path.unlink()
except Exception as e:
messagebox.showerror("Fehler", f"Konnte config nicht löschen:\n{path}\n\n{e}")
return
# 2) Defaults anwenden (ohne Autosave-Trigger-Chaos)
self._apply_settings_from_dict(DEFAULT_SETTINGS)
# UI/Preview aktualisieren
self._apply_mode()
self._update_preview()
# 3) Optional: Defaults direkt wieder speichern (damit nächster Start garantiert Default hat)
self._save_settings_now()
messagebox.showinfo("OK", f"Zurückgesetzt.\n\nConfig-Pfad:\n{path}")
def main() -> None: def main() -> None:
root = tk.Tk() root = tk.Tk()