2733 lines
127 KiB
Python
2733 lines
127 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
TodoMixin – _open_todo_window Methode als Mixin-Klasse.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import tkinter as tk
|
|||
|
|
from tkinter import ttk, messagebox
|
|||
|
|
from tkinter.scrolledtext import ScrolledText
|
|||
|
|
from datetime import datetime
|
|||
|
|
import threading
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
|
|||
|
|
from aza_persistence import (
|
|||
|
|
load_todos,
|
|||
|
|
save_todos,
|
|||
|
|
load_notes,
|
|||
|
|
save_notes,
|
|||
|
|
load_checklists,
|
|||
|
|
save_checklists,
|
|||
|
|
load_todo_geometry,
|
|||
|
|
save_todo_geometry,
|
|||
|
|
load_todo_inbox,
|
|||
|
|
save_todo_inbox,
|
|||
|
|
send_todo_to_inbox,
|
|||
|
|
cloud_pull_todos,
|
|||
|
|
cloud_pull_notes,
|
|||
|
|
cloud_get_status,
|
|||
|
|
extract_date_from_todo_text,
|
|||
|
|
load_user_profile,
|
|||
|
|
load_todo_settings,
|
|||
|
|
save_todo_settings,
|
|||
|
|
)
|
|||
|
|
from aza_ui_helpers import (
|
|||
|
|
add_tooltip,
|
|||
|
|
center_window,
|
|||
|
|
add_resize_grip,
|
|||
|
|
add_text_font_size_control,
|
|||
|
|
setup_window_geometry_saving,
|
|||
|
|
apply_initial_scaling_to_window,
|
|||
|
|
load_text_font_size,
|
|||
|
|
save_text_font_size,
|
|||
|
|
)
|
|||
|
|
from aza_audio import AudioRecorder
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TodoMixin:
|
|||
|
|
"""Mixin-Klasse für das To-Do-Fenster."""
|
|||
|
|
|
|||
|
|
def _open_todo_window(self):
|
|||
|
|
"""Öffnet ein Todoist-ähnliches To-do Fenster mit Diktat, Kalender und Überfällig-Markierung."""
|
|||
|
|
from datetime import datetime, date, timedelta
|
|||
|
|
import calendar as cal_mod
|
|||
|
|
|
|||
|
|
TODO_MIN_W, TODO_MIN_H = 500, 560
|
|||
|
|
win = tk.Toplevel(self)
|
|||
|
|
win.title("To-do")
|
|||
|
|
win.minsize(TODO_MIN_W, TODO_MIN_H)
|
|||
|
|
win.configure(bg="#E8F4FA")
|
|||
|
|
win.attributes("-topmost", True)
|
|||
|
|
self._register_window(win)
|
|||
|
|
|
|||
|
|
saved_geom = load_todo_geometry()
|
|||
|
|
if saved_geom:
|
|||
|
|
# Gespeicherte Grösse übernehmen, aber immer zentrieren
|
|||
|
|
try:
|
|||
|
|
size_part = saved_geom.split("+")[0]
|
|||
|
|
w_saved, h_saved = [int(x) for x in size_part.split("x")]
|
|||
|
|
except (ValueError, IndexError):
|
|||
|
|
w_saved, h_saved = TODO_MIN_W, TODO_MIN_H
|
|||
|
|
win.geometry(f"{w_saved}x{h_saved}")
|
|||
|
|
center_window(win, w_saved, h_saved)
|
|||
|
|
else:
|
|||
|
|
win.geometry(f"{TODO_MIN_W}x{TODO_MIN_H}")
|
|||
|
|
center_window(win, TODO_MIN_W, TODO_MIN_H)
|
|||
|
|
|
|||
|
|
_geom_after = [None]
|
|||
|
|
|
|||
|
|
def _save_geom(event=None):
|
|||
|
|
if _geom_after[0]:
|
|||
|
|
win.after_cancel(_geom_after[0])
|
|||
|
|
_geom_after[0] = win.after(400, lambda: save_todo_geometry(win.geometry()))
|
|||
|
|
|
|||
|
|
win.bind("<Configure>", _save_geom)
|
|||
|
|
|
|||
|
|
# Aufnahme-State
|
|||
|
|
todo_recorder = [None]
|
|||
|
|
is_recording = [False]
|
|||
|
|
|
|||
|
|
def on_close():
|
|||
|
|
if is_recording[0] and todo_recorder[0]:
|
|||
|
|
try:
|
|||
|
|
is_recording[0] = False
|
|||
|
|
wav_path = todo_recorder[0].stop_and_save_wav()
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
todo_recorder[0] = None
|
|||
|
|
if _notes_is_recording[0] and _notes_recorder[0]:
|
|||
|
|
try:
|
|||
|
|
_notes_is_recording[0] = False
|
|||
|
|
wav_path = _notes_recorder[0].stop_and_save_wav()
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
_notes_recorder[0] = None
|
|||
|
|
try:
|
|||
|
|
save_todo_geometry(win.geometry())
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
save_todo_settings({
|
|||
|
|
"active_tab": _active_tab[0],
|
|||
|
|
"active_category": _active_category[0],
|
|||
|
|
"custom_categories": _custom_categories,
|
|||
|
|
})
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if hasattr(self, "_aza_windows"):
|
|||
|
|
self._aza_windows.discard(win)
|
|||
|
|
win.destroy()
|
|||
|
|
|
|||
|
|
win.protocol("WM_DELETE_WINDOW", on_close)
|
|||
|
|
|
|||
|
|
todos = load_todos()
|
|||
|
|
todo_widgets = []
|
|||
|
|
|
|||
|
|
# Inbox prüfen: empfangene Aufgaben importieren
|
|||
|
|
inbox = load_todo_inbox()
|
|||
|
|
my_name = self._user_profile.get("name", "")
|
|||
|
|
imported_count = 0
|
|||
|
|
remaining_inbox = []
|
|||
|
|
for inbox_item in inbox:
|
|||
|
|
if inbox_item.get("recipient", "") == my_name or not inbox_item.get("recipient"):
|
|||
|
|
new_todo = {
|
|||
|
|
"id": inbox_item.get("id", int(datetime.now().timestamp() * 1000)),
|
|||
|
|
"text": inbox_item.get("text", ""),
|
|||
|
|
"done": False,
|
|||
|
|
"date": inbox_item.get("date"),
|
|||
|
|
"priority": inbox_item.get("priority", 0),
|
|||
|
|
"notes": inbox_item.get("notes", ""),
|
|||
|
|
"created": inbox_item.get("sent_at", datetime.now().isoformat()),
|
|||
|
|
"sender": inbox_item.get("sender", ""),
|
|||
|
|
}
|
|||
|
|
todos.append(new_todo)
|
|||
|
|
imported_count += 1
|
|||
|
|
else:
|
|||
|
|
remaining_inbox.append(inbox_item)
|
|||
|
|
if imported_count > 0:
|
|||
|
|
save_todo_inbox(remaining_inbox)
|
|||
|
|
save_todos(todos)
|
|||
|
|
|
|||
|
|
# ─── Header ───
|
|||
|
|
header = tk.Frame(win, bg="#B9ECFA")
|
|||
|
|
header.pack(fill="x")
|
|||
|
|
tk.Label(header, text="📋 To-do Liste", font=("Segoe UI", 14, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#1a4d6d").pack(side="left", padx=12, pady=8)
|
|||
|
|
|
|||
|
|
_todo_minimized = [False]
|
|||
|
|
_todo_geom_before = [None]
|
|||
|
|
_todo_restoring = [False]
|
|||
|
|
|
|||
|
|
def _restore_todo_content():
|
|||
|
|
"""Inhalt wiederherstellen (aus minimiertem Zustand)."""
|
|||
|
|
if not _todo_minimized[0]:
|
|||
|
|
return
|
|||
|
|
_todo_restoring[0] = True
|
|||
|
|
counter_bar.pack(fill="x", after=header)
|
|||
|
|
tab_bar.pack(fill="x", after=counter_bar)
|
|||
|
|
_all_pages[_active_tab[0]].pack(fill="both", expand=True, after=tab_bar)
|
|||
|
|
btn_minimize_todo.configure(text="—")
|
|||
|
|
_todo_minimized[0] = False
|
|||
|
|
win.minsize(500, 560)
|
|||
|
|
win.after(200, lambda: _todo_restoring.__setitem__(0, False))
|
|||
|
|
|
|||
|
|
def _toggle_minimize_todo():
|
|||
|
|
if _todo_minimized[0]:
|
|||
|
|
_restore_todo_content()
|
|||
|
|
if _todo_geom_before[0]:
|
|||
|
|
try:
|
|||
|
|
win.geometry(_todo_geom_before[0])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
else:
|
|||
|
|
_todo_geom_before[0] = win.geometry()
|
|||
|
|
for key, page in _all_pages.items():
|
|||
|
|
page.pack_forget()
|
|||
|
|
tab_bar.pack_forget()
|
|||
|
|
counter_bar.pack_forget()
|
|||
|
|
btn_minimize_todo.configure(text="□")
|
|||
|
|
_todo_minimized[0] = True
|
|||
|
|
win.minsize(220, 50)
|
|||
|
|
w_cur = win.winfo_width()
|
|||
|
|
win.geometry(f"{w_cur}x50")
|
|||
|
|
|
|||
|
|
def _on_todo_configure(e):
|
|||
|
|
if e.widget is not win:
|
|||
|
|
return
|
|||
|
|
if _todo_minimized[0] and not _todo_restoring[0] and e.height > 80:
|
|||
|
|
_restore_todo_content()
|
|||
|
|
|
|||
|
|
win.bind("<Configure>", _on_todo_configure, add="+")
|
|||
|
|
|
|||
|
|
btn_minimize_todo = tk.Label(header, text="—", font=("Segoe UI", 14, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#5A90B0", cursor="hand2", padx=6)
|
|||
|
|
btn_minimize_todo.pack(side="right", padx=(0, 8))
|
|||
|
|
btn_minimize_todo.bind("<Button-1>", lambda e: _toggle_minimize_todo())
|
|||
|
|
btn_minimize_todo.bind("<Enter>", lambda e: btn_minimize_todo.configure(fg="#1a4d6d"))
|
|||
|
|
btn_minimize_todo.bind("<Leave>", lambda e: btn_minimize_todo.configure(fg="#5A90B0"))
|
|||
|
|
|
|||
|
|
win._aza_minimize = _toggle_minimize_todo
|
|||
|
|
win._aza_is_minimized = lambda: _todo_minimized[0]
|
|||
|
|
if hasattr(self, "_aza_windows"):
|
|||
|
|
self._aza_windows.add(win)
|
|||
|
|
|
|||
|
|
todo_font_size_key = "todo_list"
|
|||
|
|
todo_font_size = [load_text_font_size(todo_font_size_key, 11)]
|
|||
|
|
|
|||
|
|
_fs_fg = "#8AAFC0"
|
|||
|
|
_fs_fg_hover = "#1a4d6d"
|
|||
|
|
|
|||
|
|
font_ctrl = tk.Frame(header, bg="#B9ECFA", highlightthickness=0, bd=0)
|
|||
|
|
font_ctrl.pack(side="right", padx=(0, 4), pady=6)
|
|||
|
|
tk.Label(font_ctrl, text="Aa", font=("Segoe UI", 8),
|
|||
|
|
bg="#B9ECFA", fg=_fs_fg).pack(side="left", padx=(0, 1))
|
|||
|
|
|
|||
|
|
_fs_size_lbl = tk.Label(font_ctrl, text=str(todo_font_size[0]),
|
|||
|
|
font=("Segoe UI", 8), bg="#B9ECFA", fg=_fs_fg,
|
|||
|
|
width=2, anchor="center")
|
|||
|
|
_fs_size_lbl.pack(side="left")
|
|||
|
|
|
|||
|
|
def _update_todo_font(new_size):
|
|||
|
|
new_size = max(7, min(16, new_size))
|
|||
|
|
todo_font_size[0] = new_size
|
|||
|
|
_fs_size_lbl.configure(text=str(new_size))
|
|||
|
|
save_text_font_size(todo_font_size_key, new_size)
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
_fs_btn_up = tk.Label(font_ctrl, text="\u25B2", font=("Segoe UI", 7),
|
|||
|
|
bg="#B9ECFA", fg=_fs_fg, cursor="hand2",
|
|||
|
|
bd=0, highlightthickness=0, padx=0, pady=0)
|
|||
|
|
_fs_btn_up.pack(side="left", padx=(2, 0))
|
|||
|
|
|
|||
|
|
_fs_btn_down = tk.Label(font_ctrl, text="\u25BC", font=("Segoe UI", 7),
|
|||
|
|
bg="#B9ECFA", fg=_fs_fg, cursor="hand2",
|
|||
|
|
bd=0, highlightthickness=0, padx=0, pady=0)
|
|||
|
|
_fs_btn_down.pack(side="left", padx=(0, 0))
|
|||
|
|
|
|||
|
|
_fs_btn_up.bind("<Button-1>", lambda e: _update_todo_font(todo_font_size[0] + 1))
|
|||
|
|
_fs_btn_down.bind("<Button-1>", lambda e: _update_todo_font(todo_font_size[0] - 1))
|
|||
|
|
for _fsw in (_fs_btn_up, _fs_btn_down):
|
|||
|
|
_fsw.bind("<Enter>", lambda e, ww=_fsw: ww.configure(fg=_fs_fg_hover))
|
|||
|
|
_fsw.bind("<Leave>", lambda e, ww=_fsw: ww.configure(fg=_fs_fg))
|
|||
|
|
|
|||
|
|
# Info-Button (ⓘ) für Diktat-Datumserkennung
|
|||
|
|
_info_popup = [None]
|
|||
|
|
|
|||
|
|
def _show_diktat_info(event=None):
|
|||
|
|
if _info_popup[0] and _info_popup[0].winfo_exists():
|
|||
|
|
_info_popup[0].lift()
|
|||
|
|
return
|
|||
|
|
tip = tk.Toplevel(win)
|
|||
|
|
tip.title("🎤 Diktat-Datumserkennung")
|
|||
|
|
tip.attributes("-topmost", True)
|
|||
|
|
tip.configure(bg="#E8F4FA")
|
|||
|
|
tip.resizable(False, False)
|
|||
|
|
self._register_window(tip)
|
|||
|
|
|
|||
|
|
def _close_info():
|
|||
|
|
_info_popup[0] = None
|
|||
|
|
tip.destroy()
|
|||
|
|
|
|||
|
|
tip.protocol("WM_DELETE_WINDOW", _close_info)
|
|||
|
|
|
|||
|
|
tip_header = tk.Frame(tip, bg="#B9ECFA")
|
|||
|
|
tip_header.pack(fill="x")
|
|||
|
|
tk.Label(tip_header, text="🎤 Datumserkennung beim Diktieren",
|
|||
|
|
font=("Segoe UI", 10, "bold"), bg="#B9ECFA", fg="#1a4d6d",
|
|||
|
|
anchor="w").pack(side="left", padx=10, pady=6)
|
|||
|
|
close_lbl = tk.Label(tip_header, text=" ✕ ", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#999", cursor="hand2")
|
|||
|
|
close_lbl.pack(side="right", padx=6, pady=6)
|
|||
|
|
close_lbl.bind("<Button-1>", lambda e: _close_info())
|
|||
|
|
close_lbl.bind("<Enter>", lambda e: close_lbl.configure(fg="#E87070"))
|
|||
|
|
close_lbl.bind("<Leave>", lambda e: close_lbl.configure(fg="#999"))
|
|||
|
|
|
|||
|
|
inner = tk.Frame(tip, bg="white", padx=12, pady=8)
|
|||
|
|
inner.pack(fill="both", expand=True, padx=1, pady=(0, 1))
|
|||
|
|
|
|||
|
|
tk.Label(inner, text="Datum im Diktat wird automatisch erkannt und gesetzt:",
|
|||
|
|
font=("Segoe UI", 8), bg="white", fg="#666",
|
|||
|
|
anchor="w").pack(fill="x", pady=(0, 6))
|
|||
|
|
|
|||
|
|
table = tk.Frame(inner, bg="white")
|
|||
|
|
table.pack(fill="x")
|
|||
|
|
|
|||
|
|
headers = ["Beispiel-Diktat", "Erkanntes Datum"]
|
|||
|
|
for c, h in enumerate(headers):
|
|||
|
|
tk.Label(table, text=h, font=("Segoe UI", 8, "bold"), bg="#E8F4FA",
|
|||
|
|
fg="#1a4d6d", padx=6, pady=2, anchor="w").grid(row=0, column=c, sticky="ew", padx=1, pady=1)
|
|||
|
|
|
|||
|
|
examples = [
|
|||
|
|
("«…bis morgen»", "morgen"),
|
|||
|
|
("«…bis nächsten Mittwoch»", "nächster Mi"),
|
|||
|
|
("«…bis 20. März»", "20.03."),
|
|||
|
|
("«…in 3 Tagen»", "+3 Tage"),
|
|||
|
|
("«…bis Ende Woche»", "Freitag"),
|
|||
|
|
("«…bis Ende Monat»", "Monatsende"),
|
|||
|
|
("«…am 15.4.»", "15.04."),
|
|||
|
|
("«…übermorgen»", "übermorgen"),
|
|||
|
|
("«…in einer Woche»", "+1 Woche"),
|
|||
|
|
("«…in zwei Monaten»", "+2 Monate"),
|
|||
|
|
]
|
|||
|
|
for r, (ex, dt_txt) in enumerate(examples, start=1):
|
|||
|
|
bg_r = "#F5FCFF" if r % 2 == 0 else "white"
|
|||
|
|
tk.Label(table, text=ex, font=("Segoe UI", 8), bg=bg_r,
|
|||
|
|
fg="#1a4d6d", padx=6, pady=1, anchor="w").grid(row=r, column=0, sticky="ew", padx=1)
|
|||
|
|
tk.Label(table, text=dt_txt, font=("Segoe UI", 8, "bold"), bg=bg_r,
|
|||
|
|
fg="#5B8DB3", padx=6, pady=1, anchor="w").grid(row=r, column=1, sticky="ew", padx=1)
|
|||
|
|
|
|||
|
|
tip.update_idletasks()
|
|||
|
|
x = win.winfo_rootx() + info_btn.winfo_x()
|
|||
|
|
y = win.winfo_rooty() + info_btn.winfo_y() + info_btn.winfo_height() + 4
|
|||
|
|
tip.geometry(f"+{max(0, x)}+{y}")
|
|||
|
|
|
|||
|
|
_info_popup[0] = tip
|
|||
|
|
|
|||
|
|
info_btn = tk.Label(header, text=" ⓘ ", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#7EC8E3", fg="white", cursor="hand2",
|
|||
|
|
relief="flat", bd=0, padx=4, pady=2)
|
|||
|
|
info_btn.pack(side="right", padx=(0, 8), pady=8)
|
|||
|
|
info_btn.bind("<Button-1>", _show_diktat_info)
|
|||
|
|
info_btn.bind("<Enter>", lambda e: info_btn.configure(bg="#5B8DB3"))
|
|||
|
|
info_btn.bind("<Leave>", lambda e: info_btn.configure(bg="#7EC8E3"))
|
|||
|
|
|
|||
|
|
# 📱 iPhone-App-Button (startet Webserver + zeigt QR-Code)
|
|||
|
|
_todo_server_thread = [None]
|
|||
|
|
_todo_server_running = [False]
|
|||
|
|
|
|||
|
|
def _start_todo_server():
|
|||
|
|
if _todo_server_running[0]:
|
|||
|
|
_show_qr_window()
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
from todo_server import _get_local_ip, PORT, TodoHandler, TodoServer, _add_firewall_rule, _supabase_push
|
|||
|
|
import urllib.request
|
|||
|
|
local_ip = _get_local_ip()
|
|||
|
|
url = f"http://{local_ip}:{PORT}"
|
|||
|
|
|
|||
|
|
# Todos nach Supabase pushen
|
|||
|
|
try:
|
|||
|
|
_supabase_push(todos)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Firewall-Regel
|
|||
|
|
_add_firewall_rule()
|
|||
|
|
|
|||
|
|
_server_ref = [None]
|
|||
|
|
_server_error = [None]
|
|||
|
|
_server_ready = threading.Event()
|
|||
|
|
|
|||
|
|
def run_server():
|
|||
|
|
try:
|
|||
|
|
server = TodoServer(("0.0.0.0", PORT), TodoHandler)
|
|||
|
|
_server_ref[0] = server
|
|||
|
|
_server_ready.set()
|
|||
|
|
server.serve_forever()
|
|||
|
|
except Exception as e:
|
|||
|
|
_server_error[0] = str(e)
|
|||
|
|
_server_ready.set()
|
|||
|
|
|
|||
|
|
t = threading.Thread(target=run_server, daemon=True)
|
|||
|
|
t.start()
|
|||
|
|
_todo_server_thread[0] = t
|
|||
|
|
_server_ready.wait(timeout=3)
|
|||
|
|
|
|||
|
|
if _server_error[0]:
|
|||
|
|
messagebox.showerror(
|
|||
|
|
"Server-Fehler",
|
|||
|
|
f"Server konnte nicht starten:\n{_server_error[0]}\n\n"
|
|||
|
|
f"Evtl. Port {PORT} belegt – Programm neu starten.",
|
|||
|
|
parent=win)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
test_ok = False
|
|||
|
|
try:
|
|||
|
|
resp = urllib.request.urlopen(f"http://localhost:{PORT}/", timeout=3)
|
|||
|
|
if resp.status == 200:
|
|||
|
|
test_ok = True
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
if not test_ok:
|
|||
|
|
messagebox.showerror(
|
|||
|
|
"Server-Fehler",
|
|||
|
|
"Server gestartet, antwortet aber nicht.\n"
|
|||
|
|
"Bitte Programm neu starten und erneut versuchen.",
|
|||
|
|
parent=win)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
_todo_server_running[0] = True
|
|||
|
|
phone_btn.configure(bg="#5BDB7B")
|
|||
|
|
|
|||
|
|
win.after(300, lambda: _show_qr_window(url))
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror(
|
|||
|
|
"Fehler beim Starten",
|
|||
|
|
f"Der To-Do-Server konnte nicht gestartet werden.\n\n"
|
|||
|
|
f"Fehler: {e}",
|
|||
|
|
parent=win)
|
|||
|
|
|
|||
|
|
def _show_qr_window(url=None):
|
|||
|
|
if url is None:
|
|||
|
|
try:
|
|||
|
|
from todo_server import _get_local_ip, PORT
|
|||
|
|
url = f"http://{_get_local_ip()}:{PORT}"
|
|||
|
|
except Exception:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
qr_win = tk.Toplevel(win)
|
|||
|
|
qr_win.title("📱 iPhone To-Do App")
|
|||
|
|
qr_win.attributes("-topmost", True)
|
|||
|
|
qr_win.configure(bg="#E8F4FA")
|
|||
|
|
qr_win.resizable(False, False)
|
|||
|
|
qr_win.geometry("360x660")
|
|||
|
|
center_window(qr_win, 360, 660)
|
|||
|
|
self._register_window(qr_win)
|
|||
|
|
|
|||
|
|
tk.Label(qr_win, text="📱 AzA To-Do – iPhone App", font=("Segoe UI", 14, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=10)
|
|||
|
|
|
|||
|
|
# Cloud-Status
|
|||
|
|
cloud_frame = tk.Frame(qr_win, bg="#EDF7ED", padx=10, pady=4)
|
|||
|
|
cloud_frame.pack(fill="x", padx=16, pady=(8, 0))
|
|||
|
|
tk.Label(cloud_frame,
|
|||
|
|
text="☁ Supabase Cloud-Sync aktiv – App funktioniert weltweit!",
|
|||
|
|
font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#EDF7ED", fg="#2A7A2A").pack(anchor="w")
|
|||
|
|
tk.Label(cloud_frame,
|
|||
|
|
text="Die App synchronisiert automatisch, egal ob\n"
|
|||
|
|
"WLAN, 5G, unterwegs – von überall auf der Welt.\n"
|
|||
|
|
"Kein laufender PC nötig!",
|
|||
|
|
font=("Segoe UI", 8), bg="#EDF7ED", fg="#2A7A2A",
|
|||
|
|
justify="left").pack(anchor="w")
|
|||
|
|
|
|||
|
|
tk.Label(qr_win, text="Schritt 1: QR-Code scannen (einmalig, im WLAN):",
|
|||
|
|
font=("Segoe UI", 10, "bold"), bg="#E8F4FA", fg="#1a4d6d"
|
|||
|
|
).pack(fill="x", padx=16, pady=(10, 4))
|
|||
|
|
|
|||
|
|
qr_frame = tk.Frame(qr_win, bg="white", padx=10, pady=10)
|
|||
|
|
qr_frame.pack(pady=4)
|
|||
|
|
|
|||
|
|
qr_generated = False
|
|||
|
|
try:
|
|||
|
|
import qrcode
|
|||
|
|
from PIL import ImageTk
|
|||
|
|
img = qrcode.make(url, box_size=6, border=2)
|
|||
|
|
img_tk = ImageTk.PhotoImage(img)
|
|||
|
|
qr_label = tk.Label(qr_frame, image=img_tk, bg="white")
|
|||
|
|
qr_label.image = img_tk
|
|||
|
|
qr_label.pack()
|
|||
|
|
qr_generated = True
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
if not qr_generated:
|
|||
|
|
tk.Label(qr_frame,
|
|||
|
|
text="QR-Code nicht verfügbar.\n\n"
|
|||
|
|
"Bitte installieren:\n"
|
|||
|
|
"py -3.11 -m pip install qrcode[pil]\n\n"
|
|||
|
|
"Alternativ diese URL im\n"
|
|||
|
|
"iPhone-Browser öffnen:",
|
|||
|
|
font=("Segoe UI", 9), bg="white", fg="#C03030",
|
|||
|
|
justify="center").pack(padx=20, pady=10)
|
|||
|
|
|
|||
|
|
url_label = tk.Label(qr_win, text=url, font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#5B8DB3", cursor="hand2")
|
|||
|
|
url_label.pack(pady=2)
|
|||
|
|
|
|||
|
|
tk.Label(qr_win, text="Schritt 2: Auf dem iPhone → Teilen → Zum\n"
|
|||
|
|
"Home-Bildschirm hinzufügen (AzA-Logo erscheint!)",
|
|||
|
|
font=("Segoe UI", 9, "bold"), bg="#E8F4FA", fg="#1a4d6d",
|
|||
|
|
justify="center").pack(padx=16, pady=(4, 2))
|
|||
|
|
|
|||
|
|
tk.Label(qr_win, text="Danach funktioniert die App eigenständig –\n"
|
|||
|
|
"auch ohne WLAN, von überall auf der Welt!",
|
|||
|
|
font=("Segoe UI", 8), bg="#E8F4FA", fg="#4a8aaa",
|
|||
|
|
justify="center").pack(padx=16, pady=(0, 4))
|
|||
|
|
|
|||
|
|
# Server-Status
|
|||
|
|
status_bg = "#EDF7ED" if _todo_server_running[0] else "#FDECEC"
|
|||
|
|
status_fg = "#2A7A2A" if _todo_server_running[0] else "#C03030"
|
|||
|
|
status_txt = "● Server läuft (für Ersteinrichtung)" if _todo_server_running[0] \
|
|||
|
|
else "● Server nicht gestartet"
|
|||
|
|
tk.Label(qr_win, text=status_txt, font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg=status_bg, fg=status_fg).pack(fill="x", padx=16, ipady=3, pady=(2, 0))
|
|||
|
|
|
|||
|
|
# Firewall-Hilfe
|
|||
|
|
help_frame = tk.Frame(qr_win, bg="#FFF8E0", padx=10, pady=3)
|
|||
|
|
help_frame.pack(fill="x", padx=16, pady=(4, 0))
|
|||
|
|
tk.Label(help_frame,
|
|||
|
|
text="Falls iPhone nicht verbindet: Firewall öffnen\n"
|
|||
|
|
"→ Python bei Privat + Öffentlich anhaken",
|
|||
|
|
font=("Segoe UI", 7), bg="#FFF8E0", fg="#806000",
|
|||
|
|
justify="left").pack(anchor="w")
|
|||
|
|
|
|||
|
|
def _open_firewall_settings():
|
|||
|
|
try:
|
|||
|
|
os.startfile("firewall.cpl")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _open_in_browser():
|
|||
|
|
import webbrowser
|
|||
|
|
webbrowser.open(url)
|
|||
|
|
|
|||
|
|
def _close_qr():
|
|||
|
|
qr_win.destroy()
|
|||
|
|
|
|||
|
|
btn_row = tk.Frame(qr_win, bg="#E8F4FA")
|
|||
|
|
btn_row.pack(pady=6)
|
|||
|
|
tk.Button(btn_row, text="🔥 Firewall", font=("Segoe UI", 8),
|
|||
|
|
bg="#F0D060", fg="#604000", activebackground="#E0C050",
|
|||
|
|
relief="flat", padx=8, pady=2, cursor="hand2",
|
|||
|
|
command=_open_firewall_settings).pack(side="left", padx=3)
|
|||
|
|
tk.Button(btn_row, text="🌐 Browser testen", font=("Segoe UI", 8),
|
|||
|
|
bg="#7EC8E3", fg="#1a4d6d", activebackground="#6CB8D3",
|
|||
|
|
relief="flat", padx=8, pady=2, cursor="hand2",
|
|||
|
|
command=_open_in_browser).pack(side="left", padx=3)
|
|||
|
|
tk.Button(btn_row, text="Schliessen", font=("Segoe UI", 8),
|
|||
|
|
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
|
|||
|
|
relief="flat", padx=8, pady=2, cursor="hand2",
|
|||
|
|
command=_close_qr).pack(side="left", padx=3)
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("Fehler", f"QR-Fenster konnte nicht geöffnet werden:\n{e}", parent=win)
|
|||
|
|
|
|||
|
|
phone_btn = tk.Label(header, text=" 📱 ", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#7EC8E3", fg="white", cursor="hand2",
|
|||
|
|
relief="flat", bd=0, padx=4, pady=2)
|
|||
|
|
phone_btn.pack(side="right", padx=(0, 4), pady=8)
|
|||
|
|
phone_btn.bind("<Button-1>", lambda e: _start_todo_server())
|
|||
|
|
phone_btn.bind("<Enter>", lambda e: phone_btn.configure(
|
|||
|
|
bg="#5BDB7B" if _todo_server_running[0] else "#5B8DB3"))
|
|||
|
|
phone_btn.bind("<Leave>", lambda e: phone_btn.configure(
|
|||
|
|
bg="#5BDB7B" if _todo_server_running[0] else "#7EC8E3"))
|
|||
|
|
|
|||
|
|
counter_bar = tk.Frame(win, bg="#B9ECFA")
|
|||
|
|
counter_bar.pack(fill="x")
|
|||
|
|
counter_label = tk.Label(counter_bar, text="", font=("Segoe UI", 9),
|
|||
|
|
bg="#B9ECFA", fg="#4a8aaa")
|
|||
|
|
counter_label.pack(side="left", padx=12, pady=(0, 4))
|
|||
|
|
|
|||
|
|
# ─── Gespeicherte Todo-Einstellungen laden ───
|
|||
|
|
_todo_settings = load_todo_settings()
|
|||
|
|
_saved_tab = _todo_settings.get("active_tab", "todo")
|
|||
|
|
_saved_category = _todo_settings.get("active_category", "Allgemein")
|
|||
|
|
_custom_categories = _todo_settings.get("custom_categories", [])
|
|||
|
|
_DEFAULT_CATEGORIES = ["Allgemein", "MPA", "Ärzte"]
|
|||
|
|
|
|||
|
|
def _build_category_list():
|
|||
|
|
return _DEFAULT_CATEGORIES + _custom_categories + ["Alle"]
|
|||
|
|
|
|||
|
|
# ─── Tab-Leiste (Aufgaben / Notizen / Checkliste) ───
|
|||
|
|
tab_bar = tk.Frame(win, bg="#A8D8E8")
|
|||
|
|
tab_bar.pack(fill="x")
|
|||
|
|
|
|||
|
|
_active_tab = [_saved_tab if _saved_tab in ("todo", "notes", "checklist") else "todo"]
|
|||
|
|
|
|||
|
|
todo_page = tk.Frame(win, bg="#E8F4FA")
|
|||
|
|
notes_page = tk.Frame(win, bg="#E8F4FA")
|
|||
|
|
checklist_page = tk.Frame(win, bg="#E8F4FA")
|
|||
|
|
|
|||
|
|
_tab_btn_style_active = {"bg": "#E8F4FA", "fg": "#1a4d6d", "font": ("Segoe UI", 10, "bold")}
|
|||
|
|
_tab_btn_style_inactive = {"bg": "#A8D8E8", "fg": "#5A90B0", "font": ("Segoe UI", 10)}
|
|||
|
|
|
|||
|
|
tab_btn_todo = tk.Label(tab_bar, text="📋 Aufgaben", cursor="hand2",
|
|||
|
|
padx=16, pady=6, **(_tab_btn_style_active if _active_tab[0] == "todo" else _tab_btn_style_inactive))
|
|||
|
|
tab_btn_todo.pack(side="left")
|
|||
|
|
|
|||
|
|
tab_btn_notes = tk.Label(tab_bar, text="📝 Notizen", cursor="hand2",
|
|||
|
|
padx=16, pady=6, **(_tab_btn_style_active if _active_tab[0] == "notes" else _tab_btn_style_inactive))
|
|||
|
|
tab_btn_notes.pack(side="left")
|
|||
|
|
|
|||
|
|
tab_btn_checklist = tk.Label(tab_bar, text="✅ Checkliste", cursor="hand2",
|
|||
|
|
padx=16, pady=6, **(_tab_btn_style_active if _active_tab[0] == "checklist" else _tab_btn_style_inactive))
|
|||
|
|
tab_btn_checklist.pack(side="left")
|
|||
|
|
|
|||
|
|
_all_tab_btns = {"todo": tab_btn_todo, "notes": tab_btn_notes, "checklist": tab_btn_checklist}
|
|||
|
|
_all_pages = {"todo": todo_page, "notes": notes_page, "checklist": checklist_page}
|
|||
|
|
|
|||
|
|
def _switch_tab(tab_name):
|
|||
|
|
_active_tab[0] = tab_name
|
|||
|
|
for key, btn in _all_tab_btns.items():
|
|||
|
|
btn.configure(**((_tab_btn_style_active if key == tab_name else _tab_btn_style_inactive)))
|
|||
|
|
for key, page in _all_pages.items():
|
|||
|
|
page.pack_forget()
|
|||
|
|
_all_pages[tab_name].pack(fill="both", expand=True)
|
|||
|
|
if tab_name == "notes":
|
|||
|
|
_rebuild_notes_list()
|
|||
|
|
elif tab_name == "checklist":
|
|||
|
|
_rebuild_checklist()
|
|||
|
|
|
|||
|
|
tab_btn_todo.bind("<Button-1>", lambda e: _switch_tab("todo"))
|
|||
|
|
tab_btn_notes.bind("<Button-1>", lambda e: _switch_tab("notes"))
|
|||
|
|
tab_btn_checklist.bind("<Button-1>", lambda e: _switch_tab("checklist"))
|
|||
|
|
|
|||
|
|
_all_pages[_active_tab[0]].pack(fill="both", expand=True)
|
|||
|
|
_need_initial_rebuild = [_active_tab[0]]
|
|||
|
|
|
|||
|
|
# ─── Aufgabenbereiche (Kategorie-Filter) ───
|
|||
|
|
_TODO_CATEGORIES = _build_category_list()
|
|||
|
|
_initial_cat = _saved_category if _saved_category in _TODO_CATEGORIES else "Allgemein"
|
|||
|
|
_active_category = [_initial_cat]
|
|||
|
|
|
|||
|
|
cat_bar = tk.Frame(todo_page, bg="#D4EEF7", pady=2, padx=8)
|
|||
|
|
cat_bar.pack(fill="x")
|
|||
|
|
tk.Label(cat_bar, text="Bereich:", font=("Segoe UI", 8),
|
|||
|
|
bg="#D4EEF7", fg="#5A90B0").pack(side="left", padx=(0, 4))
|
|||
|
|
|
|||
|
|
_cat_btns_frame = tk.Frame(cat_bar, bg="#D4EEF7")
|
|||
|
|
_cat_btns_frame.pack(side="left", fill="x", expand=True)
|
|||
|
|
|
|||
|
|
_cat_btns = {}
|
|||
|
|
_cat_active_style = {"bg": "#5B8DB3", "fg": "white"}
|
|||
|
|
_cat_inactive_style = {"bg": "#C8DDE6", "fg": "#1a4d6d"}
|
|||
|
|
|
|||
|
|
def _set_category_filter(cat):
|
|||
|
|
_active_category[0] = cat
|
|||
|
|
for c, b in _cat_btns.items():
|
|||
|
|
if c == cat:
|
|||
|
|
b.configure(**_cat_active_style)
|
|||
|
|
else:
|
|||
|
|
b.configure(**_cat_inactive_style)
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
def _rebuild_cat_buttons():
|
|||
|
|
for w in _cat_btns_frame.winfo_children():
|
|||
|
|
w.destroy()
|
|||
|
|
_cat_btns.clear()
|
|||
|
|
_TODO_CATEGORIES.clear()
|
|||
|
|
_TODO_CATEGORIES.extend(_build_category_list())
|
|||
|
|
if _active_category[0] not in _TODO_CATEGORIES:
|
|||
|
|
_active_category[0] = "Allgemein"
|
|||
|
|
for cat in _TODO_CATEGORIES:
|
|||
|
|
style = _cat_active_style if cat == _active_category[0] else _cat_inactive_style
|
|||
|
|
b = tk.Label(_cat_btns_frame, text=cat, font=("Segoe UI", 8),
|
|||
|
|
cursor="hand2", padx=8, pady=2, relief="flat", bd=0,
|
|||
|
|
**style)
|
|||
|
|
b.pack(side="left", padx=1)
|
|||
|
|
b.bind("<Button-1>", lambda e, c=cat: _set_category_filter(c))
|
|||
|
|
_cat_btns[cat] = b
|
|||
|
|
_update_cat_combobox()
|
|||
|
|
|
|||
|
|
def _add_custom_category():
|
|||
|
|
dlg = tk.Toplevel(win)
|
|||
|
|
dlg.title("Neuen Bereich erstellen")
|
|||
|
|
dlg.configure(bg="#E8F4FA")
|
|||
|
|
dlg.resizable(False, False)
|
|||
|
|
dlg.attributes("-topmost", True)
|
|||
|
|
dlg.grab_set()
|
|||
|
|
center_window(dlg, 320, 130)
|
|||
|
|
self._register_window(dlg)
|
|||
|
|
|
|||
|
|
tk.Label(dlg, text="Name des neuen Bereichs:",
|
|||
|
|
font=("Segoe UI", 10), bg="#E8F4FA", fg="#1a4d6d").pack(padx=16, pady=(12, 4))
|
|||
|
|
name_var = tk.StringVar()
|
|||
|
|
name_entry = tk.Entry(dlg, textvariable=name_var, font=("Segoe UI", 10),
|
|||
|
|
bg="white", fg="#1a4d6d", relief="flat", bd=0,
|
|||
|
|
insertbackground="#1a4d6d")
|
|||
|
|
name_entry.pack(padx=16, fill="x", ipady=4)
|
|||
|
|
name_entry.focus_set()
|
|||
|
|
|
|||
|
|
btn_row = tk.Frame(dlg, bg="#E8F4FA")
|
|||
|
|
btn_row.pack(pady=10)
|
|||
|
|
|
|||
|
|
def _do_add():
|
|||
|
|
name = name_var.get().strip()
|
|||
|
|
if not name:
|
|||
|
|
return
|
|||
|
|
all_cats = _DEFAULT_CATEGORIES + _custom_categories + ["Alle"]
|
|||
|
|
if name in all_cats:
|
|||
|
|
messagebox.showwarning("Bereich existiert",
|
|||
|
|
f"Der Bereich «{name}» existiert bereits.",
|
|||
|
|
parent=dlg)
|
|||
|
|
return
|
|||
|
|
_custom_categories.append(name)
|
|||
|
|
_rebuild_cat_buttons()
|
|||
|
|
_set_category_filter(name)
|
|||
|
|
dlg.destroy()
|
|||
|
|
|
|||
|
|
name_entry.bind("<Return>", lambda e: _do_add())
|
|||
|
|
tk.Button(btn_row, text="Erstellen", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
padx=12, pady=2, cursor="hand2",
|
|||
|
|
command=_do_add).pack(side="left", padx=4)
|
|||
|
|
tk.Button(btn_row, text="Abbrechen", font=("Segoe UI", 9),
|
|||
|
|
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
|
|||
|
|
relief="flat", bd=0, padx=12, pady=2, cursor="hand2",
|
|||
|
|
command=dlg.destroy).pack(side="left", padx=4)
|
|||
|
|
|
|||
|
|
def _remove_custom_category():
|
|||
|
|
removable = [c for c in _custom_categories]
|
|||
|
|
if not removable:
|
|||
|
|
messagebox.showinfo("Keine eigenen Bereiche",
|
|||
|
|
"Es gibt keine selbst erstellten Bereiche zum Entfernen.\n"
|
|||
|
|
"Die Standardbereiche (Allgemein, MPA, Ärzte, Alle) können nicht entfernt werden.",
|
|||
|
|
parent=win)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
dlg = tk.Toplevel(win)
|
|||
|
|
dlg.title("Bereich entfernen")
|
|||
|
|
dlg.configure(bg="#E8F4FA")
|
|||
|
|
dlg.resizable(False, False)
|
|||
|
|
dlg.attributes("-topmost", True)
|
|||
|
|
dlg.grab_set()
|
|||
|
|
center_window(dlg, 320, 160)
|
|||
|
|
self._register_window(dlg)
|
|||
|
|
|
|||
|
|
tk.Label(dlg, text="Welchen Bereich möchten Sie entfernen?",
|
|||
|
|
font=("Segoe UI", 10), bg="#E8F4FA", fg="#1a4d6d").pack(padx=16, pady=(12, 4))
|
|||
|
|
|
|||
|
|
sel_var = tk.StringVar(value=removable[0])
|
|||
|
|
combo = ttk.Combobox(dlg, textvariable=sel_var, values=removable,
|
|||
|
|
state="readonly", font=("Segoe UI", 10), width=20)
|
|||
|
|
combo.pack(padx=16, pady=4)
|
|||
|
|
|
|||
|
|
btn_row = tk.Frame(dlg, bg="#E8F4FA")
|
|||
|
|
btn_row.pack(pady=10)
|
|||
|
|
|
|||
|
|
def _do_remove():
|
|||
|
|
name = sel_var.get()
|
|||
|
|
if not name:
|
|||
|
|
return
|
|||
|
|
confirm = messagebox.askyesno(
|
|||
|
|
"Bereich entfernen",
|
|||
|
|
f"Möchten Sie den Bereich «{name}» wirklich entfernen?\n\n"
|
|||
|
|
f"Die Aufgaben in diesem Bereich werden nicht gelöscht,\n"
|
|||
|
|
f"sondern in «Allgemein» verschoben.",
|
|||
|
|
parent=dlg)
|
|||
|
|
if not confirm:
|
|||
|
|
return
|
|||
|
|
_custom_categories.remove(name)
|
|||
|
|
for todo in todos:
|
|||
|
|
if todo.get("category") == name:
|
|||
|
|
todo["category"] = "Allgemein"
|
|||
|
|
save_todos(todos)
|
|||
|
|
if _active_category[0] == name:
|
|||
|
|
_active_category[0] = "Allgemein"
|
|||
|
|
_rebuild_cat_buttons()
|
|||
|
|
rebuild_list()
|
|||
|
|
dlg.destroy()
|
|||
|
|
|
|||
|
|
tk.Button(btn_row, text="Entfernen", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E87070", fg="white", activebackground="#D06060",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
padx=12, pady=2, cursor="hand2",
|
|||
|
|
command=_do_remove).pack(side="left", padx=4)
|
|||
|
|
tk.Button(btn_row, text="Abbrechen", font=("Segoe UI", 9),
|
|||
|
|
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
|
|||
|
|
relief="flat", bd=0, padx=12, pady=2, cursor="hand2",
|
|||
|
|
command=dlg.destroy).pack(side="left", padx=4)
|
|||
|
|
|
|||
|
|
_cat_pm_frame = tk.Frame(cat_bar, bg="#D4EEF7")
|
|||
|
|
_cat_pm_frame.pack(side="right")
|
|||
|
|
|
|||
|
|
_plus_btn = tk.Label(_cat_pm_frame, text="+", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#D4EEF7", fg="#5B8DB3", cursor="hand2", padx=2)
|
|||
|
|
_plus_btn.pack(side="left")
|
|||
|
|
_plus_btn.bind("<Button-1>", lambda e: _add_custom_category())
|
|||
|
|
_plus_btn.bind("<Enter>", lambda e: _plus_btn.configure(fg="#1a4d6d"))
|
|||
|
|
_plus_btn.bind("<Leave>", lambda e: _plus_btn.configure(fg="#5B8DB3"))
|
|||
|
|
|
|||
|
|
_minus_btn = tk.Label(_cat_pm_frame, text="−", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#D4EEF7", fg="#5B8DB3", cursor="hand2", padx=2)
|
|||
|
|
_minus_btn.pack(side="left")
|
|||
|
|
_minus_btn.bind("<Button-1>", lambda e: _remove_custom_category())
|
|||
|
|
_minus_btn.bind("<Enter>", lambda e: _minus_btn.configure(fg="#E87070"))
|
|||
|
|
_minus_btn.bind("<Leave>", lambda e: _minus_btn.configure(fg="#5B8DB3"))
|
|||
|
|
|
|||
|
|
# ─── Eingabebereich ───
|
|||
|
|
input_frame = tk.Frame(todo_page, bg="#D4EEF7", pady=8, padx=8)
|
|||
|
|
input_frame.pack(fill="x")
|
|||
|
|
|
|||
|
|
# Zeile 1: Textfeld + Aufnahme-Button
|
|||
|
|
entry_frame = tk.Frame(input_frame, bg="#D4EEF7")
|
|||
|
|
entry_frame.pack(fill="x")
|
|||
|
|
|
|||
|
|
entry_var = tk.StringVar()
|
|||
|
|
entry = tk.Entry(entry_frame, textvariable=entry_var, font=("Segoe UI", 11),
|
|||
|
|
bg="white", fg="#1a4d6d", relief="flat", bd=0,
|
|||
|
|
insertbackground="#1a4d6d")
|
|||
|
|
entry.pack(side="left", fill="x", expand=True, ipady=6, padx=(0, 6))
|
|||
|
|
entry.insert(0, "Neue Aufgabe eingeben oder diktieren…")
|
|||
|
|
entry.configure(fg="#999")
|
|||
|
|
|
|||
|
|
def on_entry_focus_in(e):
|
|||
|
|
if entry.get() == "Neue Aufgabe eingeben oder diktieren…":
|
|||
|
|
entry.delete(0, "end")
|
|||
|
|
entry.configure(fg="#1a4d6d")
|
|||
|
|
|
|||
|
|
def on_entry_focus_out(e):
|
|||
|
|
if not entry.get().strip():
|
|||
|
|
entry.insert(0, "Neue Aufgabe eingeben oder diktieren…")
|
|||
|
|
entry.configure(fg="#999")
|
|||
|
|
|
|||
|
|
entry.bind("<FocusIn>", on_entry_focus_in)
|
|||
|
|
entry.bind("<FocusOut>", on_entry_focus_out)
|
|||
|
|
|
|||
|
|
# Aufnahme-Button
|
|||
|
|
rec_btn = tk.Button(entry_frame, text="⏺", font=("Segoe UI", 14),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
width=3, cursor="hand2")
|
|||
|
|
rec_btn.pack(side="right")
|
|||
|
|
|
|||
|
|
rec_status_var = tk.StringVar(value="")
|
|||
|
|
rec_status_label = tk.Label(input_frame, textvariable=rec_status_var,
|
|||
|
|
font=("Segoe UI", 8), bg="#D4EEF7", fg="#5B8DB3")
|
|||
|
|
rec_status_label.pack(fill="x")
|
|||
|
|
|
|||
|
|
def toggle_todo_record():
|
|||
|
|
if not self.ensure_ready():
|
|||
|
|
return
|
|||
|
|
if not is_recording[0]:
|
|||
|
|
rec = AudioRecorder()
|
|||
|
|
todo_recorder[0] = rec
|
|||
|
|
try:
|
|||
|
|
rec.start()
|
|||
|
|
is_recording[0] = True
|
|||
|
|
rec_btn.configure(text="⏹", bg="#C03030", activebackground="#A02020")
|
|||
|
|
rec_status_var.set("🔴 Aufnahme läuft… Klicken zum Stoppen.")
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("Aufnahme-Fehler", str(e), parent=win)
|
|||
|
|
rec_status_var.set("")
|
|||
|
|
else:
|
|||
|
|
is_recording[0] = False
|
|||
|
|
rec = todo_recorder[0]
|
|||
|
|
rec_btn.configure(text="⏺", bg="#5B8DB3", activebackground="#4A7A9E")
|
|||
|
|
rec_status_var.set("Transkribiere…")
|
|||
|
|
|
|||
|
|
def worker():
|
|||
|
|
try:
|
|||
|
|
wav_path = rec.stop_and_save_wav()
|
|||
|
|
import wave
|
|||
|
|
try:
|
|||
|
|
with wave.open(wav_path, 'rb') as wf:
|
|||
|
|
dur = wf.getnframes() / float(wf.getframerate())
|
|||
|
|
if dur < 0.3:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
todo_recorder[0] = None
|
|||
|
|
self.after(0, lambda: rec_status_var.set("Kein Audio erkannt."))
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
transcript_result = self.transcribe_wav(wav_path)
|
|||
|
|
if hasattr(transcript_result, 'text'):
|
|||
|
|
text = transcript_result.text
|
|||
|
|
elif isinstance(transcript_result, str):
|
|||
|
|
text = transcript_result
|
|||
|
|
else:
|
|||
|
|
text = ""
|
|||
|
|
try:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if not text or not text.strip():
|
|||
|
|
todo_recorder[0] = None
|
|||
|
|
self.after(0, lambda: rec_status_var.set("Kein Text erkannt."))
|
|||
|
|
return
|
|||
|
|
text = self._diktat_apply_punctuation(text)
|
|||
|
|
cleaned_text, extracted_date = extract_date_from_todo_text(text)
|
|||
|
|
def _done(ct=cleaned_text, ed=extracted_date):
|
|||
|
|
todo_recorder[0] = None
|
|||
|
|
current = entry_var.get()
|
|||
|
|
if current == "Neue Aufgabe eingeben oder diktieren…":
|
|||
|
|
current = ""
|
|||
|
|
new_text = (current + " " + ct).strip() if current.strip() else ct.strip()
|
|||
|
|
entry_var.set(new_text)
|
|||
|
|
entry.configure(fg="#1a4d6d")
|
|||
|
|
entry.icursor("end")
|
|||
|
|
if ed:
|
|||
|
|
set_date(ed)
|
|||
|
|
rec_status_var.set(f"✓ Diktiert – Datum erkannt: {ed.strftime('%d.%m.%Y')}")
|
|||
|
|
else:
|
|||
|
|
rec_status_var.set("✓ Diktiert – bei Bedarf bearbeiten, dann hinzufügen.")
|
|||
|
|
self.after(0, _done)
|
|||
|
|
except Exception as e:
|
|||
|
|
self.after(0, lambda: rec_status_var.set(f"Fehler: {e}"))
|
|||
|
|
todo_recorder[0] = None
|
|||
|
|
|
|||
|
|
threading.Thread(target=worker, daemon=True).start()
|
|||
|
|
|
|||
|
|
rec_btn.configure(command=toggle_todo_record)
|
|||
|
|
|
|||
|
|
# Zeile 2: Datum (Kalender) + Hinzufügen
|
|||
|
|
date_frame = tk.Frame(input_frame, bg="#D4EEF7")
|
|||
|
|
date_frame.pack(fill="x", pady=(4, 0))
|
|||
|
|
|
|||
|
|
tk.Label(date_frame, text="Fällig:", font=("Segoe UI", 9),
|
|||
|
|
bg="#D4EEF7", fg="#4a8aaa").pack(side="left")
|
|||
|
|
|
|||
|
|
selected_date = [None]
|
|||
|
|
|
|||
|
|
date_display = tk.Label(date_frame, text="kein Datum", font=("Segoe UI", 10),
|
|||
|
|
bg="#D4EEF7", fg="#999")
|
|||
|
|
date_display.pack(side="left", padx=(4, 4))
|
|||
|
|
|
|||
|
|
def set_date(d):
|
|||
|
|
selected_date[0] = d
|
|||
|
|
if d:
|
|||
|
|
date_display.configure(text=d.strftime("%d.%m.%Y"), fg="#1a4d6d")
|
|||
|
|
else:
|
|||
|
|
date_display.configure(text="kein Datum", fg="#999")
|
|||
|
|
|
|||
|
|
def clear_date():
|
|||
|
|
set_date(None)
|
|||
|
|
|
|||
|
|
# ─── Kalender-Popup ───
|
|||
|
|
def open_calendar():
|
|||
|
|
cal_win = tk.Toplevel(win)
|
|||
|
|
cal_win.title("Datum wählen")
|
|||
|
|
cal_win.attributes("-topmost", True)
|
|||
|
|
cal_win.configure(bg="#E8F4FA")
|
|||
|
|
cal_win.resizable(False, False)
|
|||
|
|
self._register_window(cal_win)
|
|||
|
|
|
|||
|
|
today = date.today()
|
|||
|
|
current = [selected_date[0].year if selected_date[0] else today.year,
|
|||
|
|
selected_date[0].month if selected_date[0] else today.month]
|
|||
|
|
|
|||
|
|
nav = tk.Frame(cal_win, bg="#B9ECFA")
|
|||
|
|
nav.pack(fill="x")
|
|||
|
|
|
|||
|
|
month_label = tk.Label(nav, text="", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#1a4d6d")
|
|||
|
|
|
|||
|
|
def update_cal():
|
|||
|
|
month_label.configure(
|
|||
|
|
text=f"{cal_mod.month_name[current[1]]} {current[0]}")
|
|||
|
|
for w in days_frame.winfo_children():
|
|||
|
|
w.destroy()
|
|||
|
|
weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
|
|||
|
|
for c, wd in enumerate(weekdays):
|
|||
|
|
fg = "#C03030" if c >= 5 else "#4a8aaa"
|
|||
|
|
tk.Label(days_frame, text=wd, font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg=fg, width=4).grid(row=0, column=c)
|
|||
|
|
first_wd, num_days = cal_mod.monthrange(current[0], current[1])
|
|||
|
|
row = 1
|
|||
|
|
col = first_wd
|
|||
|
|
for day in range(1, num_days + 1):
|
|||
|
|
d = date(current[0], current[1], day)
|
|||
|
|
is_today = (d == today)
|
|||
|
|
is_selected = (d == selected_date[0])
|
|||
|
|
is_past = (d < today)
|
|||
|
|
if is_selected:
|
|||
|
|
bg, fg = "#5B8DB3", "white"
|
|||
|
|
elif is_today:
|
|||
|
|
bg, fg = "#D4EEF7", "#1a4d6d"
|
|||
|
|
else:
|
|||
|
|
bg, fg = "#F5FCFF", ("#999" if is_past else "#1a4d6d")
|
|||
|
|
btn = tk.Label(days_frame, text=str(day), font=("Segoe UI", 10),
|
|||
|
|
bg=bg, fg=fg, width=4, cursor="hand2",
|
|||
|
|
relief="flat", bd=0)
|
|||
|
|
btn.grid(row=row, column=col, padx=1, pady=1)
|
|||
|
|
btn.bind("<Button-1>", lambda e, dd=d: (set_date(dd), cal_win.destroy()))
|
|||
|
|
btn.bind("<Enter>", lambda e, b=btn: b.configure(bg="#B9ECFA") if b.cget("bg") not in ("#5B8DB3",) else None)
|
|||
|
|
btn.bind("<Leave>", lambda e, b=btn, bg0=bg: b.configure(bg=bg0))
|
|||
|
|
col += 1
|
|||
|
|
if col > 6:
|
|||
|
|
col = 0
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
def prev_month():
|
|||
|
|
current[1] -= 1
|
|||
|
|
if current[1] < 1:
|
|||
|
|
current[1] = 12
|
|||
|
|
current[0] -= 1
|
|||
|
|
update_cal()
|
|||
|
|
|
|||
|
|
def next_month():
|
|||
|
|
current[1] += 1
|
|||
|
|
if current[1] > 12:
|
|||
|
|
current[1] = 1
|
|||
|
|
current[0] += 1
|
|||
|
|
update_cal()
|
|||
|
|
|
|||
|
|
btn_prev = tk.Label(nav, text="◀", font=("Segoe UI", 12), bg="#B9ECFA",
|
|||
|
|
fg="#1a4d6d", cursor="hand2", padx=8)
|
|||
|
|
btn_prev.pack(side="left")
|
|||
|
|
btn_prev.bind("<Button-1>", lambda e: prev_month())
|
|||
|
|
|
|||
|
|
month_label.pack(side="left", fill="x", expand=True)
|
|||
|
|
|
|||
|
|
btn_next = tk.Label(nav, text="▶", font=("Segoe UI", 12), bg="#B9ECFA",
|
|||
|
|
fg="#1a4d6d", cursor="hand2", padx=8)
|
|||
|
|
btn_next.pack(side="right")
|
|||
|
|
btn_next.bind("<Button-1>", lambda e: next_month())
|
|||
|
|
|
|||
|
|
days_frame = tk.Frame(cal_win, bg="#E8F4FA", padx=4, pady=4)
|
|||
|
|
days_frame.pack(fill="both")
|
|||
|
|
|
|||
|
|
# Schnellauswahl
|
|||
|
|
quick = tk.Frame(cal_win, bg="#D4EEF7", pady=4)
|
|||
|
|
quick.pack(fill="x")
|
|||
|
|
for label_text, delta in [("Heute", 0), ("Morgen", 1), ("+1 Wo", 7)]:
|
|||
|
|
d = today + timedelta(days=delta)
|
|||
|
|
tk.Button(quick, text=label_text, font=("Segoe UI", 8),
|
|||
|
|
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
|
|||
|
|
relief="flat", bd=0, padx=6, pady=1, cursor="hand2",
|
|||
|
|
command=lambda dd=d: (set_date(dd), cal_win.destroy())
|
|||
|
|
).pack(side="left", padx=2)
|
|||
|
|
tk.Button(quick, text="Kein Datum", font=("Segoe UI", 8),
|
|||
|
|
bg="#E0E0E0", fg="#666", activebackground="#D0D0D0",
|
|||
|
|
relief="flat", bd=0, padx=6, pady=1, cursor="hand2",
|
|||
|
|
command=lambda: (clear_date(), cal_win.destroy())
|
|||
|
|
).pack(side="right", padx=2)
|
|||
|
|
|
|||
|
|
update_cal()
|
|||
|
|
|
|||
|
|
cal_win.update_idletasks()
|
|||
|
|
x = win.winfo_x() + 50
|
|||
|
|
y = win.winfo_y() + 120
|
|||
|
|
cal_win.geometry(f"+{x}+{y}")
|
|||
|
|
|
|||
|
|
cal_btn = tk.Button(date_frame, text="📅", font=("Segoe UI", 12),
|
|||
|
|
bg="#D4EEF7", fg="#1a4d6d", activebackground="#C4DEE7",
|
|||
|
|
relief="flat", bd=0, cursor="hand2", command=open_calendar)
|
|||
|
|
cal_btn.pack(side="left", padx=(0, 4))
|
|||
|
|
|
|||
|
|
clear_date_btn = tk.Label(date_frame, text="✕", font=("Segoe UI", 9),
|
|||
|
|
bg="#D4EEF7", fg="#ccc", cursor="hand2")
|
|||
|
|
clear_date_btn.pack(side="left")
|
|||
|
|
clear_date_btn.bind("<Button-1>", lambda e: clear_date())
|
|||
|
|
clear_date_btn.bind("<Enter>", lambda e: clear_date_btn.configure(fg="#E87070"))
|
|||
|
|
clear_date_btn.bind("<Leave>", lambda e: clear_date_btn.configure(fg="#ccc"))
|
|||
|
|
|
|||
|
|
def _get_new_todo_category():
|
|||
|
|
cat = _active_category[0]
|
|||
|
|
return "Allgemein" if cat == "Alle" else cat
|
|||
|
|
|
|||
|
|
def _update_cat_combobox():
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
_rebuild_cat_buttons()
|
|||
|
|
|
|||
|
|
add_btn = tk.Button(date_frame, text="+ Hinzufügen", font=("Segoe UI", 10, "bold"),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0, padx=12, pady=2,
|
|||
|
|
cursor="hand2")
|
|||
|
|
add_btn.pack(side="right", padx=(0, 8))
|
|||
|
|
|
|||
|
|
# ─── Scrollbarer Bereich für To-dos ───
|
|||
|
|
list_outer = tk.Frame(todo_page, bg="#E8F4FA")
|
|||
|
|
list_outer.pack(fill="both", expand=True, padx=8, pady=(4, 0))
|
|||
|
|
|
|||
|
|
canvas = tk.Canvas(list_outer, bg="#E8F4FA", highlightthickness=0)
|
|||
|
|
scrollbar = ttk.Scrollbar(list_outer, orient="vertical", command=canvas.yview)
|
|||
|
|
list_frame = tk.Frame(canvas, bg="#E8F4FA")
|
|||
|
|
|
|||
|
|
list_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|||
|
|
canvas.create_window((0, 0), window=list_frame, anchor="nw", tags="list_win")
|
|||
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|||
|
|
|
|||
|
|
def _resize_list(event):
|
|||
|
|
canvas.itemconfig("list_win", width=event.width)
|
|||
|
|
|
|||
|
|
canvas.bind("<Configure>", _resize_list)
|
|||
|
|
canvas.pack(side="left", fill="both", expand=True)
|
|||
|
|
scrollbar.pack(side="right", fill="y")
|
|||
|
|
|
|||
|
|
def _on_mousewheel(event):
|
|||
|
|
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|||
|
|
|
|||
|
|
def _bind_mousewheel_recursive(widget):
|
|||
|
|
"""Bindet Mausrad-Scrolling an Widget und alle Kinder (rekursiv)."""
|
|||
|
|
widget.bind("<MouseWheel>", _on_mousewheel)
|
|||
|
|
for child in widget.winfo_children():
|
|||
|
|
_bind_mousewheel_recursive(child)
|
|||
|
|
|
|||
|
|
canvas.bind("<MouseWheel>", _on_mousewheel)
|
|||
|
|
list_frame.bind("<MouseWheel>", _on_mousewheel)
|
|||
|
|
|
|||
|
|
# ─── Fusszeile ───
|
|||
|
|
footer = tk.Frame(todo_page, bg="#D4EEF7", pady=4)
|
|||
|
|
footer.pack(fill="x", side="bottom")
|
|||
|
|
|
|||
|
|
btn_del_done = tk.Button(footer, text="🗑 Erledigte löschen", font=("Segoe UI", 9),
|
|||
|
|
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
|
|||
|
|
relief="flat", bd=0, padx=8, pady=2, cursor="hand2")
|
|||
|
|
btn_del_done.pack(side="left", padx=8)
|
|||
|
|
|
|||
|
|
btn_email = tk.Button(footer, text="✉ E-Mail senden", font=("Segoe UI", 9),
|
|||
|
|
bg="#A8D8E8", fg="#1a4d6d", activebackground="#98C8D8",
|
|||
|
|
relief="flat", bd=0, padx=8, pady=2, cursor="hand2")
|
|||
|
|
btn_email.pack(side="right", padx=(0, 8))
|
|||
|
|
|
|||
|
|
btn_medwork = tk.Button(footer, text="📤 An MedWork senden", font=("Segoe UI", 9),
|
|||
|
|
bg="#7EC8E3", fg="#1a4d6d", activebackground="#6CB8D3",
|
|||
|
|
relief="flat", bd=0, padx=8, pady=2, cursor="hand2")
|
|||
|
|
btn_medwork.pack(side="right", padx=(0, 4))
|
|||
|
|
|
|||
|
|
# Senden-Auswahl (Häkchen-Tracking)
|
|||
|
|
send_selection = {} # idx -> BooleanVar
|
|||
|
|
|
|||
|
|
# MedWork-Kontakte (erweiterbar)
|
|||
|
|
medwork_contacts = [
|
|||
|
|
"Dr. med. Muster (Innere Medizin)",
|
|||
|
|
"Dr. med. Beispiel (Chirurgie)",
|
|||
|
|
"Dr. med. Test (Kardiologie)",
|
|||
|
|
"Praxis-Team intern",
|
|||
|
|
]
|
|||
|
|
MEDWORK_CONTACTS_FILE = "kg_diktat_medwork_contacts.json"
|
|||
|
|
|
|||
|
|
def _load_medwork_contacts():
|
|||
|
|
try:
|
|||
|
|
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), MEDWORK_CONTACTS_FILE)
|
|||
|
|
if os.path.isfile(p):
|
|||
|
|
with open(p, "r", encoding="utf-8") as f:
|
|||
|
|
return json.load(f)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return medwork_contacts[:]
|
|||
|
|
|
|||
|
|
def _save_medwork_contacts(contacts):
|
|||
|
|
try:
|
|||
|
|
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), MEDWORK_CONTACTS_FILE)
|
|||
|
|
with open(p, "w", encoding="utf-8") as f:
|
|||
|
|
json.dump(contacts, f, indent=2, ensure_ascii=False)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _get_selected_items():
|
|||
|
|
"""Gibt die mit 'senden'-Häkchen ausgewählten To-dos zurück."""
|
|||
|
|
selected = []
|
|||
|
|
for i, sv in send_selection.items():
|
|||
|
|
if sv.get() and 0 <= i < len(todos):
|
|||
|
|
selected.append(todos[i])
|
|||
|
|
return selected
|
|||
|
|
|
|||
|
|
def _format_todo_for_email(t):
|
|||
|
|
"""Formatiert ein To-do für E-Mail-Text."""
|
|||
|
|
prio_map = {1: "HOCH", 2: "MITTEL", 0: ""}
|
|||
|
|
line = f"- {t.get('text', '')}"
|
|||
|
|
if t.get("date"):
|
|||
|
|
try:
|
|||
|
|
dt = datetime.strptime(t["date"], "%Y-%m-%d")
|
|||
|
|
line += f" [Fällig: {dt.strftime('%d.%m.%Y')}]"
|
|||
|
|
except Exception:
|
|||
|
|
line += f" [Fällig: {t['date']}]"
|
|||
|
|
p = prio_map.get(t.get("priority", 0), "")
|
|||
|
|
if p:
|
|||
|
|
line += f" [Priorität: {p}]"
|
|||
|
|
if t.get("notes", "").strip():
|
|||
|
|
line += f"\n Notiz: {t['notes'].strip()}"
|
|||
|
|
return line
|
|||
|
|
|
|||
|
|
def _send_items_to_medwork(items):
|
|||
|
|
"""Öffnet Dialog zum Senden von To-do(s) an MedWork (in die Inbox des Empfängers)."""
|
|||
|
|
if not items:
|
|||
|
|
messagebox.showinfo("MedWork", "Keine Aufgaben ausgewählt.", parent=win)
|
|||
|
|
return
|
|||
|
|
mw_win = tk.Toplevel(win)
|
|||
|
|
mw_win.title("An MedWork senden")
|
|||
|
|
mw_win.attributes("-topmost", True)
|
|||
|
|
mw_win.configure(bg="#E8F4FA")
|
|||
|
|
mw_win.resizable(False, False)
|
|||
|
|
mw_win.geometry("420x440")
|
|||
|
|
center_window(mw_win, 420, 440)
|
|||
|
|
self._register_window(mw_win)
|
|||
|
|
|
|||
|
|
tk.Label(mw_win, text="📤 Aufgabe(n) an MedWork senden", font=("Segoe UI", 12, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
|
|||
|
|
|
|||
|
|
# Aufgaben-Vorschau
|
|||
|
|
tk.Label(mw_win, text=f"{len(items)} Aufgabe(n):", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", padx=12, pady=(8, 2))
|
|||
|
|
preview_f = tk.Frame(mw_win, bg="white", padx=6, pady=4)
|
|||
|
|
preview_f.pack(fill="x", padx=12, pady=(0, 8))
|
|||
|
|
for it in items[:5]:
|
|||
|
|
tk.Label(preview_f, text=f"• {it['text'][:55]}{'…' if len(it.get('text',''))>55 else ''}",
|
|||
|
|
font=("Segoe UI", 9), bg="white", fg="#1a4d6d", anchor="w").pack(fill="x")
|
|||
|
|
if len(items) > 5:
|
|||
|
|
tk.Label(preview_f, text=f"… und {len(items)-5} weitere",
|
|||
|
|
font=("Segoe UI", 8, "italic"), bg="white", fg="#999").pack(fill="x")
|
|||
|
|
|
|||
|
|
# Kontakt-Auswahl
|
|||
|
|
tk.Label(mw_win, text="Empfänger:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", padx=12, pady=(4, 2))
|
|||
|
|
contacts = _load_medwork_contacts()
|
|||
|
|
contact_var = tk.StringVar()
|
|||
|
|
contact_var.set(contacts[0] if contacts else "")
|
|||
|
|
contact_menu = ttk.Combobox(mw_win, textvariable=contact_var, values=contacts,
|
|||
|
|
font=("Segoe UI", 10))
|
|||
|
|
contact_menu.pack(fill="x", padx=12, pady=(0, 8))
|
|||
|
|
|
|||
|
|
# Nachricht mit Diktat-Button
|
|||
|
|
msg_label_row = tk.Frame(mw_win, bg="#E8F4FA")
|
|||
|
|
msg_label_row.pack(fill="x", padx=12, pady=(4, 2))
|
|||
|
|
tk.Label(msg_label_row, text="Nachricht (optional):", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
|
|||
|
|
|
|||
|
|
mw_recorder = [None]
|
|||
|
|
mw_is_recording = [False]
|
|||
|
|
mw_rec_status = tk.StringVar(value="")
|
|||
|
|
|
|||
|
|
mw_rec_btn = tk.Button(msg_label_row, text="⏺", font=("Segoe UI", 11),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
width=3, cursor="hand2")
|
|||
|
|
mw_rec_btn.pack(side="right")
|
|||
|
|
|
|||
|
|
msg_text = tk.Text(mw_win, font=("Segoe UI", 10), height=4, bg="white",
|
|||
|
|
fg="#1a4d6d", relief="flat", bd=1)
|
|||
|
|
msg_text.pack(fill="x", padx=12, pady=(0, 2))
|
|||
|
|
|
|||
|
|
tk.Label(mw_win, textvariable=mw_rec_status, font=("Segoe UI", 8),
|
|||
|
|
bg="#E8F4FA", fg="#5B8DB3").pack(fill="x", padx=12, pady=(0, 6))
|
|||
|
|
|
|||
|
|
def _mw_toggle_record():
|
|||
|
|
if not self.ensure_ready():
|
|||
|
|
return
|
|||
|
|
if not mw_is_recording[0]:
|
|||
|
|
rec = AudioRecorder()
|
|||
|
|
mw_recorder[0] = rec
|
|||
|
|
try:
|
|||
|
|
rec.start()
|
|||
|
|
mw_is_recording[0] = True
|
|||
|
|
mw_rec_btn.configure(text="⏹", bg="#C03030", activebackground="#A02020")
|
|||
|
|
mw_rec_status.set("🔴 Aufnahme läuft…")
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("Aufnahme-Fehler", str(e), parent=mw_win)
|
|||
|
|
mw_rec_status.set("")
|
|||
|
|
else:
|
|||
|
|
mw_is_recording[0] = False
|
|||
|
|
rec = mw_recorder[0]
|
|||
|
|
mw_rec_btn.configure(text="⏺", bg="#5B8DB3", activebackground="#4A7A9E")
|
|||
|
|
mw_rec_status.set("Transkribiere…")
|
|||
|
|
|
|||
|
|
def worker():
|
|||
|
|
try:
|
|||
|
|
wav_path = rec.stop_and_save_wav()
|
|||
|
|
import wave as _wave
|
|||
|
|
try:
|
|||
|
|
with _wave.open(wav_path, 'rb') as wf:
|
|||
|
|
dur = wf.getnframes() / float(wf.getframerate())
|
|||
|
|
if dur < 0.3:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
mw_recorder[0] = None
|
|||
|
|
self.after(0, lambda: mw_rec_status.set("Kein Audio erkannt."))
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
transcript_result = self.transcribe_wav(wav_path)
|
|||
|
|
if hasattr(transcript_result, 'text'):
|
|||
|
|
text = transcript_result.text
|
|||
|
|
elif isinstance(transcript_result, str):
|
|||
|
|
text = transcript_result
|
|||
|
|
else:
|
|||
|
|
text = ""
|
|||
|
|
try:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if not text or not text.strip():
|
|||
|
|
mw_recorder[0] = None
|
|||
|
|
self.after(0, lambda: mw_rec_status.set("Kein Text erkannt."))
|
|||
|
|
return
|
|||
|
|
text = self._diktat_apply_punctuation(text)
|
|||
|
|
def _done():
|
|||
|
|
mw_recorder[0] = None
|
|||
|
|
current = msg_text.get("1.0", "end").strip()
|
|||
|
|
new_t = (current + " " + text).strip() if current else text.strip()
|
|||
|
|
msg_text.delete("1.0", "end")
|
|||
|
|
msg_text.insert("1.0", new_t)
|
|||
|
|
mw_rec_status.set("✓ Diktiert.")
|
|||
|
|
self.after(0, _done)
|
|||
|
|
except Exception as e:
|
|||
|
|
self.after(0, lambda: mw_rec_status.set(f"Fehler: {e}"))
|
|||
|
|
mw_recorder[0] = None
|
|||
|
|
|
|||
|
|
threading.Thread(target=worker, daemon=True).start()
|
|||
|
|
|
|||
|
|
mw_rec_btn.configure(command=_mw_toggle_record)
|
|||
|
|
|
|||
|
|
def _mw_on_close():
|
|||
|
|
if mw_is_recording[0] and mw_recorder[0]:
|
|||
|
|
try:
|
|||
|
|
mw_is_recording[0] = False
|
|||
|
|
wp = mw_recorder[0].stop_and_save_wav()
|
|||
|
|
if os.path.exists(wp):
|
|||
|
|
os.remove(wp)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
mw_recorder[0] = None
|
|||
|
|
mw_win.destroy()
|
|||
|
|
|
|||
|
|
mw_win.protocol("WM_DELETE_WINDOW", _mw_on_close)
|
|||
|
|
|
|||
|
|
def do_send():
|
|||
|
|
contact = contact_var.get().strip()
|
|||
|
|
if not contact:
|
|||
|
|
messagebox.showwarning("MedWork", "Bitte Empfänger eingeben.", parent=mw_win)
|
|||
|
|
return
|
|||
|
|
if contact not in contacts:
|
|||
|
|
contacts.append(contact)
|
|||
|
|
_save_medwork_contacts(contacts)
|
|||
|
|
sender_name = self._user_profile.get("name", "Unbekannt")
|
|||
|
|
for it in items:
|
|||
|
|
send_todo_to_inbox(it, sender_name, contact)
|
|||
|
|
messagebox.showinfo(
|
|||
|
|
"MedWork",
|
|||
|
|
f"{len(items)} Aufgabe(n) gesendet an:\n{contact}\n\n"
|
|||
|
|
f"Die Aufgaben erscheinen in der To-Do-Liste des Empfängers.",
|
|||
|
|
parent=mw_win,
|
|||
|
|
)
|
|||
|
|
mw_win.destroy()
|
|||
|
|
|
|||
|
|
tk.Button(mw_win, text="📤 Senden", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
relief="flat", bd=0, padx=20, pady=6, cursor="hand2",
|
|||
|
|
command=do_send).pack(pady=12)
|
|||
|
|
|
|||
|
|
def _send_items_via_email(items):
|
|||
|
|
"""Öffnet den Standard-E-Mail-Client mit allen To-do-Infos."""
|
|||
|
|
import urllib.parse
|
|||
|
|
if not items:
|
|||
|
|
messagebox.showinfo("E-Mail", "Keine Aufgaben ausgewählt.", parent=win)
|
|||
|
|
return
|
|||
|
|
sender = self._user_profile.get("name", "")
|
|||
|
|
subject = f"To-Do Liste von {sender}" if sender else "To-Do Liste"
|
|||
|
|
body_lines = [f"To-Do Liste ({date.today().strftime('%d.%m.%Y')})", ""]
|
|||
|
|
for it in items:
|
|||
|
|
body_lines.append(_format_todo_for_email(it))
|
|||
|
|
body_lines.append("")
|
|||
|
|
body_lines.append(f"Gesendet von AzA – {sender}")
|
|||
|
|
body = "\n".join(body_lines)
|
|||
|
|
mailto_url = f"mailto:?subject={urllib.parse.quote(subject)}&body={urllib.parse.quote(body)}"
|
|||
|
|
try:
|
|||
|
|
os.startfile(mailto_url)
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("E-Mail", f"Konnte E-Mail-Client nicht öffnen:\n{e}", parent=win)
|
|||
|
|
|
|||
|
|
def send_to_medwork():
|
|||
|
|
selected = _get_selected_items()
|
|||
|
|
if selected:
|
|||
|
|
_send_items_to_medwork(selected)
|
|||
|
|
else:
|
|||
|
|
open_items = [t for t in todos if not t.get("done")]
|
|||
|
|
if not open_items:
|
|||
|
|
messagebox.showinfo("MedWork", "Keine offenen Aufgaben zum Senden.", parent=win)
|
|||
|
|
return
|
|||
|
|
_send_items_to_medwork(open_items)
|
|||
|
|
|
|||
|
|
def send_email():
|
|||
|
|
selected = _get_selected_items()
|
|||
|
|
if selected:
|
|||
|
|
_send_items_via_email(selected)
|
|||
|
|
else:
|
|||
|
|
all_items = [t for t in todos if not t.get("done")]
|
|||
|
|
if not all_items:
|
|||
|
|
messagebox.showinfo("E-Mail", "Keine offenen Aufgaben zum Senden.", parent=win)
|
|||
|
|
return
|
|||
|
|
_send_items_via_email(all_items)
|
|||
|
|
|
|||
|
|
btn_medwork.configure(command=send_to_medwork)
|
|||
|
|
btn_email.configure(command=send_email)
|
|||
|
|
|
|||
|
|
# ─── Notizen-Seite ───
|
|||
|
|
user_notes = load_notes()
|
|||
|
|
notes_widgets = []
|
|||
|
|
_notes_recorder = [None]
|
|||
|
|
_notes_is_recording = [False]
|
|||
|
|
_notes_target_widget = [None]
|
|||
|
|
|
|||
|
|
notes_input_frame = tk.Frame(notes_page, bg="#D4EEF7", pady=8, padx=8)
|
|||
|
|
notes_input_frame.pack(fill="x")
|
|||
|
|
|
|||
|
|
notes_rec_status_var = tk.StringVar(value="")
|
|||
|
|
|
|||
|
|
def _notes_generic_toggle_record(target_widget, btn_ref):
|
|||
|
|
if not self.ensure_ready():
|
|||
|
|
return
|
|||
|
|
if not _notes_is_recording[0]:
|
|||
|
|
rec = AudioRecorder()
|
|||
|
|
_notes_recorder[0] = rec
|
|||
|
|
_notes_target_widget[0] = target_widget
|
|||
|
|
try:
|
|||
|
|
rec.start()
|
|||
|
|
_notes_is_recording[0] = True
|
|||
|
|
btn_ref.configure(text="⏹", bg="#C03030", activebackground="#A02020")
|
|||
|
|
notes_rec_status_var.set("🔴 Aufnahme läuft…")
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("Aufnahme-Fehler", str(e), parent=win)
|
|||
|
|
notes_rec_status_var.set("")
|
|||
|
|
else:
|
|||
|
|
_notes_is_recording[0] = False
|
|||
|
|
rec = _notes_recorder[0]
|
|||
|
|
btn_ref.configure(text="⏺", bg="#5B8DB3", activebackground="#4A7A9E")
|
|||
|
|
notes_rec_status_var.set("Transkribiere…")
|
|||
|
|
tw_ref = _notes_target_widget[0]
|
|||
|
|
is_entry = isinstance(tw_ref, tk.Entry)
|
|||
|
|
|
|||
|
|
def worker():
|
|||
|
|
try:
|
|||
|
|
wav_path = rec.stop_and_save_wav()
|
|||
|
|
import wave as _wave
|
|||
|
|
try:
|
|||
|
|
with _wave.open(wav_path, 'rb') as wf:
|
|||
|
|
dur = wf.getnframes() / float(wf.getframerate())
|
|||
|
|
if dur < 0.3:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
_notes_recorder[0] = None
|
|||
|
|
self.after(0, lambda: notes_rec_status_var.set("Kein Audio erkannt."))
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
transcript_result = self.transcribe_wav(wav_path)
|
|||
|
|
if hasattr(transcript_result, 'text'):
|
|||
|
|
text = transcript_result.text
|
|||
|
|
elif isinstance(transcript_result, str):
|
|||
|
|
text = transcript_result
|
|||
|
|
else:
|
|||
|
|
text = ""
|
|||
|
|
try:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if not text or not text.strip():
|
|||
|
|
_notes_recorder[0] = None
|
|||
|
|
self.after(0, lambda: notes_rec_status_var.set("Kein Text erkannt."))
|
|||
|
|
return
|
|||
|
|
text = self._diktat_apply_punctuation(text)
|
|||
|
|
def _done(t=text):
|
|||
|
|
_notes_recorder[0] = None
|
|||
|
|
if is_entry:
|
|||
|
|
current = tw_ref.get()
|
|||
|
|
if current == "Neue Notiz – Titel eingeben…":
|
|||
|
|
current = ""
|
|||
|
|
new_text = (current + " " + t).strip() if current.strip() else t.strip()
|
|||
|
|
tw_ref.delete(0, "end")
|
|||
|
|
tw_ref.insert(0, new_text)
|
|||
|
|
tw_ref.configure(fg="#1a4d6d")
|
|||
|
|
else:
|
|||
|
|
current = tw_ref.get("1.0", "end").strip()
|
|||
|
|
new_text = (current + " " + t).strip() if current else t.strip()
|
|||
|
|
tw_ref.delete("1.0", "end")
|
|||
|
|
tw_ref.insert("1.0", new_text)
|
|||
|
|
notes_rec_status_var.set("✓ Diktiert.")
|
|||
|
|
self.after(0, _done)
|
|||
|
|
except Exception as e:
|
|||
|
|
self.after(0, lambda: notes_rec_status_var.set(f"Fehler: {e}"))
|
|||
|
|
_notes_recorder[0] = None
|
|||
|
|
|
|||
|
|
threading.Thread(target=worker, daemon=True).start()
|
|||
|
|
|
|||
|
|
# Titel-Zeile mit Diktier-Button
|
|||
|
|
tk.Label(notes_input_frame, text="Titel:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#D4EEF7", fg="#1a4d6d").pack(anchor="w")
|
|||
|
|
|
|||
|
|
notes_entry_frame = tk.Frame(notes_input_frame, bg="#D4EEF7")
|
|||
|
|
notes_entry_frame.pack(fill="x")
|
|||
|
|
|
|||
|
|
notes_title_var = tk.StringVar()
|
|||
|
|
notes_title_entry = tk.Entry(notes_entry_frame, textvariable=notes_title_var,
|
|||
|
|
font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|||
|
|
relief="flat", bd=0, insertbackground="#1a4d6d")
|
|||
|
|
notes_title_entry.insert(0, "Neue Notiz – Titel eingeben…")
|
|||
|
|
notes_title_entry.configure(fg="#999")
|
|||
|
|
|
|||
|
|
notes_title_rec_btn = tk.Button(notes_entry_frame, text="⏺", font=("Segoe UI", 11),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
width=3, cursor="hand2")
|
|||
|
|
notes_title_rec_btn.pack(side="right")
|
|||
|
|
notes_title_rec_btn.configure(
|
|||
|
|
command=lambda: _notes_generic_toggle_record(notes_title_entry, notes_title_rec_btn))
|
|||
|
|
notes_title_entry.pack(side="left", fill="x", expand=True, ipady=6, padx=(0, 4))
|
|||
|
|
|
|||
|
|
def _notes_entry_focus_in(e):
|
|||
|
|
if notes_title_entry.get() == "Neue Notiz – Titel eingeben…":
|
|||
|
|
notes_title_entry.delete(0, "end")
|
|||
|
|
notes_title_entry.configure(fg="#1a4d6d")
|
|||
|
|
|
|||
|
|
def _notes_entry_focus_out(e):
|
|||
|
|
if not notes_title_entry.get().strip():
|
|||
|
|
notes_title_entry.insert(0, "Neue Notiz – Titel eingeben…")
|
|||
|
|
notes_title_entry.configure(fg="#999")
|
|||
|
|
|
|||
|
|
notes_title_entry.bind("<FocusIn>", _notes_entry_focus_in)
|
|||
|
|
notes_title_entry.bind("<FocusOut>", _notes_entry_focus_out)
|
|||
|
|
|
|||
|
|
# Inhalt-Zeile mit Diktier-Button
|
|||
|
|
notes_text_label_row = tk.Frame(notes_input_frame, bg="#D4EEF7")
|
|||
|
|
notes_text_label_row.pack(fill="x", pady=(6, 0))
|
|||
|
|
tk.Label(notes_text_label_row, text="Inhalt:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#D4EEF7", fg="#1a4d6d").pack(side="left")
|
|||
|
|
|
|||
|
|
notes_text_rec_btn = tk.Button(notes_text_label_row, text="⏺", font=("Segoe UI", 11),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
width=3, cursor="hand2")
|
|||
|
|
notes_text_rec_btn.pack(side="right")
|
|||
|
|
|
|||
|
|
notes_text_input = tk.Text(notes_input_frame, font=("Segoe UI", 10), height=3,
|
|||
|
|
bg="white", fg="#1a4d6d", relief="flat", bd=1, wrap="word")
|
|||
|
|
notes_text_input.pack(fill="x", pady=(2, 0))
|
|||
|
|
|
|||
|
|
notes_text_rec_btn.configure(
|
|||
|
|
command=lambda: _notes_generic_toggle_record(notes_text_input, notes_text_rec_btn))
|
|||
|
|
|
|||
|
|
tk.Label(notes_input_frame, textvariable=notes_rec_status_var, font=("Segoe UI", 8),
|
|||
|
|
bg="#D4EEF7", fg="#5B8DB3").pack(fill="x")
|
|||
|
|
|
|||
|
|
notes_add_frame = tk.Frame(notes_input_frame, bg="#D4EEF7")
|
|||
|
|
notes_add_frame.pack(fill="x", pady=(4, 0))
|
|||
|
|
|
|||
|
|
def _add_note(event=None):
|
|||
|
|
title = notes_title_var.get().strip()
|
|||
|
|
if not title or title == "Neue Notiz – Titel eingeben…":
|
|||
|
|
title = "Notiz"
|
|||
|
|
text_content = notes_text_input.get("1.0", "end").strip()
|
|||
|
|
new_note = {
|
|||
|
|
"id": int(datetime.now().timestamp() * 1000),
|
|||
|
|
"title": title,
|
|||
|
|
"text": text_content,
|
|||
|
|
"created": datetime.now().isoformat(),
|
|||
|
|
}
|
|||
|
|
user_notes.append(new_note)
|
|||
|
|
notes_title_var.set("")
|
|||
|
|
notes_title_entry.delete(0, "end")
|
|||
|
|
notes_title_entry.insert(0, "Neue Notiz – Titel eingeben…")
|
|||
|
|
notes_title_entry.configure(fg="#999")
|
|||
|
|
notes_text_input.delete("1.0", "end")
|
|||
|
|
notes_rec_status_var.set("")
|
|||
|
|
save_notes(user_notes)
|
|||
|
|
_rebuild_notes_list()
|
|||
|
|
|
|||
|
|
notes_title_entry.bind("<Return>", _add_note)
|
|||
|
|
|
|||
|
|
tk.Button(notes_add_frame, text="+ Notiz hinzufügen", font=("Segoe UI", 10, "bold"),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0, padx=12, pady=2,
|
|||
|
|
cursor="hand2", command=_add_note).pack(side="right")
|
|||
|
|
|
|||
|
|
# Scrollbarer Bereich für Notizen
|
|||
|
|
notes_list_outer = tk.Frame(notes_page, bg="#E8F4FA")
|
|||
|
|
notes_list_outer.pack(fill="both", expand=True, padx=8, pady=(4, 0))
|
|||
|
|
|
|||
|
|
notes_canvas = tk.Canvas(notes_list_outer, bg="#E8F4FA", highlightthickness=0)
|
|||
|
|
notes_scrollbar = ttk.Scrollbar(notes_list_outer, orient="vertical", command=notes_canvas.yview)
|
|||
|
|
notes_list_frame = tk.Frame(notes_canvas, bg="#E8F4FA")
|
|||
|
|
|
|||
|
|
notes_list_frame.bind("<Configure>",
|
|||
|
|
lambda e: notes_canvas.configure(scrollregion=notes_canvas.bbox("all")))
|
|||
|
|
notes_canvas.create_window((0, 0), window=notes_list_frame, anchor="nw", tags="notes_win")
|
|||
|
|
notes_canvas.configure(yscrollcommand=notes_scrollbar.set)
|
|||
|
|
|
|||
|
|
def _notes_resize(event):
|
|||
|
|
notes_canvas.itemconfig("notes_win", width=event.width)
|
|||
|
|
|
|||
|
|
notes_canvas.bind("<Configure>", _notes_resize)
|
|||
|
|
notes_canvas.pack(side="left", fill="both", expand=True)
|
|||
|
|
notes_scrollbar.pack(side="right", fill="y")
|
|||
|
|
|
|||
|
|
def _notes_mousewheel(event):
|
|||
|
|
notes_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|||
|
|
|
|||
|
|
def _bind_notes_mousewheel_recursive(widget):
|
|||
|
|
widget.bind("<MouseWheel>", _notes_mousewheel)
|
|||
|
|
for child in widget.winfo_children():
|
|||
|
|
_bind_notes_mousewheel_recursive(child)
|
|||
|
|
|
|||
|
|
notes_canvas.bind("<MouseWheel>", _notes_mousewheel)
|
|||
|
|
notes_list_frame.bind("<MouseWheel>", _notes_mousewheel)
|
|||
|
|
|
|||
|
|
# Notizen-Fusszeile
|
|||
|
|
notes_footer = tk.Frame(notes_page, bg="#D4EEF7", pady=4)
|
|||
|
|
notes_footer.pack(fill="x", side="bottom")
|
|||
|
|
|
|||
|
|
notes_counter_label = tk.Label(notes_footer, text="", font=("Segoe UI", 9),
|
|||
|
|
bg="#D4EEF7", fg="#4a8aaa")
|
|||
|
|
notes_counter_label.pack(side="left", padx=8)
|
|||
|
|
|
|||
|
|
def _open_note_detail(idx):
|
|||
|
|
if idx < 0 or idx >= len(user_notes):
|
|||
|
|
return
|
|||
|
|
note = user_notes[idx]
|
|||
|
|
|
|||
|
|
nd = tk.Toplevel(win)
|
|||
|
|
nd.title("Notiz bearbeiten")
|
|||
|
|
nd.attributes("-topmost", True)
|
|||
|
|
nd.configure(bg="#E8F4FA")
|
|||
|
|
nd.resizable(True, True)
|
|||
|
|
nd.geometry("460x420")
|
|||
|
|
center_window(nd, 460, 420)
|
|||
|
|
self._register_window(nd)
|
|||
|
|
|
|||
|
|
tk.Label(nd, text="📝 Notiz bearbeiten", font=("Segoe UI", 12, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
|
|||
|
|
|
|||
|
|
nd_content = tk.Frame(nd, bg="#E8F4FA", padx=12, pady=8)
|
|||
|
|
nd_content.pack(fill="both", expand=True)
|
|||
|
|
|
|||
|
|
nd_recorder = [None]
|
|||
|
|
nd_is_recording = [False]
|
|||
|
|
nd_target = [None]
|
|||
|
|
|
|||
|
|
def nd_on_close():
|
|||
|
|
if nd_is_recording[0] and nd_recorder[0]:
|
|||
|
|
try:
|
|||
|
|
nd_is_recording[0] = False
|
|||
|
|
wp = nd_recorder[0].stop_and_save_wav()
|
|||
|
|
if os.path.exists(wp):
|
|||
|
|
os.remove(wp)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
nd_recorder[0] = None
|
|||
|
|
nd.destroy()
|
|||
|
|
|
|||
|
|
nd.protocol("WM_DELETE_WINDOW", nd_on_close)
|
|||
|
|
|
|||
|
|
def _nd_toggle_record(target_widget, btn_ref, status_var):
|
|||
|
|
if not self.ensure_ready():
|
|||
|
|
return
|
|||
|
|
if not nd_is_recording[0]:
|
|||
|
|
rec = AudioRecorder()
|
|||
|
|
nd_recorder[0] = rec
|
|||
|
|
nd_target[0] = target_widget
|
|||
|
|
try:
|
|||
|
|
rec.start()
|
|||
|
|
nd_is_recording[0] = True
|
|||
|
|
btn_ref.configure(text="⏹", bg="#C03030", activebackground="#A02020")
|
|||
|
|
status_var.set("🔴 Aufnahme läuft…")
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("Aufnahme-Fehler", str(e), parent=nd)
|
|||
|
|
status_var.set("")
|
|||
|
|
else:
|
|||
|
|
nd_is_recording[0] = False
|
|||
|
|
rec = nd_recorder[0]
|
|||
|
|
btn_ref.configure(text="⏺", bg="#5B8DB3", activebackground="#4A7A9E")
|
|||
|
|
status_var.set("Transkribiere…")
|
|||
|
|
tw = nd_target[0]
|
|||
|
|
|
|||
|
|
def worker():
|
|||
|
|
try:
|
|||
|
|
wav_path = rec.stop_and_save_wav()
|
|||
|
|
import wave as _wave
|
|||
|
|
try:
|
|||
|
|
with _wave.open(wav_path, 'rb') as wf:
|
|||
|
|
dur = wf.getnframes() / float(wf.getframerate())
|
|||
|
|
if dur < 0.3:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
nd_recorder[0] = None
|
|||
|
|
self.after(0, lambda: status_var.set("Kein Audio erkannt."))
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
transcript_result = self.transcribe_wav(wav_path)
|
|||
|
|
if hasattr(transcript_result, 'text'):
|
|||
|
|
text = transcript_result.text
|
|||
|
|
elif isinstance(transcript_result, str):
|
|||
|
|
text = transcript_result
|
|||
|
|
else:
|
|||
|
|
text = ""
|
|||
|
|
try:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if not text or not text.strip():
|
|||
|
|
nd_recorder[0] = None
|
|||
|
|
self.after(0, lambda: status_var.set("Kein Text erkannt."))
|
|||
|
|
return
|
|||
|
|
text = self._diktat_apply_punctuation(text)
|
|||
|
|
def _done(t=text):
|
|||
|
|
nd_recorder[0] = None
|
|||
|
|
current = tw.get("1.0", "end").strip()
|
|||
|
|
new_text = (current + " " + t).strip() if current else t.strip()
|
|||
|
|
tw.delete("1.0", "end")
|
|||
|
|
tw.insert("1.0", new_text)
|
|||
|
|
status_var.set("✓ Diktiert.")
|
|||
|
|
self.after(0, _done)
|
|||
|
|
except Exception as e:
|
|||
|
|
self.after(0, lambda: status_var.set(f"Fehler: {e}"))
|
|||
|
|
nd_recorder[0] = None
|
|||
|
|
|
|||
|
|
threading.Thread(target=worker, daemon=True).start()
|
|||
|
|
|
|||
|
|
nd_rec_status = tk.StringVar(value="")
|
|||
|
|
|
|||
|
|
# Titel
|
|||
|
|
nd_title_row = tk.Frame(nd_content, bg="#E8F4FA")
|
|||
|
|
nd_title_row.pack(fill="x", pady=(0, 2))
|
|||
|
|
tk.Label(nd_title_row, text="Titel:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
|
|||
|
|
|
|||
|
|
nd_title_rec_btn = tk.Button(nd_title_row, text="⏺", font=("Segoe UI", 11),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
width=3, cursor="hand2")
|
|||
|
|
nd_title_rec_btn.pack(side="right")
|
|||
|
|
|
|||
|
|
nd_title_edit = tk.Text(nd_content, font=("Segoe UI", 11), height=1, bg="white",
|
|||
|
|
fg="#1a4d6d", relief="flat", bd=1, wrap="word")
|
|||
|
|
nd_title_edit.pack(fill="x")
|
|||
|
|
nd_title_edit.insert("1.0", note.get("title", ""))
|
|||
|
|
|
|||
|
|
nd_title_rec_btn.configure(
|
|||
|
|
command=lambda: _nd_toggle_record(nd_title_edit, nd_title_rec_btn, nd_rec_status))
|
|||
|
|
|
|||
|
|
# Text
|
|||
|
|
nd_text_row = tk.Frame(nd_content, bg="#E8F4FA")
|
|||
|
|
nd_text_row.pack(fill="x", pady=(8, 2))
|
|||
|
|
tk.Label(nd_text_row, text="Inhalt:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
|
|||
|
|
|
|||
|
|
nd_text_rec_btn = tk.Button(nd_text_row, text="⏺", font=("Segoe UI", 11),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
width=3, cursor="hand2")
|
|||
|
|
nd_text_rec_btn.pack(side="right")
|
|||
|
|
|
|||
|
|
nd_text_edit = tk.Text(nd_content, font=("Segoe UI", 10), height=8, bg="white",
|
|||
|
|
fg="#1a4d6d", relief="flat", bd=1, wrap="word")
|
|||
|
|
nd_text_edit.pack(fill="both", expand=True)
|
|||
|
|
add_text_font_size_control(nd_text_row, nd_text_edit, initial_size=10, bg_color="#E8F4FA", save_key="todo_note_detail")
|
|||
|
|
nd_text_edit.insert("1.0", note.get("text", ""))
|
|||
|
|
|
|||
|
|
nd_text_rec_btn.configure(
|
|||
|
|
command=lambda: _nd_toggle_record(nd_text_edit, nd_text_rec_btn, nd_rec_status))
|
|||
|
|
|
|||
|
|
tk.Label(nd_content, textvariable=nd_rec_status, font=("Segoe UI", 8),
|
|||
|
|
bg="#E8F4FA", fg="#5B8DB3").pack(fill="x", pady=(0, 4))
|
|||
|
|
|
|||
|
|
def _save_note():
|
|||
|
|
new_title = nd_title_edit.get("1.0", "end").strip()
|
|||
|
|
if not new_title:
|
|||
|
|
new_title = "Notiz"
|
|||
|
|
user_notes[idx]["title"] = new_title
|
|||
|
|
user_notes[idx]["text"] = nd_text_edit.get("1.0", "end").strip()
|
|||
|
|
save_notes(user_notes)
|
|||
|
|
nd.destroy()
|
|||
|
|
_rebuild_notes_list()
|
|||
|
|
|
|||
|
|
nd_btn_frame = tk.Frame(nd, bg="#D4EEF7", pady=8)
|
|||
|
|
nd_btn_frame.pack(fill="x", side="bottom")
|
|||
|
|
tk.Button(nd_btn_frame, text="💾 Speichern", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
relief="flat", bd=0, padx=20, pady=4, cursor="hand2",
|
|||
|
|
command=_save_note).pack(side="left", padx=12)
|
|||
|
|
tk.Button(nd_btn_frame, text="Abbrechen", font=("Segoe UI", 10),
|
|||
|
|
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
|
|||
|
|
relief="flat", bd=0, padx=12, pady=4, cursor="hand2",
|
|||
|
|
command=nd.destroy).pack(side="right", padx=12)
|
|||
|
|
|
|||
|
|
def _delete_note(idx):
|
|||
|
|
if 0 <= idx < len(user_notes):
|
|||
|
|
user_notes.pop(idx)
|
|||
|
|
save_notes(user_notes)
|
|||
|
|
_rebuild_notes_list()
|
|||
|
|
|
|||
|
|
_notes_drag = {"active": False, "idx": None, "indicator": None}
|
|||
|
|
_notes_border_frames = []
|
|||
|
|
|
|||
|
|
def _notes_drag_start(event, idx):
|
|||
|
|
_notes_drag["active"] = True
|
|||
|
|
_notes_drag["idx"] = idx
|
|||
|
|
event.widget.configure(cursor="fleur")
|
|||
|
|
if 0 <= idx < len(_notes_border_frames):
|
|||
|
|
_notes_border_frames[idx].configure(bg="#5B8DB3")
|
|||
|
|
|
|||
|
|
def _notes_drag_motion(event, idx):
|
|||
|
|
if not _notes_drag["active"]:
|
|||
|
|
return
|
|||
|
|
abs_y = event.widget.winfo_rooty() + event.y
|
|||
|
|
target = _notes_find_drop_target(abs_y)
|
|||
|
|
if _notes_drag.get("indicator"):
|
|||
|
|
_notes_drag["indicator"].destroy()
|
|||
|
|
_notes_drag["indicator"] = None
|
|||
|
|
if target is not None and target != _notes_drag["idx"]:
|
|||
|
|
insert_before = target
|
|||
|
|
if insert_before < len(_notes_border_frames):
|
|||
|
|
ind = tk.Frame(notes_list_frame, bg="#5B8DB3", height=3)
|
|||
|
|
ind.pack(before=_notes_border_frames[insert_before], fill="x", pady=0)
|
|||
|
|
else:
|
|||
|
|
ind = tk.Frame(notes_list_frame, bg="#5B8DB3", height=3)
|
|||
|
|
ind.pack(fill="x", pady=0)
|
|||
|
|
_notes_drag["indicator"] = ind
|
|||
|
|
|
|||
|
|
def _notes_drag_end(event, idx):
|
|||
|
|
if not _notes_drag["active"]:
|
|||
|
|
return
|
|||
|
|
_notes_drag["active"] = False
|
|||
|
|
if _notes_drag.get("indicator"):
|
|||
|
|
_notes_drag["indicator"].destroy()
|
|||
|
|
_notes_drag["indicator"] = None
|
|||
|
|
event.widget.configure(cursor="hand2")
|
|||
|
|
abs_y = event.widget.winfo_rooty() + event.y
|
|||
|
|
target = _notes_find_drop_target(abs_y)
|
|||
|
|
src = _notes_drag["idx"]
|
|||
|
|
if target is not None and target != src:
|
|||
|
|
item = user_notes.pop(src)
|
|||
|
|
if target > src:
|
|||
|
|
target -= 1
|
|||
|
|
user_notes.insert(target, item)
|
|||
|
|
save_notes(user_notes)
|
|||
|
|
_rebuild_notes_list()
|
|||
|
|
else:
|
|||
|
|
if 0 <= src < len(_notes_border_frames):
|
|||
|
|
_rebuild_notes_list()
|
|||
|
|
|
|||
|
|
def _notes_find_drop_target(abs_y):
|
|||
|
|
for i, bf in enumerate(_notes_border_frames):
|
|||
|
|
try:
|
|||
|
|
wy = bf.winfo_rooty()
|
|||
|
|
wh = bf.winfo_height()
|
|||
|
|
mid = wy + wh // 2
|
|||
|
|
if abs_y < mid:
|
|||
|
|
return i
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return len(_notes_border_frames)
|
|||
|
|
|
|||
|
|
def _rebuild_notes_list():
|
|||
|
|
for w in notes_list_frame.winfo_children():
|
|||
|
|
w.destroy()
|
|||
|
|
notes_widgets.clear()
|
|||
|
|
_notes_border_frames.clear()
|
|||
|
|
|
|||
|
|
if not user_notes:
|
|||
|
|
tk.Label(notes_list_frame, text="Noch keine Notizen vorhanden.\n"
|
|||
|
|
"Erstellen Sie eine neue Notiz oben.",
|
|||
|
|
font=("Segoe UI", 10), bg="#E8F4FA", fg="#999",
|
|||
|
|
justify="center").pack(pady=40)
|
|||
|
|
notes_counter_label.configure(text="0 Notizen")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
fs = todo_font_size[0]
|
|||
|
|
for idx, note in enumerate(user_notes):
|
|||
|
|
border_frame = tk.Frame(notes_list_frame, bg="#B0D8E8", padx=1, pady=1)
|
|||
|
|
border_frame.pack(fill="x", pady=2)
|
|||
|
|
_notes_border_frames.append(border_frame)
|
|||
|
|
|
|||
|
|
row = tk.Frame(border_frame, bg="white", padx=8, pady=8, cursor="hand2")
|
|||
|
|
row.pack(fill="x")
|
|||
|
|
|
|||
|
|
top_row = tk.Frame(row, bg="white")
|
|||
|
|
top_row.pack(fill="x")
|
|||
|
|
|
|||
|
|
drag_handle = tk.Label(top_row, text="≡", font=("Segoe UI", 12, "bold"),
|
|||
|
|
bg="white", fg="#B0C8D8", cursor="hand2")
|
|||
|
|
drag_handle.pack(side="left", padx=(0, 6))
|
|||
|
|
drag_handle.bind("<Button-1>", lambda e, i=idx: _notes_drag_start(e, i))
|
|||
|
|
drag_handle.bind("<B1-Motion>", lambda e, i=idx: _notes_drag_motion(e, i))
|
|||
|
|
drag_handle.bind("<ButtonRelease-1>", lambda e, i=idx: _notes_drag_end(e, i))
|
|||
|
|
drag_handle.bind("<Enter>", lambda e, h=drag_handle: h.configure(fg="#5B8DB3"))
|
|||
|
|
drag_handle.bind("<Leave>", lambda e, h=drag_handle: h.configure(fg="#B0C8D8"))
|
|||
|
|
|
|||
|
|
title_text = note.get("title", "Notiz")
|
|||
|
|
title_lbl = tk.Label(top_row, text=title_text, font=("Segoe UI", fs, "bold"),
|
|||
|
|
bg="white", fg="#1a4d6d", anchor="w",
|
|||
|
|
cursor="hand2", wraplength=320, justify="left")
|
|||
|
|
title_lbl.pack(side="left", fill="x", expand=True)
|
|||
|
|
|
|||
|
|
note_text = note.get("text", "").strip()
|
|||
|
|
preview_lbl = None
|
|||
|
|
if note_text:
|
|||
|
|
preview = note_text[:120] + ("\u2026" if len(note_text) > 120 else "")
|
|||
|
|
preview_lbl = tk.Label(row, text=preview, font=("Segoe UI", max(fs - 1, 8)),
|
|||
|
|
bg="white", fg="#5A8898", anchor="w",
|
|||
|
|
cursor="hand2", wraplength=350, justify="left")
|
|||
|
|
preview_lbl.pack(fill="x", anchor="w", pady=(2, 0))
|
|||
|
|
|
|||
|
|
bottom_row = tk.Frame(row, bg="white")
|
|||
|
|
bottom_row.pack(fill="x", pady=(4, 0))
|
|||
|
|
|
|||
|
|
created = note.get("created", "")
|
|||
|
|
try:
|
|||
|
|
dt_obj = datetime.fromisoformat(created)
|
|||
|
|
date_str = dt_obj.strftime("%d.%m.%Y %H:%M")
|
|||
|
|
except Exception:
|
|||
|
|
date_str = ""
|
|||
|
|
if date_str:
|
|||
|
|
tk.Label(bottom_row, text=date_str, font=("Segoe UI", 7),
|
|||
|
|
bg="white", fg="#B0C8D8").pack(side="left")
|
|||
|
|
|
|||
|
|
del_lbl = tk.Label(bottom_row, text="\u2715", font=("Segoe UI", 10),
|
|||
|
|
bg="white", fg="#ccc", cursor="hand2")
|
|||
|
|
del_lbl.pack(side="right")
|
|||
|
|
del_lbl.bind("<Enter>", lambda e, b=del_lbl: b.configure(fg="#E87070"))
|
|||
|
|
del_lbl.bind("<Leave>", lambda e, b=del_lbl: b.configure(fg="#ccc"))
|
|||
|
|
del_lbl.bind("<Button-1>", lambda e, i=idx: _delete_note(i))
|
|||
|
|
|
|||
|
|
for _w in [row, title_lbl]:
|
|||
|
|
_w.bind("<Button-1>", lambda e, i=idx: _open_note_detail(i))
|
|||
|
|
_w.bind("<Double-Button-1>", lambda e, i=idx: _open_note_detail(i))
|
|||
|
|
if preview_lbl:
|
|||
|
|
preview_lbl.bind("<Button-1>", lambda e, i=idx: _open_note_detail(i))
|
|||
|
|
preview_lbl.bind("<Double-Button-1>", lambda e, i=idx: _open_note_detail(i))
|
|||
|
|
|
|||
|
|
notes_widgets.append(row)
|
|||
|
|
|
|||
|
|
notes_counter_label.configure(text=f"{len(user_notes)} Notiz{'en' if len(user_notes) != 1 else ''}")
|
|||
|
|
_bind_notes_mousewheel_recursive(notes_list_frame)
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════
|
|||
|
|
# ─── Checkliste-Seite ───
|
|||
|
|
# ═══════════════════════════════════════════════════
|
|||
|
|
user_checklists = load_checklists()
|
|||
|
|
|
|||
|
|
cl_input_frame = tk.Frame(checklist_page, bg="#D4EEF7", pady=8, padx=8)
|
|||
|
|
cl_input_frame.pack(fill="x")
|
|||
|
|
tk.Label(cl_input_frame, text="Neue Checkliste:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#D4EEF7", fg="#1a4d6d").pack(anchor="w")
|
|||
|
|
cl_title_frame = tk.Frame(cl_input_frame, bg="#D4EEF7")
|
|||
|
|
cl_title_frame.pack(fill="x", pady=(2, 0))
|
|||
|
|
cl_title_var = tk.StringVar()
|
|||
|
|
cl_title_entry = tk.Entry(cl_title_frame, textvariable=cl_title_var,
|
|||
|
|
font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|||
|
|
relief="flat", bd=0, insertbackground="#1a4d6d")
|
|||
|
|
cl_title_entry.pack(side="left", fill="x", expand=True, ipady=4, padx=(0, 6))
|
|||
|
|
cl_title_entry.insert(0, "Titel der Checkliste…")
|
|||
|
|
cl_title_entry.configure(fg="#999")
|
|||
|
|
|
|||
|
|
def _cl_focus_in(e):
|
|||
|
|
if cl_title_entry.get() == "Titel der Checkliste…":
|
|||
|
|
cl_title_entry.delete(0, "end")
|
|||
|
|
cl_title_entry.configure(fg="#1a4d6d")
|
|||
|
|
def _cl_focus_out(e):
|
|||
|
|
if not cl_title_entry.get().strip():
|
|||
|
|
cl_title_entry.insert(0, "Titel der Checkliste…")
|
|||
|
|
cl_title_entry.configure(fg="#999")
|
|||
|
|
cl_title_entry.bind("<FocusIn>", _cl_focus_in)
|
|||
|
|
cl_title_entry.bind("<FocusOut>", _cl_focus_out)
|
|||
|
|
|
|||
|
|
def _add_checklist(event=None):
|
|||
|
|
title = cl_title_var.get().strip()
|
|||
|
|
if not title or title == "Titel der Checkliste…":
|
|||
|
|
return
|
|||
|
|
new_cl = {
|
|||
|
|
"id": int(datetime.now().timestamp() * 1000),
|
|||
|
|
"title": title,
|
|||
|
|
"items": [],
|
|||
|
|
"created": datetime.now().isoformat(),
|
|||
|
|
}
|
|||
|
|
user_checklists.append(new_cl)
|
|||
|
|
save_checklists(user_checklists)
|
|||
|
|
cl_title_var.set("")
|
|||
|
|
cl_title_entry.delete(0, "end")
|
|||
|
|
cl_title_entry.insert(0, "Titel der Checkliste…")
|
|||
|
|
cl_title_entry.configure(fg="#999")
|
|||
|
|
_rebuild_checklist()
|
|||
|
|
|
|||
|
|
cl_title_entry.bind("<Return>", _add_checklist)
|
|||
|
|
tk.Button(cl_title_frame, text="+", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
relief="flat", bd=0, padx=8, cursor="hand2",
|
|||
|
|
command=_add_checklist).pack(side="right")
|
|||
|
|
|
|||
|
|
cl_list_outer = tk.Frame(checklist_page, bg="#E8F4FA")
|
|||
|
|
cl_list_outer.pack(fill="both", expand=True, padx=8, pady=(4, 0))
|
|||
|
|
cl_canvas = tk.Canvas(cl_list_outer, bg="#E8F4FA", highlightthickness=0)
|
|||
|
|
cl_sb = ttk.Scrollbar(cl_list_outer, orient="vertical", command=cl_canvas.yview)
|
|||
|
|
cl_list_frame = tk.Frame(cl_canvas, bg="#E8F4FA")
|
|||
|
|
cl_list_frame.bind("<Configure>", lambda e: cl_canvas.configure(scrollregion=cl_canvas.bbox("all")))
|
|||
|
|
cl_canvas.create_window((0, 0), window=cl_list_frame, anchor="nw", tags="cl_win")
|
|||
|
|
cl_canvas.configure(yscrollcommand=cl_sb.set)
|
|||
|
|
cl_canvas.bind("<Configure>", lambda e: cl_canvas.itemconfig("cl_win", width=e.width))
|
|||
|
|
cl_canvas.pack(side="left", fill="both", expand=True)
|
|||
|
|
cl_sb.pack(side="right", fill="y")
|
|||
|
|
def _cl_mw(event):
|
|||
|
|
cl_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|||
|
|
cl_canvas.bind("<MouseWheel>", _cl_mw)
|
|||
|
|
|
|||
|
|
cl_footer = tk.Frame(checklist_page, bg="#D4EEF7", pady=4)
|
|||
|
|
cl_footer.pack(fill="x", side="bottom")
|
|||
|
|
cl_counter = tk.Label(cl_footer, text="", font=("Segoe UI", 9),
|
|||
|
|
bg="#D4EEF7", fg="#4a8aaa")
|
|||
|
|
cl_counter.pack(side="left", padx=8)
|
|||
|
|
|
|||
|
|
def _rebuild_checklist():
|
|||
|
|
for w in cl_list_frame.winfo_children():
|
|||
|
|
w.destroy()
|
|||
|
|
|
|||
|
|
if not user_checklists:
|
|||
|
|
tk.Label(cl_list_frame, text="Noch keine Checklisten vorhanden.\n"
|
|||
|
|
"Erstellen Sie eine neue Checkliste oben.",
|
|||
|
|
font=("Segoe UI", 10), bg="#E8F4FA", fg="#999",
|
|||
|
|
justify="center").pack(pady=40)
|
|||
|
|
cl_counter.configure(text="0 Checklisten")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
for cl_idx, cl in enumerate(user_checklists):
|
|||
|
|
border = tk.Frame(cl_list_frame, bg="#D0E8F0", padx=1, pady=1)
|
|||
|
|
border.pack(fill="x", pady=2)
|
|||
|
|
card = tk.Frame(border, bg="white", padx=8, pady=6)
|
|||
|
|
card.pack(fill="x")
|
|||
|
|
|
|||
|
|
# Header: Titel + Löschen
|
|||
|
|
hdr = tk.Frame(card, bg="white")
|
|||
|
|
hdr.pack(fill="x")
|
|||
|
|
tk.Label(hdr, text=cl.get("title", "Checkliste"),
|
|||
|
|
font=("Segoe UI", 10, "bold"), bg="white", fg="#1a4d6d",
|
|||
|
|
anchor="w").pack(side="left", fill="x", expand=True)
|
|||
|
|
|
|||
|
|
items_done = sum(1 for it in cl.get("items", []) if it.get("done"))
|
|||
|
|
items_total = len(cl.get("items", []))
|
|||
|
|
progress_text = f"{items_done}/{items_total}" if items_total else "leer"
|
|||
|
|
tk.Label(hdr, text=progress_text, font=("Segoe UI", 8),
|
|||
|
|
bg="white", fg="#5B8DB3").pack(side="right", padx=(4, 0))
|
|||
|
|
|
|||
|
|
del_btn = tk.Label(hdr, text="✕", font=("Segoe UI", 9),
|
|||
|
|
bg="white", fg="#ccc", cursor="hand2")
|
|||
|
|
del_btn.pack(side="right", padx=(8, 0))
|
|||
|
|
del_btn.bind("<Enter>", lambda e, b=del_btn: b.configure(fg="#E87070"))
|
|||
|
|
del_btn.bind("<Leave>", lambda e, b=del_btn: b.configure(fg="#ccc"))
|
|||
|
|
def _del_cl(i=cl_idx):
|
|||
|
|
user_checklists.pop(i)
|
|||
|
|
save_checklists(user_checklists)
|
|||
|
|
_rebuild_checklist()
|
|||
|
|
del_btn.bind("<Button-1>", lambda e, i=cl_idx: _del_cl(i))
|
|||
|
|
|
|||
|
|
# Items
|
|||
|
|
items = cl.get("items", [])
|
|||
|
|
for it_idx, item in enumerate(items):
|
|||
|
|
it_frame = tk.Frame(card, bg="white")
|
|||
|
|
it_frame.pack(fill="x", pady=1)
|
|||
|
|
var = tk.BooleanVar(value=item.get("done", False))
|
|||
|
|
def _toggle_item(ci=cl_idx, ii=it_idx, v=var):
|
|||
|
|
user_checklists[ci]["items"][ii]["done"] = v.get()
|
|||
|
|
save_checklists(user_checklists)
|
|||
|
|
_rebuild_checklist()
|
|||
|
|
cb = tk.Checkbutton(it_frame, variable=var, command=lambda ci=cl_idx, ii=it_idx, v=var: _toggle_item(ci, ii, v),
|
|||
|
|
bg="white", activebackground="white")
|
|||
|
|
cb.pack(side="left")
|
|||
|
|
txt = item.get("text", "")
|
|||
|
|
fg_c = "#999" if item.get("done") else "#1a4d6d"
|
|||
|
|
font_c = ("Segoe UI", 9, "overstrike") if item.get("done") else ("Segoe UI", 9)
|
|||
|
|
tk.Label(it_frame, text=txt, font=font_c, bg="white", fg=fg_c,
|
|||
|
|
anchor="w").pack(side="left", fill="x", expand=True)
|
|||
|
|
|
|||
|
|
it_del = tk.Label(it_frame, text="✕", font=("Segoe UI", 7),
|
|||
|
|
bg="white", fg="#ddd", cursor="hand2")
|
|||
|
|
it_del.pack(side="right")
|
|||
|
|
it_del.bind("<Enter>", lambda e, b=it_del: b.configure(fg="#E87070"))
|
|||
|
|
it_del.bind("<Leave>", lambda e, b=it_del: b.configure(fg="#ddd"))
|
|||
|
|
def _del_item(ci=cl_idx, ii=it_idx):
|
|||
|
|
user_checklists[ci]["items"].pop(ii)
|
|||
|
|
save_checklists(user_checklists)
|
|||
|
|
_rebuild_checklist()
|
|||
|
|
it_del.bind("<Button-1>", lambda e, ci=cl_idx, ii=it_idx: _del_item(ci, ii))
|
|||
|
|
|
|||
|
|
# Neues Item hinzufügen
|
|||
|
|
add_frame = tk.Frame(card, bg="white")
|
|||
|
|
add_frame.pack(fill="x", pady=(4, 0))
|
|||
|
|
add_var = tk.StringVar()
|
|||
|
|
add_entry = tk.Entry(add_frame, textvariable=add_var, font=("Segoe UI", 9),
|
|||
|
|
bg="#F5FCFF", fg="#1a4d6d", relief="flat", bd=1,
|
|||
|
|
insertbackground="#1a4d6d")
|
|||
|
|
add_entry.pack(side="left", fill="x", expand=True, ipady=2, padx=(0, 4))
|
|||
|
|
add_entry.insert(0, "Neuer Punkt…")
|
|||
|
|
add_entry.configure(fg="#999")
|
|||
|
|
add_entry.bind("<FocusIn>", lambda e, en=add_entry: (en.delete(0, "end"), en.configure(fg="#1a4d6d")) if en.get() == "Neuer Punkt…" else None)
|
|||
|
|
add_entry.bind("<FocusOut>", lambda e, en=add_entry: (en.insert(0, "Neuer Punkt…"), en.configure(fg="#999")) if not en.get().strip() else None)
|
|||
|
|
def _add_item(ci=cl_idx, v=add_var, en=add_entry):
|
|||
|
|
txt = v.get().strip()
|
|||
|
|
if not txt or txt == "Neuer Punkt…":
|
|||
|
|
return
|
|||
|
|
user_checklists[ci]["items"].append({"text": txt, "done": False})
|
|||
|
|
save_checklists(user_checklists)
|
|||
|
|
_rebuild_checklist()
|
|||
|
|
add_entry.bind("<Return>", lambda e, ci=cl_idx, v=add_var, en=add_entry: _add_item(ci, v, en))
|
|||
|
|
tk.Button(add_frame, text="+", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#7EC8E3", fg="white", activebackground="#6CB8D3",
|
|||
|
|
relief="flat", bd=0, padx=4, cursor="hand2",
|
|||
|
|
command=lambda ci=cl_idx, v=add_var, en=add_entry: _add_item(ci, v, en)).pack(side="right")
|
|||
|
|
|
|||
|
|
cl_counter.configure(text=f"{len(user_checklists)} Checkliste{'n' if len(user_checklists) != 1 else ''}")
|
|||
|
|
def _cl_mw_recursive(widget):
|
|||
|
|
widget.bind("<MouseWheel>", _cl_mw)
|
|||
|
|
for child in widget.winfo_children():
|
|||
|
|
_cl_mw_recursive(child)
|
|||
|
|
_cl_mw_recursive(cl_list_frame)
|
|||
|
|
|
|||
|
|
# ─── Detail-Fenster ───
|
|||
|
|
def open_detail(idx):
|
|||
|
|
if idx < 0 or idx >= len(todos):
|
|||
|
|
return
|
|||
|
|
todo = todos[idx]
|
|||
|
|
|
|||
|
|
det = tk.Toplevel(win)
|
|||
|
|
det.title("Aufgabe bearbeiten")
|
|||
|
|
det.attributes("-topmost", True)
|
|||
|
|
det.configure(bg="#E8F4FA")
|
|||
|
|
det.resizable(True, True)
|
|||
|
|
det.geometry("440x500")
|
|||
|
|
center_window(det, 440, 500)
|
|||
|
|
self._register_window(det)
|
|||
|
|
|
|||
|
|
# Header
|
|||
|
|
tk.Label(det, text="✏ Aufgabe bearbeiten", font=("Segoe UI", 12, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
|
|||
|
|
|
|||
|
|
content = tk.Frame(det, bg="#E8F4FA", padx=12, pady=8)
|
|||
|
|
content.pack(fill="both", expand=True)
|
|||
|
|
|
|||
|
|
det_recorder = [None]
|
|||
|
|
det_is_recording = [False]
|
|||
|
|
det_target_widget = [None] # welches Text-Widget gerade diktiert wird
|
|||
|
|
|
|||
|
|
def det_on_close():
|
|||
|
|
if det_is_recording[0] and det_recorder[0]:
|
|||
|
|
try:
|
|||
|
|
det_is_recording[0] = False
|
|||
|
|
wp = det_recorder[0].stop_and_save_wav()
|
|||
|
|
if os.path.exists(wp):
|
|||
|
|
os.remove(wp)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
det_recorder[0] = None
|
|||
|
|
det.destroy()
|
|||
|
|
|
|||
|
|
det.protocol("WM_DELETE_WINDOW", det_on_close)
|
|||
|
|
|
|||
|
|
def _det_toggle_record(target_text_widget, rec_btn_ref, status_var):
|
|||
|
|
if not self.ensure_ready():
|
|||
|
|
return
|
|||
|
|
if not det_is_recording[0]:
|
|||
|
|
rec = AudioRecorder()
|
|||
|
|
det_recorder[0] = rec
|
|||
|
|
det_target_widget[0] = target_text_widget
|
|||
|
|
try:
|
|||
|
|
rec.start()
|
|||
|
|
det_is_recording[0] = True
|
|||
|
|
rec_btn_ref.configure(text="⏹", bg="#C03030", activebackground="#A02020")
|
|||
|
|
status_var.set("🔴 Aufnahme läuft…")
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("Aufnahme-Fehler", str(e), parent=det)
|
|||
|
|
status_var.set("")
|
|||
|
|
else:
|
|||
|
|
det_is_recording[0] = False
|
|||
|
|
rec = det_recorder[0]
|
|||
|
|
rec_btn_ref.configure(text="⏺", bg="#5B8DB3", activebackground="#4A7A9E")
|
|||
|
|
status_var.set("Transkribiere…")
|
|||
|
|
|
|||
|
|
tw_ref = det_target_widget[0]
|
|||
|
|
|
|||
|
|
def worker():
|
|||
|
|
try:
|
|||
|
|
wav_path = rec.stop_and_save_wav()
|
|||
|
|
import wave as _wave
|
|||
|
|
try:
|
|||
|
|
with _wave.open(wav_path, 'rb') as wf:
|
|||
|
|
dur = wf.getnframes() / float(wf.getframerate())
|
|||
|
|
if dur < 0.3:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
det_recorder[0] = None
|
|||
|
|
self.after(0, lambda: status_var.set("Kein Audio erkannt."))
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
transcript_result = self.transcribe_wav(wav_path)
|
|||
|
|
if hasattr(transcript_result, 'text'):
|
|||
|
|
text = transcript_result.text
|
|||
|
|
elif isinstance(transcript_result, str):
|
|||
|
|
text = transcript_result
|
|||
|
|
else:
|
|||
|
|
text = ""
|
|||
|
|
try:
|
|||
|
|
if os.path.exists(wav_path):
|
|||
|
|
os.remove(wav_path)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if not text or not text.strip():
|
|||
|
|
det_recorder[0] = None
|
|||
|
|
self.after(0, lambda: status_var.set("Kein Text erkannt."))
|
|||
|
|
return
|
|||
|
|
text = self._diktat_apply_punctuation(text)
|
|||
|
|
cleaned_text, extracted_date = extract_date_from_todo_text(text)
|
|||
|
|
def _done(ct=cleaned_text, ed=extracted_date):
|
|||
|
|
det_recorder[0] = None
|
|||
|
|
current = tw_ref.get("1.0", "end").strip()
|
|||
|
|
new_text = (current + " " + ct).strip() if current else ct.strip()
|
|||
|
|
tw_ref.delete("1.0", "end")
|
|||
|
|
tw_ref.insert("1.0", new_text)
|
|||
|
|
if ed:
|
|||
|
|
det_set_date(ed)
|
|||
|
|
status_var.set(f"✓ Datum erkannt: {ed.strftime('%d.%m.%Y')}")
|
|||
|
|
else:
|
|||
|
|
status_var.set("✓ Diktiert.")
|
|||
|
|
self.after(0, _done)
|
|||
|
|
except Exception as e:
|
|||
|
|
self.after(0, lambda: status_var.set(f"Fehler: {e}"))
|
|||
|
|
det_recorder[0] = None
|
|||
|
|
|
|||
|
|
threading.Thread(target=worker, daemon=True).start()
|
|||
|
|
|
|||
|
|
# Aufgabentext
|
|||
|
|
text_label_row = tk.Frame(content, bg="#E8F4FA")
|
|||
|
|
text_label_row.pack(fill="x", pady=(0, 2))
|
|||
|
|
tk.Label(text_label_row, text="Aufgabe:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
|
|||
|
|
|
|||
|
|
text_rec_status = tk.StringVar(value="")
|
|||
|
|
text_rec_btn = tk.Button(text_label_row, text="⏺", font=("Segoe UI", 11),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
width=3, cursor="hand2")
|
|||
|
|
text_rec_btn.pack(side="right")
|
|||
|
|
text_rec_btn.configure(
|
|||
|
|
command=lambda: _det_toggle_record(text_edit, text_rec_btn, text_rec_status))
|
|||
|
|
|
|||
|
|
text_edit = tk.Text(content, font=("Segoe UI", 11), height=3, bg="white",
|
|||
|
|
fg="#1a4d6d", relief="flat", bd=1, wrap="word")
|
|||
|
|
text_edit.pack(fill="x")
|
|||
|
|
text_edit.insert("1.0", todo.get("text", ""))
|
|||
|
|
|
|||
|
|
tk.Label(content, textvariable=text_rec_status, font=("Segoe UI", 8),
|
|||
|
|
bg="#E8F4FA", fg="#5B8DB3").pack(fill="x", pady=(0, 6))
|
|||
|
|
|
|||
|
|
# Notizen
|
|||
|
|
notes_label_row = tk.Frame(content, bg="#E8F4FA")
|
|||
|
|
notes_label_row.pack(fill="x", pady=(0, 2))
|
|||
|
|
tk.Label(notes_label_row, text="Notizen:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
|
|||
|
|
|
|||
|
|
notes_rec_status = tk.StringVar(value="")
|
|||
|
|
notes_rec_btn = tk.Button(notes_label_row, text="⏺", font=("Segoe UI", 11),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
activeforeground="white", relief="flat", bd=0,
|
|||
|
|
width=3, cursor="hand2")
|
|||
|
|
notes_rec_btn.pack(side="right")
|
|||
|
|
notes_rec_btn.configure(
|
|||
|
|
command=lambda: _det_toggle_record(notes_edit, notes_rec_btn, notes_rec_status))
|
|||
|
|
|
|||
|
|
notes_edit = tk.Text(content, font=("Segoe UI", 10), height=4, bg="white",
|
|||
|
|
fg="#1a4d6d", relief="flat", bd=1, wrap="word")
|
|||
|
|
notes_edit.pack(fill="x")
|
|||
|
|
notes_edit.insert("1.0", todo.get("notes", ""))
|
|||
|
|
|
|||
|
|
tk.Label(content, textvariable=notes_rec_status, font=("Segoe UI", 8),
|
|||
|
|
bg="#E8F4FA", fg="#5B8DB3").pack(fill="x", pady=(0, 6))
|
|||
|
|
|
|||
|
|
# Datum
|
|||
|
|
det_date = [None]
|
|||
|
|
if todo.get("date"):
|
|||
|
|
try:
|
|||
|
|
det_date[0] = datetime.strptime(todo["date"], "%Y-%m-%d").date()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
date_row = tk.Frame(content, bg="#E8F4FA")
|
|||
|
|
date_row.pack(fill="x", pady=(0, 8))
|
|||
|
|
tk.Label(date_row, text="Fällig:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
|
|||
|
|
|
|||
|
|
det_date_lbl = tk.Label(date_row, text="kein Datum", font=("Segoe UI", 10),
|
|||
|
|
bg="#E8F4FA", fg="#999")
|
|||
|
|
det_date_lbl.pack(side="left", padx=(4, 4))
|
|||
|
|
|
|||
|
|
def det_set_date(d):
|
|||
|
|
det_date[0] = d
|
|||
|
|
if d:
|
|||
|
|
det_date_lbl.configure(text=d.strftime("%d.%m.%Y"), fg="#1a4d6d")
|
|||
|
|
else:
|
|||
|
|
det_date_lbl.configure(text="kein Datum", fg="#999")
|
|||
|
|
|
|||
|
|
if det_date[0]:
|
|||
|
|
det_set_date(det_date[0])
|
|||
|
|
|
|||
|
|
def det_open_cal():
|
|||
|
|
dcal = tk.Toplevel(det)
|
|||
|
|
dcal.title("Datum wählen")
|
|||
|
|
dcal.attributes("-topmost", True)
|
|||
|
|
dcal.configure(bg="#E8F4FA")
|
|||
|
|
dcal.resizable(False, False)
|
|||
|
|
self._register_window(dcal)
|
|||
|
|
t = date.today()
|
|||
|
|
cur = [det_date[0].year if det_date[0] else t.year,
|
|||
|
|
det_date[0].month if det_date[0] else t.month]
|
|||
|
|
|
|||
|
|
nav2 = tk.Frame(dcal, bg="#B9ECFA")
|
|||
|
|
nav2.pack(fill="x")
|
|||
|
|
ml2 = tk.Label(nav2, text="", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#B9ECFA", fg="#1a4d6d")
|
|||
|
|
df2 = tk.Frame(dcal, bg="#E8F4FA", padx=4, pady=4)
|
|||
|
|
df2.pack(fill="both")
|
|||
|
|
|
|||
|
|
def upd2():
|
|||
|
|
ml2.configure(text=f"{cal_mod.month_name[cur[1]]} {cur[0]}")
|
|||
|
|
for w2 in df2.winfo_children():
|
|||
|
|
w2.destroy()
|
|||
|
|
for c2, wd2 in enumerate(["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]):
|
|||
|
|
fg2 = "#C03030" if c2 >= 5 else "#4a8aaa"
|
|||
|
|
tk.Label(df2, text=wd2, font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg=fg2, width=4).grid(row=0, column=c2)
|
|||
|
|
fw, nd = cal_mod.monthrange(cur[0], cur[1])
|
|||
|
|
r2, c2 = 1, fw
|
|||
|
|
for dy in range(1, nd + 1):
|
|||
|
|
dd = date(cur[0], cur[1], dy)
|
|||
|
|
sel = (dd == det_date[0])
|
|||
|
|
bg2 = "#5B8DB3" if sel else ("#D4EEF7" if dd == t else "#F5FCFF")
|
|||
|
|
fg2 = "white" if sel else ("#999" if dd < t else "#1a4d6d")
|
|||
|
|
lb2 = tk.Label(df2, text=str(dy), font=("Segoe UI", 10),
|
|||
|
|
bg=bg2, fg=fg2, width=4, cursor="hand2")
|
|||
|
|
lb2.grid(row=r2, column=c2, padx=1, pady=1)
|
|||
|
|
lb2.bind("<Button-1>", lambda e, d2=dd: (det_set_date(d2), dcal.destroy()))
|
|||
|
|
c2 += 1
|
|||
|
|
if c2 > 6:
|
|||
|
|
c2 = 0
|
|||
|
|
r2 += 1
|
|||
|
|
|
|||
|
|
def pm2():
|
|||
|
|
cur[1] -= 1
|
|||
|
|
if cur[1] < 1:
|
|||
|
|
cur[1] = 12
|
|||
|
|
cur[0] -= 1
|
|||
|
|
upd2()
|
|||
|
|
|
|||
|
|
def nm2():
|
|||
|
|
cur[1] += 1
|
|||
|
|
if cur[1] > 12:
|
|||
|
|
cur[1] = 1
|
|||
|
|
cur[0] += 1
|
|||
|
|
upd2()
|
|||
|
|
|
|||
|
|
tk.Label(nav2, text="◀", font=("Segoe UI", 12), bg="#B9ECFA",
|
|||
|
|
fg="#1a4d6d", cursor="hand2", padx=8).pack(side="left")
|
|||
|
|
nav2.winfo_children()[-1].bind("<Button-1>", lambda e: pm2())
|
|||
|
|
ml2.pack(side="left", fill="x", expand=True)
|
|||
|
|
tk.Label(nav2, text="▶", font=("Segoe UI", 12), bg="#B9ECFA",
|
|||
|
|
fg="#1a4d6d", cursor="hand2", padx=8).pack(side="right")
|
|||
|
|
nav2.winfo_children()[-1].bind("<Button-1>", lambda e: nm2())
|
|||
|
|
|
|||
|
|
q2 = tk.Frame(dcal, bg="#D4EEF7", pady=4)
|
|||
|
|
q2.pack(fill="x")
|
|||
|
|
for lt, dl in [("Heute", 0), ("Morgen", 1), ("+1 Wo", 7)]:
|
|||
|
|
dd = t + timedelta(days=dl)
|
|||
|
|
tk.Button(q2, text=lt, font=("Segoe UI", 8), bg="#C8DDE6", fg="#1a4d6d",
|
|||
|
|
relief="flat", bd=0, padx=6, cursor="hand2",
|
|||
|
|
command=lambda d2=dd: (det_set_date(d2), dcal.destroy())).pack(side="left", padx=2)
|
|||
|
|
tk.Button(q2, text="Kein Datum", font=("Segoe UI", 8), bg="#E0E0E0", fg="#666",
|
|||
|
|
relief="flat", bd=0, padx=6, cursor="hand2",
|
|||
|
|
command=lambda: (det_set_date(None), dcal.destroy())).pack(side="right", padx=2)
|
|||
|
|
upd2()
|
|||
|
|
dcal.update_idletasks()
|
|||
|
|
dcal.geometry(f"+{det.winfo_x()+30}+{det.winfo_y()+100}")
|
|||
|
|
|
|||
|
|
tk.Button(date_row, text="📅", font=("Segoe UI", 12), bg="#E8F4FA", fg="#1a4d6d",
|
|||
|
|
relief="flat", bd=0, cursor="hand2", command=det_open_cal).pack(side="left")
|
|||
|
|
|
|||
|
|
# Priorität
|
|||
|
|
prio_row = tk.Frame(content, bg="#E8F4FA")
|
|||
|
|
prio_row.pack(fill="x", pady=(0, 8))
|
|||
|
|
tk.Label(prio_row, text="Priorität:", font=("Segoe UI", 9, "bold"),
|
|||
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
|
|||
|
|
|
|||
|
|
prio_var = tk.IntVar(value=todo.get("priority", 0))
|
|||
|
|
|
|||
|
|
prio_colors = {0: ("#E8F4FA", "#1a4d6d"), 1: ("#F8D0D0", "#A03030"), 2: ("#F8ECC8", "#806000")}
|
|||
|
|
prio_labels = {0: "Keine", 1: "🔴 Hoch", 2: "🟡 Mittel"}
|
|||
|
|
prio_btns = {}
|
|||
|
|
|
|||
|
|
def set_prio(p):
|
|||
|
|
prio_var.set(p)
|
|||
|
|
for pp, b in prio_btns.items():
|
|||
|
|
bg_c, fg_c = prio_colors[pp]
|
|||
|
|
if pp == p:
|
|||
|
|
b.configure(relief="solid", bd=2)
|
|||
|
|
else:
|
|||
|
|
b.configure(relief="flat", bd=1)
|
|||
|
|
|
|||
|
|
for p_val in [1, 2, 0]:
|
|||
|
|
bg_c, fg_c = prio_colors[p_val]
|
|||
|
|
b = tk.Button(prio_row, text=prio_labels[p_val], font=("Segoe UI", 9),
|
|||
|
|
bg=bg_c, fg=fg_c, activebackground=bg_c,
|
|||
|
|
relief="solid" if p_val == prio_var.get() else "flat",
|
|||
|
|
bd=2 if p_val == prio_var.get() else 1,
|
|||
|
|
padx=8, pady=1, cursor="hand2",
|
|||
|
|
command=lambda pv=p_val: set_prio(pv))
|
|||
|
|
b.pack(side="left", padx=(6, 0))
|
|||
|
|
prio_btns[p_val] = b
|
|||
|
|
|
|||
|
|
# Speichern
|
|||
|
|
def save_detail():
|
|||
|
|
new_text = text_edit.get("1.0", "end").strip()
|
|||
|
|
if not new_text:
|
|||
|
|
messagebox.showwarning("Fehler", "Aufgabentext darf nicht leer sein.", parent=det)
|
|||
|
|
return
|
|||
|
|
todos[idx]["text"] = new_text
|
|||
|
|
todos[idx]["notes"] = notes_edit.get("1.0", "end").strip()
|
|||
|
|
todos[idx]["date"] = det_date[0].isoformat() if det_date[0] else None
|
|||
|
|
todos[idx]["priority"] = prio_var.get()
|
|||
|
|
det.destroy()
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
btn_frame = tk.Frame(det, bg="#D4EEF7", pady=8)
|
|||
|
|
btn_frame.pack(fill="x", side="bottom")
|
|||
|
|
tk.Button(btn_frame, text="💾 Speichern", font=("Segoe UI", 11, "bold"),
|
|||
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|||
|
|
relief="flat", bd=0, padx=20, pady=4, cursor="hand2",
|
|||
|
|
command=save_detail).pack(side="left", padx=12)
|
|||
|
|
tk.Button(btn_frame, text="Abbrechen", font=("Segoe UI", 10),
|
|||
|
|
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
|
|||
|
|
relief="flat", bd=0, padx=12, pady=4, cursor="hand2",
|
|||
|
|
command=det.destroy).pack(side="right", padx=12)
|
|||
|
|
|
|||
|
|
# ─── Logik ───
|
|||
|
|
def update_counter():
|
|||
|
|
done = sum(1 for t in todos if t.get("done"))
|
|||
|
|
total = len(todos)
|
|||
|
|
overdue = 0
|
|||
|
|
today_str = date.today().isoformat()
|
|||
|
|
for t in todos:
|
|||
|
|
if not t.get("done") and t.get("date") and t["date"] < today_str:
|
|||
|
|
overdue += 1
|
|||
|
|
text = f"{done}/{total} erledigt"
|
|||
|
|
if overdue:
|
|||
|
|
text += f" · {overdue} überfällig"
|
|||
|
|
counter_label.configure(text=text)
|
|||
|
|
|
|||
|
|
# ─── Rechtsklick-Kontextmenü ───
|
|||
|
|
ctx_menu = tk.Menu(win, tearoff=0, font=("Segoe UI", 9))
|
|||
|
|
_ctx_idx = [None]
|
|||
|
|
|
|||
|
|
def _ctx_set_prio(p):
|
|||
|
|
i = _ctx_idx[0]
|
|||
|
|
if i is not None and 0 <= i < len(todos):
|
|||
|
|
todos[i]["priority"] = p
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
def _ctx_send_medwork():
|
|||
|
|
i = _ctx_idx[0]
|
|||
|
|
if i is not None and 0 <= i < len(todos):
|
|||
|
|
_send_items_to_medwork([todos[i]])
|
|||
|
|
|
|||
|
|
def _ctx_send_email():
|
|||
|
|
i = _ctx_idx[0]
|
|||
|
|
if i is not None and 0 <= i < len(todos):
|
|||
|
|
_send_items_via_email([todos[i]])
|
|||
|
|
|
|||
|
|
def _show_context_menu(event, idx):
|
|||
|
|
_ctx_idx[0] = idx
|
|||
|
|
ctx_menu.delete(0, "end")
|
|||
|
|
prio_menu = tk.Menu(ctx_menu, tearoff=0, font=("Segoe UI", 9))
|
|||
|
|
prio_menu.add_command(label="🔴 Hoch (1)", command=lambda: _ctx_set_prio(1))
|
|||
|
|
prio_menu.add_command(label="🟡 Mittel (2)", command=lambda: _ctx_set_prio(2))
|
|||
|
|
prio_menu.add_command(label="Keine", command=lambda: _ctx_set_prio(0))
|
|||
|
|
ctx_menu.add_cascade(label="Priorität setzen", menu=prio_menu)
|
|||
|
|
ctx_menu.add_separator()
|
|||
|
|
ctx_menu.add_command(label="📤 An MedWork senden", command=_ctx_send_medwork)
|
|||
|
|
ctx_menu.add_command(label="✉ Per E-Mail senden", command=_ctx_send_email)
|
|||
|
|
ctx_menu.tk_popup(event.x_root, event.y_root)
|
|||
|
|
|
|||
|
|
# ─── Drag-and-Drop State ───
|
|||
|
|
_drag = {"active": False, "idx": None, "indicator": None}
|
|||
|
|
_border_frames = []
|
|||
|
|
|
|||
|
|
def _drag_start(event, idx):
|
|||
|
|
_drag["active"] = True
|
|||
|
|
_drag["idx"] = idx
|
|||
|
|
event.widget.configure(cursor="fleur")
|
|||
|
|
if 0 <= idx < len(_border_frames):
|
|||
|
|
_border_frames[idx].configure(bg="#5B8DB3")
|
|||
|
|
|
|||
|
|
def _drag_motion(event, idx):
|
|||
|
|
if not _drag["active"]:
|
|||
|
|
return
|
|||
|
|
abs_y = event.widget.winfo_rooty() + event.y
|
|||
|
|
target = _find_drop_target(abs_y)
|
|||
|
|
if _drag.get("indicator"):
|
|||
|
|
_drag["indicator"].destroy()
|
|||
|
|
_drag["indicator"] = None
|
|||
|
|
if target is not None and target != _drag["idx"]:
|
|||
|
|
insert_before = target
|
|||
|
|
if insert_before < len(_border_frames):
|
|||
|
|
ind = tk.Frame(list_frame, bg="#5B8DB3", height=3)
|
|||
|
|
ind.pack(before=_border_frames[insert_before], fill="x", pady=0)
|
|||
|
|
else:
|
|||
|
|
ind = tk.Frame(list_frame, bg="#5B8DB3", height=3)
|
|||
|
|
ind.pack(fill="x", pady=0)
|
|||
|
|
_drag["indicator"] = ind
|
|||
|
|
|
|||
|
|
def _drag_end(event, idx):
|
|||
|
|
if not _drag["active"]:
|
|||
|
|
return
|
|||
|
|
_drag["active"] = False
|
|||
|
|
if _drag.get("indicator"):
|
|||
|
|
_drag["indicator"].destroy()
|
|||
|
|
_drag["indicator"] = None
|
|||
|
|
event.widget.configure(cursor="hand2")
|
|||
|
|
abs_y = event.widget.winfo_rooty() + event.y
|
|||
|
|
target = _find_drop_target(abs_y)
|
|||
|
|
src = _drag["idx"]
|
|||
|
|
if target is not None and target != src:
|
|||
|
|
item = todos.pop(src)
|
|||
|
|
if target > src:
|
|||
|
|
target -= 1
|
|||
|
|
todos.insert(target, item)
|
|||
|
|
save_todos(todos)
|
|||
|
|
rebuild_list()
|
|||
|
|
else:
|
|||
|
|
if 0 <= src < len(_border_frames):
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
def _find_drop_target(abs_y):
|
|||
|
|
for i, bf in enumerate(_border_frames):
|
|||
|
|
try:
|
|||
|
|
wy = bf.winfo_rooty()
|
|||
|
|
wh = bf.winfo_height()
|
|||
|
|
mid = wy + wh // 2
|
|||
|
|
if abs_y < mid:
|
|||
|
|
return i
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return len(_border_frames)
|
|||
|
|
|
|||
|
|
def rebuild_list():
|
|||
|
|
for w in list_frame.winfo_children():
|
|||
|
|
w.destroy()
|
|||
|
|
todo_widgets.clear()
|
|||
|
|
send_selection.clear()
|
|||
|
|
_border_frames.clear()
|
|||
|
|
today_str = date.today().isoformat()
|
|||
|
|
|
|||
|
|
active_cat = _active_category[0]
|
|||
|
|
|
|||
|
|
for idx, todo in enumerate(todos):
|
|||
|
|
todo_cat = todo.get("category", "Allgemein")
|
|||
|
|
if active_cat != "Alle" and todo_cat != active_cat:
|
|||
|
|
continue
|
|||
|
|
is_done = todo.get("done", False)
|
|||
|
|
has_date = bool(todo.get("date"))
|
|||
|
|
is_overdue = has_date and not is_done and todo["date"] < today_str
|
|||
|
|
prio = todo.get("priority", 0)
|
|||
|
|
from_sender = todo.get("sender", "")
|
|||
|
|
|
|||
|
|
if is_overdue:
|
|||
|
|
row_bg = "#FDECEC"
|
|||
|
|
border_color = "#E87070"
|
|||
|
|
elif is_done:
|
|||
|
|
row_bg = "#EDF7ED"
|
|||
|
|
border_color = "#A8D8A8"
|
|||
|
|
elif prio == 1:
|
|||
|
|
row_bg = "#FDE8E8"
|
|||
|
|
border_color = "#E8A0A0"
|
|||
|
|
elif prio == 2:
|
|||
|
|
row_bg = "#FDF5E0"
|
|||
|
|
border_color = "#E8D090"
|
|||
|
|
else:
|
|||
|
|
row_bg = "white"
|
|||
|
|
border_color = "#D0E8F0"
|
|||
|
|
|
|||
|
|
border_frame = tk.Frame(list_frame, bg=border_color, padx=1, pady=1)
|
|||
|
|
border_frame.pack(fill="x", pady=2)
|
|||
|
|
_border_frames.append(border_frame)
|
|||
|
|
|
|||
|
|
row = tk.Frame(border_frame, bg=row_bg, padx=4, pady=6)
|
|||
|
|
row.pack(fill="x")
|
|||
|
|
|
|||
|
|
# Rechtsklick auf Zeile
|
|||
|
|
row.bind("<Button-3>", lambda e, i=idx: _show_context_menu(e, i))
|
|||
|
|
|
|||
|
|
# ≡ Drag-Handle
|
|||
|
|
drag_handle = tk.Label(row, text="≡", font=("Segoe UI", 12, "bold"),
|
|||
|
|
bg=row_bg, fg="#B0C8D8", cursor="hand2")
|
|||
|
|
drag_handle.pack(side="left", padx=(0, 2))
|
|||
|
|
drag_handle.bind("<Button-1>", lambda e, i=idx: _drag_start(e, i))
|
|||
|
|
drag_handle.bind("<B1-Motion>", lambda e, i=idx: _drag_motion(e, i))
|
|||
|
|
drag_handle.bind("<ButtonRelease-1>", lambda e, i=idx: _drag_end(e, i))
|
|||
|
|
drag_handle.bind("<Enter>", lambda e, h=drag_handle: h.configure(fg="#5B8DB3"))
|
|||
|
|
drag_handle.bind("<Leave>", lambda e, h=drag_handle: h.configure(fg="#B0C8D8"))
|
|||
|
|
|
|||
|
|
# Checkbox erledigt
|
|||
|
|
var = tk.BooleanVar(value=is_done)
|
|||
|
|
cb = tk.Checkbutton(row, variable=var, bg=row_bg, activebackground=row_bg,
|
|||
|
|
command=lambda i=idx, v=var: toggle_done(i, v))
|
|||
|
|
cb.pack(side="left")
|
|||
|
|
|
|||
|
|
# Prioritäts-Indikator
|
|||
|
|
if prio == 1 and not is_done:
|
|||
|
|
tk.Label(row, text="🔴", font=("Segoe UI", 8), bg=row_bg).pack(side="left")
|
|||
|
|
elif prio == 2 and not is_done:
|
|||
|
|
tk.Label(row, text="🟡", font=("Segoe UI", 8), bg=row_bg).pack(side="left")
|
|||
|
|
|
|||
|
|
# Kategorie-Badge
|
|||
|
|
_cat_colors = {"Allgemein": "#B9ECFA", "MPA": "#D4EEF7", "Ärzte": "#C8E0F0"}
|
|||
|
|
_cat_fg = {"Allgemein": "#4a8aaa", "MPA": "#2E7D5B", "Ärzte": "#5B6DB3"}
|
|||
|
|
if active_cat == "Alle":
|
|||
|
|
cat_text = todo_cat[:3]
|
|||
|
|
cat_bg = _cat_colors.get(todo_cat, "#D0E8F0")
|
|||
|
|
cat_fgc = _cat_fg.get(todo_cat, "#4a8aaa")
|
|||
|
|
tk.Label(row, text=cat_text, font=("Segoe UI", 6),
|
|||
|
|
bg=cat_bg, fg=cat_fgc, padx=3, pady=0,
|
|||
|
|
relief="flat", bd=0).pack(side="left", padx=(0, 2))
|
|||
|
|
|
|||
|
|
# Absender-Info bei empfangenen Aufgaben
|
|||
|
|
if from_sender:
|
|||
|
|
tk.Label(row, text=f"← {from_sender}", font=("Segoe UI", 7, "italic"),
|
|||
|
|
bg=row_bg, fg="#7EC8E3").pack(side="left", padx=(0, 4))
|
|||
|
|
|
|||
|
|
# Text (klickbar → öffnet Detail)
|
|||
|
|
text_fg = "#999" if is_done else ("#C03030" if is_overdue else "#1a4d6d")
|
|||
|
|
fs = todo_font_size[0]
|
|||
|
|
text_font = ("Segoe UI", fs, "overstrike") if is_done else ("Segoe UI", fs)
|
|||
|
|
lbl = tk.Label(row, text=todo["text"], font=text_font, bg=row_bg, fg=text_fg,
|
|||
|
|
anchor="w", wraplength=250, justify="left", cursor="hand2")
|
|||
|
|
lbl.pack(side="left", fill="x", expand=True, padx=(4, 4))
|
|||
|
|
lbl.bind("<Button-1>", lambda e, i=idx: open_detail(i))
|
|||
|
|
lbl.bind("<Button-3>", lambda e, i=idx: _show_context_menu(e, i))
|
|||
|
|
|
|||
|
|
# Notiz-Indikator
|
|||
|
|
if todo.get("notes", "").strip():
|
|||
|
|
tk.Label(row, text="📝", font=("Segoe UI", 8), bg=row_bg).pack(side="left")
|
|||
|
|
|
|||
|
|
right_f = tk.Frame(row, bg=row_bg)
|
|||
|
|
right_f.pack(side="right")
|
|||
|
|
|
|||
|
|
# "senden"-Checkbox (unauffällig, rechts)
|
|||
|
|
send_var = tk.BooleanVar(value=False)
|
|||
|
|
send_selection[idx] = send_var
|
|||
|
|
send_cb = tk.Checkbutton(right_f, variable=send_var, bg=row_bg,
|
|||
|
|
activebackground=row_bg, highlightthickness=0, bd=0)
|
|||
|
|
send_cb.pack(side="right", padx=(4, 0))
|
|||
|
|
send_lbl = tk.Label(right_f, text="senden", font=("Segoe UI", 7),
|
|||
|
|
bg=row_bg, fg="#A0C8D8", cursor="hand2")
|
|||
|
|
send_lbl.pack(side="right")
|
|||
|
|
send_lbl.bind("<Button-1>", lambda e, sv=send_var: sv.set(not sv.get()))
|
|||
|
|
|
|||
|
|
if has_date:
|
|||
|
|
try:
|
|||
|
|
dt = datetime.strptime(todo["date"], "%Y-%m-%d")
|
|||
|
|
date_text = dt.strftime("%d.%m.%Y")
|
|||
|
|
except Exception:
|
|||
|
|
date_text = todo["date"]
|
|||
|
|
if is_overdue:
|
|||
|
|
date_fg = "#C03030"
|
|||
|
|
date_text = f"⚠ {date_text}"
|
|||
|
|
elif is_done:
|
|||
|
|
date_fg = "#999"
|
|||
|
|
else:
|
|||
|
|
date_fg = "#4a8aaa"
|
|||
|
|
tk.Label(right_f, text=date_text, font=("Segoe UI", 8), bg=row_bg,
|
|||
|
|
fg=date_fg).pack(side="right", padx=(0, 6))
|
|||
|
|
|
|||
|
|
del_lbl = tk.Label(right_f, text="✕", font=("Segoe UI", 10), bg=row_bg,
|
|||
|
|
fg="#ccc", cursor="hand2")
|
|||
|
|
del_lbl.pack(side="right", padx=(0, 4))
|
|||
|
|
del_lbl.bind("<Enter>", lambda e, b=del_lbl: b.configure(fg="#E87070"))
|
|||
|
|
del_lbl.bind("<Leave>", lambda e, b=del_lbl: b.configure(fg="#ccc"))
|
|||
|
|
del_lbl.bind("<Button-1>", lambda e, i=idx: delete_todo(i))
|
|||
|
|
|
|||
|
|
todo_widgets.append(row)
|
|||
|
|
|
|||
|
|
update_counter()
|
|||
|
|
save_todos(todos)
|
|||
|
|
_bind_mousewheel_recursive(list_frame)
|
|||
|
|
|
|||
|
|
def add_todo(event=None):
|
|||
|
|
text = entry_var.get().strip()
|
|||
|
|
if not text or text == "Neue Aufgabe eingeben oder diktieren…":
|
|||
|
|
return
|
|||
|
|
date_iso = selected_date[0].isoformat() if selected_date[0] else None
|
|||
|
|
new_item = {
|
|||
|
|
"id": int(datetime.now().timestamp() * 1000),
|
|||
|
|
"text": text,
|
|||
|
|
"done": False,
|
|||
|
|
"date": date_iso,
|
|||
|
|
"priority": 0,
|
|||
|
|
"notes": "",
|
|||
|
|
"created": datetime.now().isoformat(),
|
|||
|
|
"category": _get_new_todo_category(),
|
|||
|
|
}
|
|||
|
|
todos.append(new_item)
|
|||
|
|
entry_var.set("")
|
|||
|
|
entry.delete(0, "end")
|
|||
|
|
entry.insert(0, "Neue Aufgabe eingeben oder diktieren…")
|
|||
|
|
entry.configure(fg="#999")
|
|||
|
|
clear_date()
|
|||
|
|
rec_status_var.set("")
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
def toggle_done(idx, var):
|
|||
|
|
if 0 <= idx < len(todos):
|
|||
|
|
todos[idx]["done"] = var.get()
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
def delete_todo(idx):
|
|||
|
|
if 0 <= idx < len(todos):
|
|||
|
|
todos.pop(idx)
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
def delete_done():
|
|||
|
|
i = 0
|
|||
|
|
while i < len(todos):
|
|||
|
|
if todos[i].get("done"):
|
|||
|
|
todos.pop(i)
|
|||
|
|
else:
|
|||
|
|
i += 1
|
|||
|
|
rebuild_list()
|
|||
|
|
|
|||
|
|
add_btn.configure(command=add_todo)
|
|||
|
|
entry.bind("<Return>", add_todo)
|
|||
|
|
btn_del_done.configure(command=delete_done)
|
|||
|
|
|
|||
|
|
rebuild_list()
|
|||
|
|
entry.focus_set()
|
|||
|
|
|
|||
|
|
if _need_initial_rebuild[0] == "notes":
|
|||
|
|
win.after(200, _rebuild_notes_list)
|
|||
|
|
elif _need_initial_rebuild[0] == "checklist":
|
|||
|
|
win.after(200, _rebuild_checklist)
|
|||
|
|
|
|||
|
|
if imported_count > 0:
|
|||
|
|
win.after(300, lambda: messagebox.showinfo(
|
|||
|
|
"Neue Aufgaben",
|
|||
|
|
f"📥 {imported_count} neue Aufgabe(n) empfangen!",
|
|||
|
|
parent=win))
|