242 lines
9.1 KiB
Python
242 lines
9.1 KiB
Python
import os
|
||
import tkinter as tk
|
||
|
||
from whisper_app import config as cfg
|
||
|
||
|
||
def open(root: tk.Tk) -> None:
|
||
"""Open the vocabulary window as a Toplevel of *root*."""
|
||
_open_main(root)
|
||
|
||
|
||
def _open_main(root: tk.Tk) -> None:
|
||
BG = "#18181f"
|
||
BG2 = "#22222c"
|
||
BG3 = "#2c2c38"
|
||
BORDER = "#38384a"
|
||
FG = "#e8e8f0"
|
||
FG2 = "#7878a0"
|
||
AMBER = "#f5a623"
|
||
AMBER2 = "#c8831a"
|
||
RED = "#f87171"
|
||
_mono = "Consolas" if os.name == "nt" else "monospace"
|
||
_sans = "Segoe UI" if os.name == "nt" else "sans-serif"
|
||
FONT = (_sans, 11)
|
||
FONT_B = (_sans, 11, "bold")
|
||
FONT_S = (_sans, 9)
|
||
FONT_H = (_sans, 14, "bold")
|
||
FONT_M = (_mono, 10)
|
||
|
||
win = tk.Toplevel(root)
|
||
win.title("Vokabular")
|
||
win.configure(bg=BG)
|
||
win.attributes("-topmost", True)
|
||
win.resizable(True, True)
|
||
win.minsize(600, 480)
|
||
win.option_add("*Menu.background", BG3)
|
||
win.option_add("*Menu.foreground", FG)
|
||
win.option_add("*Menu.activeBackground", AMBER)
|
||
win.option_add("*Menu.activeForeground", BG)
|
||
|
||
# ── Header ──
|
||
hdr = tk.Frame(win, bg=BG2)
|
||
hdr.pack(fill="x")
|
||
tk.Frame(hdr, bg=AMBER, height=3).pack(fill="x")
|
||
tk.Label(hdr, text="VOKABULAR & KORREKTUREN", font=FONT_H,
|
||
bg=BG2, fg=FG, pady=14).pack()
|
||
tk.Label(hdr, text="Wörter lernen · Ersetzungen definieren",
|
||
font=FONT_S, bg=BG2, fg=FG2).pack(pady=(0, 10))
|
||
|
||
content = tk.Frame(win, bg=BG, padx=28, pady=12)
|
||
content.pack(fill="both", expand=True)
|
||
|
||
# ── Add-word form ─────────────────────────────────────────────────────────
|
||
is_correction = tk.BooleanVar(value=False)
|
||
|
||
form = tk.Frame(content, bg=BG3, padx=16, pady=14)
|
||
form.pack(fill="x", pady=(0, 16))
|
||
|
||
# Toggle row
|
||
tog_row = tk.Frame(form, bg=BG3)
|
||
tog_row.pack(fill="x", pady=(0, 10))
|
||
tk.Label(tog_row, text="Korrektur (falsch → richtig)", font=FONT,
|
||
bg=BG3, fg=FG).pack(side="left")
|
||
|
||
def toggle_form(*_):
|
||
if is_correction.get():
|
||
entry_from.pack(side="left", padx=(0, 6))
|
||
arrow_lbl.pack(side="left", padx=4)
|
||
entry_to.pack(side="left")
|
||
entry_word.pack_forget()
|
||
else:
|
||
entry_word.pack(side="left", fill="x", expand=True)
|
||
entry_from.pack_forget()
|
||
arrow_lbl.pack_forget()
|
||
entry_to.pack_forget()
|
||
|
||
tog_btn = tk.Checkbutton(tog_row, variable=is_correction, command=toggle_form,
|
||
bg=BG3, fg=FG2, activebackground=BG3,
|
||
selectcolor=AMBER, relief="flat", bd=0,
|
||
indicatoron=True)
|
||
tog_btn.pack(side="right")
|
||
|
||
# Input row
|
||
inp_row = tk.Frame(form, bg=BG3)
|
||
inp_row.pack(fill="x")
|
||
|
||
entry_style = dict(font=FONT_M, bg=BG, fg=FG, insertbackground=AMBER,
|
||
relief="flat", bd=6, highlightbackground=BORDER, highlightthickness=1)
|
||
|
||
entry_word = tk.Entry(inp_row, width=32, **entry_style)
|
||
entry_word.insert(0, "")
|
||
entry_from = tk.Entry(inp_row, width=14, **entry_style)
|
||
arrow_lbl = tk.Label(inp_row, text="→", font=("Segoe UI", 14), bg=BG3, fg=AMBER)
|
||
entry_to = tk.Entry(inp_row, width=14, **entry_style)
|
||
entry_word.pack(side="left", fill="x", expand=True)
|
||
|
||
def add_entry():
|
||
if is_correction.get():
|
||
frm = entry_from.get().strip()
|
||
to = entry_to.get().strip()
|
||
if frm and to:
|
||
cfg.vocab["replacements"].append({"from": frm, "to": to})
|
||
entry_from.delete(0, tk.END)
|
||
entry_to.delete(0, tk.END)
|
||
else:
|
||
w = entry_word.get().strip()
|
||
if w and w not in cfg.vocab["words"]:
|
||
cfg.vocab["words"].append(w)
|
||
entry_word.delete(0, tk.END)
|
||
cfg.save_vocab()
|
||
refresh_lists()
|
||
|
||
win.bind("<Return>", lambda _: add_entry())
|
||
|
||
add_btn = tk.Button(inp_row, text="Hinzufügen", command=add_entry,
|
||
bg=AMBER, fg=BG, font=FONT_B,
|
||
relief="flat", padx=14, pady=5, cursor="hand2", bd=0)
|
||
add_btn.pack(side="right", padx=(10, 0))
|
||
add_btn.bind("<Enter>", lambda _: add_btn.config(bg=AMBER2))
|
||
add_btn.bind("<Leave>", lambda _: add_btn.config(bg=AMBER))
|
||
|
||
# ── Lists ─────────────────────────────────────────────────────────────────
|
||
lists_frame = tk.Frame(content, bg=BG)
|
||
lists_frame.pack(fill="both", expand=True)
|
||
lists_frame.columnconfigure(0, weight=1)
|
||
lists_frame.columnconfigure(1, weight=2)
|
||
|
||
def section_label(parent, text):
|
||
tk.Label(parent, text=text, font=("Consolas", 9, "bold"),
|
||
bg=BG, fg=AMBER).pack(anchor="w", pady=(0, 6))
|
||
|
||
# Words column
|
||
col_w = tk.Frame(lists_frame, bg=BG)
|
||
col_w.grid(row=0, column=0, sticky="nsew", padx=(0, 12))
|
||
section_label(col_w, "WÖRTER")
|
||
|
||
words_box = tk.Listbox(col_w, font=FONT_M, bg=BG3, fg=FG,
|
||
selectbackground=AMBER, selectforeground=BG,
|
||
relief="flat", bd=0, highlightthickness=0,
|
||
activestyle="none", height=10)
|
||
words_box.pack(fill="both", expand=True)
|
||
|
||
def edit_word(_event=None):
|
||
sel = words_box.curselection()
|
||
if not sel:
|
||
return
|
||
word = cfg.vocab["words"].pop(sel[0])
|
||
cfg.save_vocab()
|
||
if is_correction.get():
|
||
is_correction.set(False)
|
||
toggle_form()
|
||
entry_word.delete(0, tk.END)
|
||
entry_word.insert(0, word)
|
||
entry_word.focus_set()
|
||
refresh_lists()
|
||
|
||
words_box.bind("<Double-1>", edit_word)
|
||
|
||
def del_word():
|
||
sel = words_box.curselection()
|
||
if sel:
|
||
cfg.vocab["words"].pop(sel[0])
|
||
cfg.save_vocab()
|
||
refresh_lists()
|
||
|
||
word_btns = tk.Frame(col_w, bg=BG)
|
||
word_btns.pack(anchor="e", pady=(4, 0))
|
||
tk.Button(word_btns, text="✎ Bearbeiten", command=edit_word,
|
||
bg=BG3, fg=AMBER, font=FONT_S, relief="flat",
|
||
padx=8, pady=3, cursor="hand2", bd=0).pack(side="left", padx=(0, 6))
|
||
tk.Button(word_btns, text="− Entfernen", command=del_word,
|
||
bg=BG3, fg=RED, font=FONT_S, relief="flat",
|
||
padx=8, pady=3, cursor="hand2", bd=0).pack(side="left")
|
||
|
||
# Replacements column
|
||
col_r = tk.Frame(lists_frame, bg=BG)
|
||
col_r.grid(row=0, column=1, sticky="nsew")
|
||
section_label(col_r, "KORREKTUREN")
|
||
|
||
repl_box = tk.Listbox(col_r, font=FONT_M, bg=BG3, fg=FG,
|
||
selectbackground=AMBER, selectforeground=BG,
|
||
relief="flat", bd=0, highlightthickness=0,
|
||
activestyle="none", height=10)
|
||
repl_box.pack(fill="both", expand=True)
|
||
|
||
def edit_repl(_event=None):
|
||
sel = repl_box.curselection()
|
||
if not sel:
|
||
return
|
||
repl = cfg.vocab["replacements"].pop(sel[0])
|
||
cfg.save_vocab()
|
||
if not is_correction.get():
|
||
is_correction.set(True)
|
||
toggle_form()
|
||
entry_from.delete(0, tk.END)
|
||
entry_from.insert(0, repl["from"])
|
||
entry_to.delete(0, tk.END)
|
||
entry_to.insert(0, repl["to"])
|
||
entry_from.focus_set()
|
||
refresh_lists()
|
||
|
||
repl_box.bind("<Double-1>", edit_repl)
|
||
|
||
def del_repl():
|
||
sel = repl_box.curselection()
|
||
if sel:
|
||
cfg.vocab["replacements"].pop(sel[0])
|
||
cfg.save_vocab()
|
||
refresh_lists()
|
||
|
||
repl_btns = tk.Frame(col_r, bg=BG)
|
||
repl_btns.pack(anchor="e", pady=(4, 0))
|
||
tk.Button(repl_btns, text="✎ Bearbeiten", command=edit_repl,
|
||
bg=BG3, fg=AMBER, font=FONT_S, relief="flat",
|
||
padx=8, pady=3, cursor="hand2", bd=0).pack(side="left", padx=(0, 6))
|
||
tk.Button(repl_btns, text="− Entfernen", command=del_repl,
|
||
bg=BG3, fg=RED, font=FONT_S, relief="flat",
|
||
padx=8, pady=3, cursor="hand2", bd=0).pack(side="left")
|
||
|
||
def refresh_lists():
|
||
words_box.delete(0, tk.END)
|
||
for w in cfg.vocab.get("words", []):
|
||
words_box.insert(tk.END, f" {w}")
|
||
repl_box.delete(0, tk.END)
|
||
for r in cfg.vocab.get("replacements", []):
|
||
repl_box.insert(tk.END, f" {r['from']} → {r['to']}")
|
||
|
||
refresh_lists()
|
||
|
||
# ── Footer ──
|
||
tk.Frame(win, bg=BORDER, height=1).pack(fill="x")
|
||
tk.Label(win, text="Wörter fließen als Kontext in Whisper ein · Korrekturen werden nach der Transkription angewendet",
|
||
font=FONT_S, bg=BG2, fg=FG2, pady=8).pack()
|
||
|
||
# Center on screen after layout
|
||
win.update_idletasks()
|
||
sw = win.winfo_screenwidth()
|
||
sh = win.winfo_screenheight()
|
||
w = max(win.winfo_reqwidth(), 600)
|
||
h = min(max(win.winfo_reqheight(), 480), sh - 100)
|
||
win.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
|