This commit is contained in:
2026-05-28 18:58:38 +02:00
parent 641bb10479
commit 28f429885a
4950 changed files with 933414 additions and 666 deletions

View File

@@ -1,11 +1,15 @@
# -*- coding: utf-8 -*-
"""
AzaOrdnerMixin Ordner-Fenster (Ablage: KG, Briefe, Rezepte, KOGU, Diktat; Export/Import).
AzaOrdnerMixin AzA-Ordner-Fenster (modernes AzA-Design).
Zeigt gespeicherte KG, Briefe, Rezepte, Kostengutsprachen, Diktate, Transkripte.
Doppelklick oeffnet Datei. Fenster bleibt immer offen.
"""
import os
import re
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime
from tkinter import messagebox
from aza_persistence import (
ensure_ablage_dirs,
@@ -20,77 +24,236 @@ from aza_persistence import (
save_autotext,
_clamp_geometry_str,
)
from aza_ui_helpers import center_window, add_resize_grip, add_font_scale_control, RoundedButton
from aza_ui_helpers import center_window, add_resize_grip, add_font_scale_control
from aza_config import ABLAGE_SUBFOLDERS
# ── Design ────────────────────────────────────────────────────────────────────
_WIN_BG = "#EEF4F8"
_HDR_BG = "#1A4D6D"
_HDR_FG = "#FFFFFF"
_HDR_SUB = "#A0C0DC"
_CARD_BG = "#FFFFFF"
_CARD_BD = "#C8D8E8"
_TEXT = "#1A3D55"
_TEXT_SUB = "#607890"
_TAB_ACT = "#1A4D6D"
_TAB_INACT = "#D4E7F5"
_TAB_FG_A = "#FFFFFF"
_TAB_FG_I = "#1A4D6D"
_LB_SEL = "#1A8ACC"
_FF = "Segoe UI"
# ── Sortier-Helfer fuer das Ordner-Fenster ─────────────────────────────────────
# Eintragsnamen sehen typischerweise so aus: "44 Diktat 21.05.2026 23:14".
# Die Anzeige soll *neueste oben* sein. Bisher sortierte list_ablage_files()
# in aza_persistence.py nach fuehrender Nummer absteigend — das fuehrt dazu,
# dass ein neu hinzugefuegter Eintrag mit kleiner Nummer faelschlich ganz unten
# steht. Wir lassen die Persistenz-Funktion bewusst unveraendert und
# re-sortieren die Liste hier robust nach Datum + Uhrzeit.
_ABLAGE_DT_FULL_RE = re.compile(r"(\d{1,2})\.(\d{1,2})\.(\d{4})\s+(\d{1,2}):(\d{2})")
_ABLAGE_DT_ISO_RE = re.compile(r"(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})")
_ABLAGE_DATE_ONLY_RE = re.compile(r"(\d{1,2})\.(\d{1,2})\.(\d{4})")
_ABLAGE_DATE_ISO_ONLY_RE = re.compile(r"(\d{4})-(\d{1,2})-(\d{1,2})")
_ABLAGE_LEADING_NUM_RE = re.compile(r"^\s*(\d+)")
def _parse_ablage_datetime(name: str):
"""Extrahiert datetime aus dem Anzeigenamen.
Reihenfolge: dd.mm.yyyy hh:mm > yyyy-mm-dd hh:mm > dd.mm.yyyy > yyyy-mm-dd.
Liefert ``None``, wenn nichts erkannt werden konnte.
"""
if not name:
return None
m = _ABLAGE_DT_FULL_RE.search(name)
if m:
try:
d, mo, y, hh, mm = (int(x) for x in m.groups())
return datetime(y, mo, d, hh, mm)
except (ValueError, IndexError):
pass
m = _ABLAGE_DT_ISO_RE.search(name)
if m:
try:
y, mo, d, hh, mm = (int(x) for x in m.groups())
return datetime(y, mo, d, hh, mm)
except (ValueError, IndexError):
pass
m = _ABLAGE_DATE_ONLY_RE.search(name)
if m:
try:
d, mo, y = (int(x) for x in m.groups())
return datetime(y, mo, d)
except (ValueError, IndexError):
pass
m = _ABLAGE_DATE_ISO_ONLY_RE.search(name)
if m:
try:
y, mo, d = (int(x) for x in m.groups())
return datetime(y, mo, d)
except (ValueError, IndexError):
pass
return None
def _parse_ablage_leading_number(name: str) -> int:
"""Liest die fuehrende Nummer (z.B. ``44`` aus ``44 Diktat ...``)."""
if not name:
return 0
m = _ABLAGE_LEADING_NUM_RE.match(str(name))
if not m:
return 0
try:
return int(m.group(1))
except (ValueError, IndexError):
return 0
def _ablage_store_mtime() -> float:
"""mtime der zentralen ablage.json — Fallback-Sortierschluessel.
Da alle Eintraege in einer einzigen JSON liegen, ist diese mtime fuer
Eintraege ohne lesbares Datum identisch. Sie liefert in dem Fall aber
immerhin einen stabilen, deterministischen Wert (und schlaegt
spaeter zusaetzlich, falls einzelne Eintraege jemals auf eigene
Dateien umgestellt werden sollten).
"""
try:
path = os.path.join(_ablage_base_path(), "ablage.json")
if os.path.isfile(path):
return float(os.path.getmtime(path))
except Exception:
pass
return 0.0
def _sort_ablage_entries(names) -> list:
"""Sortiert Eintragsnamen fuer die Listbox-Anzeige (neueste oben).
Sortierschluessel pro Eintrag (alle absteigend, dank ``reverse=True``):
1. erkanntes Datum + Uhrzeit (dd.mm.yyyy hh:mm bevorzugt)
2. Bei identischem Datum: fuehrende Nummer absteigend
3. Eintraege ohne erkennbares Datum landen stabil am Ende
(Pythons sort ist stabil, daher bleibt deren urspruengliche
Reihenfolge erhalten).
Die Methode beruehrt aza_persistence.list_ablage_files nicht und
funktioniert mit jeder beliebigen Liste von Eintragsnamen.
"""
if not names:
return []
fallback_mtime = _ablage_store_mtime()
def _key(n):
dt = _parse_ablage_datetime(n)
num = _parse_ablage_leading_number(n)
if dt is not None:
return (1, dt.timestamp(), num)
return (0, fallback_mtime, num)
return sorted(list(names), key=_key, reverse=True)
class AzaOrdnerMixin:
"""Mixin für das Ordner-/Ablage-Fenster (Speichern, Laden, Export/Import)."""
"""Mixin fuer den AzA-Ordner (modernes Design, Doppelklick zum Laden)."""
def open_ordner_window(self):
"""Fenster für Ablage: KG, Briefe, Rezepte, Kostengutsprachen in Unterordnern; Export/Import. Bleibt sichtbar bis es geschlossen wird."""
"""Oeffnet den AzA-Ordner in modernem Design. Bleibt offen bis Benutzer schliesst."""
ensure_ablage_dirs()
base_path = _ablage_base_path()
ORDNER_MIN_W, ORDNER_MIN_H = 750, 600
ORDNER_MIN_W, ORDNER_MIN_H = 660, 560
win = tk.Toplevel(self)
win.title("Ordner Ablage & Export/Import")
win.title("AzA-Ordner")
win.transient(self)
win.minsize(ORDNER_MIN_W, ORDNER_MIN_H)
win.configure(bg="#B9ECFA")
win.attributes("-topmost", True)
win.configure(bg=_WIN_BG)
try:
win.attributes("-alpha", 0.0)
except Exception:
pass
if hasattr(self, "_aza_windows"):
self._aza_windows.add(win)
self._register_window(win)
# Fensterposition: gespeichert laden oder zentrieren
saved_geom = load_ordner_geometry()
if saved_geom:
try:
win.geometry(_clamp_geometry_str(saved_geom, ORDNER_MIN_W, ORDNER_MIN_H))
except Exception:
win.geometry("800x650")
center_window(win, 800, 650)
else:
# Keine gespeicherte Position → zentrieren
win.geometry("640x500")
center_window(win, 640, 500)
# Groesse setzen — Position wird nach kurzer Verzoegerung gesetzt,
# damit Windows/transient die Positionierung nicht ueberschreibt
win.geometry(f"{ORDNER_MIN_W}x{ORDNER_MIN_H}")
def save_ordner_geom():
try:
save_ordner_geometry(win.geometry())
except Exception:
pass
_ordner_geom_after_id = [None]
def on_ordner_configure(e):
if e.widget is win and _ordner_geom_after_id[0]:
self.after_cancel(_ordner_geom_after_id[0])
if e.widget is win:
_ordner_geom_after_id[0] = self.after(400, save_ordner_geom)
win.bind("<Configure>", on_ordner_configure)
def on_ordner_close():
def _place_next_to_main():
try:
win.update_idletasks()
sw = win.winfo_screenwidth()
sh = win.winfo_screenheight()
px = self.winfo_rootx()
py = self.winfo_rooty()
pw = self.winfo_width()
ww, wh = ORDNER_MIN_W, ORDNER_MIN_H
x = px + pw + 8
if x + ww > sw:
x = max(0, px - ww - 8)
y = max(0, min(py, sh - wh))
win.geometry(f"{ww}x{wh}+{x}+{y}")
except Exception:
pass
win.after(50, _place_next_to_main)
# Geometrie persistieren
_after_id = [None]
def _save_geom():
try:
save_ordner_geometry(win.geometry())
except Exception:
pass
def _on_configure(e):
if e.widget is not win:
return
if _after_id[0]:
try:
self.after_cancel(_after_id[0])
except Exception:
pass
_after_id[0] = self.after(400, _save_geom)
win.bind("<Configure>", _on_configure)
def _on_close():
_save_geom()
if hasattr(self, "_aza_windows"):
self._aza_windows.discard(win)
win.destroy()
win.protocol("WM_DELETE_WINDOW", on_ordner_close)
add_resize_grip(win, ORDNER_MIN_W, ORDNER_MIN_H)
add_font_scale_control(win)
main_f = ttk.Frame(win, padding=12)
main_f.pack(fill="both", expand=True)
ttk.Label(main_f, text=f"Ablage: {base_path}").pack(anchor="w")
win.protocol("WM_DELETE_WINDOW", _on_close)
# ── Header ────────────────────────────────────────────────────────────
hdr = tk.Frame(win, bg=_HDR_BG)
hdr.pack(fill="x")
hdr_inner = tk.Frame(hdr, bg=_HDR_BG, padx=20, pady=14)
hdr_inner.pack(fill="x")
tk.Label(hdr_inner, text="AzA-Ordner", bg=_HDR_BG, fg=_HDR_FG,
font=(_FF, 13, "bold")).pack(anchor="w")
tk.Label(hdr_inner,
text="Gespeicherte Krankengeschichten, Briefe, Rezepte, Kostengutsprachen, Diktate und Transkripte",
bg=_HDR_BG, fg=_HDR_SUB, font=(_FF, 8),
wraplength=580, justify="left").pack(anchor="w", pady=(2, 0))
tk.Label(hdr_inner, text=base_path, bg=_HDR_BG, fg="#6090B8",
font=(_FF, 7)).pack(anchor="w", pady=(2, 0))
tk.Frame(win, bg=_CARD_BD, height=1).pack(fill="x")
# ── Auto-delete card ──────────────────────────────────────────────────
cb_card = tk.Frame(win, bg=_CARD_BG,
highlightbackground=_CARD_BD, highlightthickness=1,
padx=14, pady=8)
cb_card.pack(fill="x", padx=14, pady=(10, 4))
auto_delete_var = tk.BooleanVar(
value=bool(getattr(self, "_autotext_data", {}).get("ablage_auto_delete_old", True))
)
cb_auto_delete = ttk.Checkbutton(
main_f,
text="Automatisch löschen nach 2 Wochen (mit Nachfrage)",
variable=auto_delete_var,
)
cb_auto_delete.pack(anchor="w", pady=(6, 2))
def _persist_auto_delete_pref():
def _persist_auto_delete():
try:
if hasattr(self, "_autotext_data"):
self._autotext_data["ablage_auto_delete_old"] = bool(auto_delete_var.get())
@@ -98,164 +261,194 @@ class AzaOrdnerMixin:
except Exception:
pass
cb_auto_delete.configure(command=_persist_auto_delete_pref)
# ─── Tab-Leiste (blaues Design) ───
_tab_style_active = {"bg": "#E8F4FA", "fg": "#1a4d6d", "font": ("Segoe UI", 10, "bold")}
_tab_style_inactive = {"bg": "#A8D8E8", "fg": "#5A90B0", "font": ("Segoe UI", 10)}
tk.Checkbutton(
cb_card,
text="Lokale Eintraege nach 2 Wochen automatisch loeschen",
variable=auto_delete_var,
command=_persist_auto_delete,
bg=_CARD_BG, fg=_TEXT, activebackground=_CARD_BG,
activeforeground=_TEXT, selectcolor=_CARD_BG,
font=(_FF, 9), anchor="w",
).pack(anchor="w")
tk.Label(cb_card,
text="(Beim Oeffnen des Ordners wird eine Bestaetigung abgefragt)",
bg=_CARD_BG, fg=_TEXT_SUB, font=(_FF, 8)).pack(anchor="w")
tab_bar = tk.Frame(main_f, bg="#A8D8E8")
tab_bar.pack(fill="x", pady=(8, 0))
# ── Tab-Leiste ────────────────────────────────────────────────────────
tab_outer = tk.Frame(win, bg=_WIN_BG, padx=14)
tab_outer.pack(fill="x", pady=(6, 0))
tab_bar = tk.Frame(tab_outer, bg=_WIN_BG)
tab_bar.pack(fill="x")
_ordner_pages = {}
_ordner_tab_btns = {}
_active_ordner_tab = [None]
_pages: dict = {}
_tab_btns: dict = {}
_active: list = [None]
# Inhaltsbereich (Karte fuer Listbox)
content_outer = tk.Frame(win, bg=_WIN_BG, padx=14)
content_outer.pack(fill="both", expand=True, pady=(0, 14))
content_card = tk.Frame(content_outer, bg=_CARD_BG,
highlightbackground=_CARD_BD, highlightthickness=1)
content_card.pack(fill="both", expand=True)
for cat in ABLAGE_SUBFOLDERS:
page = tk.Frame(main_f, bg="#E8F4FA")
_ordner_pages[cat] = page
style = _tab_style_active if _active_ordner_tab[0] is None else _tab_style_inactive
if _active_ordner_tab[0] is None:
_active_ordner_tab[0] = cat
btn = tk.Label(tab_bar, text=cat, cursor="hand2", padx=14, pady=5, **style)
btn.pack(side="left")
_ordner_tab_btns[cat] = btn
page = tk.Frame(content_card, bg=_CARD_BG, padx=10, pady=8)
_pages[cat] = page
is_first = _active[0] is None
if is_first:
_active[0] = cat
btn = tk.Label(
tab_bar, text=cat, cursor="hand2",
bg=_TAB_ACT if is_first else _TAB_INACT,
fg=_TAB_FG_A if is_first else _TAB_FG_I,
font=(_FF, 9, "bold") if is_first else (_FF, 9),
padx=14, pady=5,
)
btn.pack(side="left", padx=(0, 2))
_tab_btns[cat] = btn
def _switch_ordner_tab(tab_name):
_active_ordner_tab[0] = tab_name
for key, btn in _ordner_tab_btns.items():
btn.configure(**(_tab_style_active if key == tab_name else _tab_style_inactive))
for key, page in _ordner_pages.items():
page.pack_forget()
_ordner_pages[tab_name].pack(fill="both", expand=True)
for cat in ABLAGE_SUBFOLDERS:
_ordner_tab_btns[cat].bind("<Button-1>", lambda e, c=cat: _switch_ordner_tab(c))
_ordner_pages[_active_ordner_tab[0]].pack(fill="both", expand=True)
def refresh_list(listbox, category):
listbox.delete(0, "end")
for f in list_ablage_files(category):
display_name = f[:-4] if (f and str(f).endswith(".txt")) else f
listbox.insert("end", display_name)
def save_current(category):
if category == "KG":
content = self.txt_output.get("1.0", "end").strip()
if not content:
messagebox.showinfo("Hinweis", "Keine Krankengeschichte zum Speichern.")
return
elif category == "Briefe":
content = self._last_brief_text
if not content:
messagebox.showinfo("Hinweis", "Zuerst einen Brief erstellen (Button Brief).")
return
elif category == "Rezepte":
content = self._last_rezept_text
if not content:
messagebox.showinfo("Hinweis", "Zuerst ein Rezept erstellen (Button Rezept).")
return
elif category == "Kostengutsprachen":
content = self._last_kogu_text
if not content:
messagebox.showinfo("Hinweis", "Zuerst eine Kostengutsprache erstellen (Button KOGU).")
return
elif category == "Diktat":
content = self.txt_transcript.get("1.0", "end").strip()
if not content:
messagebox.showinfo("Hinweis", "Kein Transkript zum Speichern vorhanden.")
return
elif category == "Transkript":
content = self.txt_transcript.get("1.0", "end").strip()
if not content:
messagebox.showinfo("Hinweis", "Kein Transkript zum Speichern vorhanden.")
return
else:
return
try:
path = save_to_ablage(category, content)
if path:
messagebox.showinfo("Gespeichert", f"Gespeichert unter:\n{path}")
for lb in listboxes:
refresh_list(lb["listbox"], lb["category"])
def _switch_tab(name: str):
_active[0] = name
for k, b in _tab_btns.items():
active = k == name
b.configure(
bg=_TAB_ACT if active else _TAB_INACT,
fg=_TAB_FG_A if active else _TAB_FG_I,
font=(_FF, 9, "bold") if active else (_FF, 9),
)
for k, p in _pages.items():
if k == name:
p.pack(fill="both", expand=True)
else:
messagebox.showwarning("Hinweis", "Nichts gespeichert (Inhalt war leer).")
except Exception as e:
messagebox.showerror("Fehler", str(e))
p.pack_forget()
def load_file_into_app(category, filename):
for cat in ABLAGE_SUBFOLDERS:
_tab_btns[cat].bind("<Button-1>", lambda e, c=cat: _switch_tab(c))
_tab_btns[cat].bind("<Enter>", lambda e, b=_tab_btns[cat], c=cat: (
b.configure(bg=_TAB_ACT) if _active[0] != c else None
))
_tab_btns[cat].bind("<Leave>", lambda e, b=_tab_btns[cat], c=cat: (
b.configure(bg=_TAB_INACT) if _active[0] != c else None
))
# Erste Tab-Seite sichtbar
_pages[_active[0]].pack(fill="both", expand=True)
# ── Pro-Tab: Listbox + Hilfstext ──────────────────────────────────────
listboxes: list = []
# Cache der zuletzt angezeigten, sortierten Dateinamen pro Kategorie.
# Wird beim Doppelklick gelesen, damit Listbox-Index und tatsaechlich
# geoeffnete Datei garantiert konsistent sind.
sorted_files_cache: dict = {}
def _refresh(lb: tk.Listbox, category: str):
lb.delete(0, "end")
files = _sort_ablage_entries(list_ablage_files(category))
sorted_files_cache[category] = files
for f in files:
disp = f[:-4] if f.endswith(".txt") else f
lb.insert("end", disp)
def _load_file(category: str, filename: str):
"""Laedt Datei in neuem Fenster. Ordner-Fenster bleibt offen."""
content = get_ablage_content(category, filename)
if not content:
messagebox.showinfo("Hinweis", "Datei ist leer oder nicht gefunden.")
messagebox.showinfo("Hinweis", "Datei ist leer oder nicht gefunden.",
parent=win)
return
# Immer in neuem Fenster öffnen; Ordner-Fenster bleibt offen
if category == "KG":
self._show_text_window("KG (geladen)", content, buttons="kg")
self.set_status("KG in neuem Fenster geöffnet.")
self.set_status("KG in neuem Fenster geoeffnet.")
elif category == "Briefe":
self._last_brief_text = content
self._show_text_window("Brief (geladen)", content, buttons="brief")
self.set_status("Brief in neuem Fenster geöffnet.")
self.set_status("Brief in neuem Fenster geoeffnet.")
elif category == "Rezepte":
self._last_rezept_text = content
self._show_text_window("Rezept (geladen)", content, buttons="rezept")
self.set_status("Rezept in neuem Fenster geöffnet.")
self.set_status("Rezept in neuem Fenster geoeffnet.")
elif category == "Kostengutsprachen":
self._last_kogu_text = content
self._show_text_window("KOGU (geladen)", content, buttons="kogu")
self.set_status("KOGU in neuem Fenster geöffnet.")
elif category == "Diktat":
self._show_text_window("Diktat (geladen)", content, buttons=None)
self.set_status("Diktat in neuem Fenster geöffnet.")
elif category == "Transkript":
self._show_text_window("Transkript (geladen)", content, buttons=None)
self.set_status("Transkript in neuem Fenster geöffnet.")
self.set_status("KOGU in neuem Fenster geoeffnet.")
elif category in ("Diktat", "Transkript"):
self._show_text_window(f"{category} (geladen)", content, buttons=None)
self.set_status(f"{category} in neuem Fenster geoeffnet.")
listboxes = []
for cat in ABLAGE_SUBFOLDERS:
page = _ordner_pages[cat]
frame = tk.Frame(page, bg="#E8F4FA", padx=8, pady=4)
frame.pack(fill="both", expand=True)
tk.Label(frame, text="Aktuelles als neue Datei speichern (Nummer + Datum/Uhrzeit):",
font=("Segoe UI", 9), bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w")
btn_row = tk.Frame(frame, bg="#E8F4FA")
btn_row.pack(fill="x", pady=(0, 4))
RoundedButton(
btn_row, "Aktuelles speichern", command=lambda c=cat: save_current(c),
width=140, height=26, canvas_bg="#E8F4FA",
).pack(side="left")
tk.Label(frame, text="Gespeicherte Dateien:",
font=("Segoe UI", 9), bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
lb = tk.Listbox(frame, height=10, font=("Segoe UI", 10))
lb.pack(fill="both", expand=True, pady=(2, 4))
refresh_list(lb, cat)
page = _pages[cat]
hint = tk.Label(page,
text="Doppelklick: Datei in neuem Fenster oeffnen",
bg=_CARD_BG, fg=_TEXT_SUB, font=(_FF, 8))
hint.pack(anchor="w", pady=(0, 4))
lb_frame = tk.Frame(page, bg=_CARD_BG)
lb_frame.pack(fill="both", expand=True)
scrollbar = tk.Scrollbar(lb_frame, orient="vertical",
bg=_WIN_BG, troughcolor=_WIN_BG)
scrollbar.pack(side="right", fill="y")
lb = tk.Listbox(
lb_frame, font=(_FF, 10),
bg=_CARD_BG, fg=_TEXT,
selectbackground=_LB_SEL, selectforeground=_HDR_FG,
activestyle="none",
highlightbackground=_CARD_BD, highlightthickness=1,
relief="flat", bd=0,
yscrollcommand=scrollbar.set,
)
lb.pack(side="left", fill="both", expand=True)
scrollbar.configure(command=lb.yview)
_refresh(lb, cat)
listboxes.append({"listbox": lb, "category": cat})
def on_select(evt, category=cat, listbox=lb):
sel = listbox.curselection()
if not sel:
return
idx = sel[0]
files = list_ablage_files(category)
if 0 <= idx < len(files):
load_file_into_app(category, files[idx])
lb.bind("<Double-Button-1>", on_select)
def load_selected(lbx=lb, c=cat):
def _on_dblclick(evt, c=cat, lbx=lb):
sel = lbx.curselection()
if not sel:
messagebox.showinfo("Hinweis", "Bitte eine Datei auswählen.")
return
files = list_ablage_files(c)
# Aus dem Cache lesen, damit Index 1:1 zur angezeigten,
# sortierten Reihenfolge passt. Cache leer? Sortierung
# rekonstruieren, damit Doppelklick auch ohne vorheriges
# _refresh sicher die richtige Datei oeffnet.
files = sorted_files_cache.get(c)
if not files:
files = _sort_ablage_entries(list_ablage_files(c))
sorted_files_cache[c] = files
if 0 <= sel[0] < len(files):
load_file_into_app(c, files[sel[0]])
_load_file(c, files[sel[0]])
lb.bind("<Double-Button-1>", _on_dblclick)
RoundedButton(
frame, "Ausgewählte Datei in App laden", command=load_selected,
width=220, height=26, canvas_bg="#E8F4FA",
).pack(fill="x", pady=(0, 4))
def _on_mousewheel(evt, lbx=lb):
lbx.yview_scroll(int(-1 * (evt.delta / 120)), "units")
lb.bind("<MouseWheel>", _on_mousewheel)
def _maybe_cleanup_old_entries():
# ── Internes Speichern (kein Button, aber Funktion erhalten) ──────────
def _save_current_internal(category: str):
"""Intern erreichbar, kein sichtbarer Button."""
if category == "KG":
content = self.txt_output.get("1.0", "end").strip()
elif category == "Briefe":
content = getattr(self, "_last_brief_text", "")
elif category == "Rezepte":
content = getattr(self, "_last_rezept_text", "")
elif category == "Kostengutsprachen":
content = getattr(self, "_last_kogu_text", "")
elif category in ("Diktat", "Transkript"):
content = self.txt_transcript.get("1.0", "end").strip()
else:
return
if not content:
return
try:
save_to_ablage(category, content)
for lb_info in listboxes:
_refresh(lb_info["listbox"], lb_info["category"])
except Exception:
pass
# ── Auto-Cleanup beim Oeffnen ─────────────────────────────────────────
def _maybe_cleanup():
if not bool(auto_delete_var.get()):
return
total_old = 0
@@ -267,69 +460,38 @@ class AzaOrdnerMixin:
if total_old <= 0:
return
if not messagebox.askyesno(
"Auto-Löschen",
"Sollen Ablage-Dateien älter als 2 Wochen jetzt gelöscht werden?"
"Automatisch loeschen",
f"{total_old} Eintraege aelter als 2 Wochen gefunden.\n"
"Jetzt loeschen?",
parent=win,
):
return
total_deleted = 0
deleted = 0
for cat in ABLAGE_SUBFOLDERS:
try:
total_deleted += int(delete_entries_older_than(cat, days=14))
deleted += int(delete_entries_older_than(cat, days=14))
except Exception:
pass
for lb in listboxes:
refresh_list(lb["listbox"], lb["category"])
if total_deleted > 0:
messagebox.showinfo("Auto-Löschen", f"{total_deleted} alte Einträge wurden gelöscht.")
else:
messagebox.showinfo("Auto-Löschen", "Keine Einträge älter als 2 Wochen gefunden.")
for lb_info in listboxes:
_refresh(lb_info["listbox"], lb_info["category"])
if deleted > 0:
messagebox.showinfo("Geloescht",
f"{deleted} alte Eintraege wurden geloescht.",
parent=win)
# Nachfrage beim Öffnen des Fensters (nur wenn Checkbox aktiv).
win.after(250, _maybe_cleanup_old_entries)
win.after(300, _maybe_cleanup)
btn_bottom = ttk.Frame(main_f)
btn_bottom.pack(fill="x", pady=(8, 0))
# ── Fenster sichtbar machen ───────────────────────────────────────────
try:
win.attributes("-alpha", 1.0)
except Exception:
pass
try:
win.lift()
win.focus_force()
win.after(600, lambda: win.attributes("-topmost", False))
win.attributes("-topmost", True)
except Exception:
pass
def do_export():
from tkinter import filedialog
import zipfile
dest = filedialog.asksaveasfilename(
title="Ablage exportieren (ZIP)",
defaultextension=".zip",
filetypes=[("ZIP", "*.zip"), ("Alle", "*.*")],
)
if not dest:
return
try:
with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(base_path):
for f in files:
if not f.endswith(".txt"):
continue
path = os.path.join(root, f)
arcname = os.path.relpath(path, base_path)
zf.write(path, arcname)
messagebox.showinfo("Export", f"Exportiert nach:\n{dest}")
except Exception as e:
messagebox.showerror("Export fehlgeschlagen", str(e))
def do_import():
from tkinter import filedialog
import zipfile
src = filedialog.askopenfilename(
title="ZIP importieren (Inhalt in Ablage entpacken)",
filetypes=[("ZIP", "*.zip"), ("Alle", "*.*")],
)
if not src:
return
try:
with zipfile.ZipFile(src, "r") as zf:
zf.extractall(base_path)
messagebox.showinfo("Import", "Import abgeschlossen.")
for lb in listboxes:
refresh_list(lb["listbox"], lb["category"])
except Exception as e:
messagebox.showerror("Import fehlgeschlagen", str(e))
RoundedButton(btn_bottom, "Export (ZIP)", command=do_export, width=120, height=26, canvas_bg="#B9ECFA").pack(side="left", padx=(0, 8))
RoundedButton(btn_bottom, "Import (ZIP)", command=do_import, width=120, height=26, canvas_bg="#B9ECFA").pack(side="left")
add_font_scale_control(win)