update
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user