mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
feat: initialize CCMA member administration
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from queue import Empty, Queue
|
||||
from tkinter import ttk
|
||||
|
||||
from ccma import __version__
|
||||
from ccma.domain.models import HousekeeperFinding
|
||||
from ccma.services.housekeeper import Housekeeper, HousekeeperSettings
|
||||
from ccma.storage.repository import MemberRepository
|
||||
from ccma.ui.monitors import MonitorBounds, preferred_monitor
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class StartupResult:
|
||||
repository: MemberRepository
|
||||
validation_errors: list[str]
|
||||
findings: list[HousekeeperFinding]
|
||||
|
||||
|
||||
class SplashScreen(tk.Toplevel):
|
||||
def __init__(
|
||||
self,
|
||||
master: tk.Tk,
|
||||
repository: MemberRepository,
|
||||
on_complete: Callable[[StartupResult], None],
|
||||
on_error: Callable[[Exception], None],
|
||||
*,
|
||||
run_housekeeper: bool = True,
|
||||
monitor: MonitorBounds | None = None,
|
||||
housekeeper_settings: HousekeeperSettings | None = None,
|
||||
):
|
||||
super().__init__(master)
|
||||
self.repository = repository
|
||||
self.on_complete = on_complete
|
||||
self.on_error = on_error
|
||||
self.run_housekeeper = run_housekeeper
|
||||
self.monitor = monitor
|
||||
self.housekeeper_settings = housekeeper_settings
|
||||
self._messages: Queue[tuple[str, object]] = Queue()
|
||||
self.overrideredirect(True)
|
||||
self.resizable(False, False)
|
||||
self.configure(background="#0b1117")
|
||||
self.canvas: tk.Canvas
|
||||
self.status_item: int
|
||||
self._build_ui()
|
||||
self._center()
|
||||
self.after(120, self._start)
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
width, height = 620, 330
|
||||
self.geometry(f"{width}x{height}")
|
||||
self.canvas = tk.Canvas(
|
||||
self,
|
||||
width=width,
|
||||
height=height,
|
||||
highlightthickness=0,
|
||||
background="#0b1117",
|
||||
)
|
||||
self.canvas.pack(fill="both", expand=True)
|
||||
self.canvas.create_rectangle(0, 0, width, 6, fill="#00d084", outline="")
|
||||
self.canvas.create_text(
|
||||
34,
|
||||
58,
|
||||
anchor="w",
|
||||
text="CCMA",
|
||||
fill="#00d084",
|
||||
font=("TkFixedFont", 30, "bold"),
|
||||
)
|
||||
self.canvas.create_text(
|
||||
34,
|
||||
101,
|
||||
anchor="w",
|
||||
text="CHAOTIC CREATURE MEMBER ADMINISTRATION",
|
||||
fill="#d7e0e8",
|
||||
font=("TkFixedFont", 12, "bold"),
|
||||
)
|
||||
self.canvas.create_text(
|
||||
34,
|
||||
139,
|
||||
anchor="w",
|
||||
text="Chaos Computer Club Mannheim e.V.",
|
||||
fill="#7f8e9b",
|
||||
font=("TkDefaultFont", 10),
|
||||
)
|
||||
self.canvas.create_text(
|
||||
width - 34, 58, anchor="e", text=f"v{__version__}", fill="#7f8e9b", font=("TkFixedFont", 10)
|
||||
)
|
||||
self.status_item = self.canvas.create_text(
|
||||
34,
|
||||
245,
|
||||
anchor="w",
|
||||
text="Initialisiere …",
|
||||
fill="#d7e0e8",
|
||||
font=("TkFixedFont", 10),
|
||||
)
|
||||
self.progress = ttk.Progressbar(self, mode="indeterminate")
|
||||
self.canvas.create_window(34, 280, anchor="w", width=552, window=self.progress)
|
||||
|
||||
def _center(self) -> None:
|
||||
self.update_idletasks()
|
||||
width, height = self.winfo_width(), self.winfo_height()
|
||||
monitor = self.monitor or preferred_monitor(self)
|
||||
pointer_x, pointer_y = monitor.center
|
||||
x, y = centered_position(
|
||||
width=width,
|
||||
height=height,
|
||||
pointer_x=pointer_x,
|
||||
pointer_y=pointer_y,
|
||||
screen_x=monitor.x,
|
||||
screen_y=monitor.y,
|
||||
screen_width=monitor.width,
|
||||
screen_height=monitor.height,
|
||||
)
|
||||
self.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
def _start(self) -> None:
|
||||
self.progress.start(10)
|
||||
|
||||
def worker() -> None:
|
||||
try:
|
||||
self._messages.put(("status", "Öffne Mitglieder-Store …"))
|
||||
self.repository.initialize()
|
||||
self._messages.put(("status", "Validiere Mitgliederakten …"))
|
||||
errors = self.repository.validate()
|
||||
self._messages.put(("status", "Baue Suchindex …"))
|
||||
self.repository.list_members()
|
||||
findings = []
|
||||
if self.run_housekeeper:
|
||||
self._messages.put(("status", "Starte Hausmeister …"))
|
||||
findings = Housekeeper(self.repository, self.housekeeper_settings).run()
|
||||
result = StartupResult(self.repository, errors, findings)
|
||||
self._messages.put(("result", result))
|
||||
except Exception as exc:
|
||||
self._messages.put(("error", exc))
|
||||
|
||||
threading.Thread(target=worker, name="ccma-startup", daemon=True).start()
|
||||
self.after(30, self._poll_messages)
|
||||
|
||||
def _poll_messages(self) -> None:
|
||||
keep_polling = True
|
||||
try:
|
||||
while True:
|
||||
kind, payload = self._messages.get_nowait()
|
||||
if kind == "status":
|
||||
self._set_status(str(payload))
|
||||
elif kind == "result":
|
||||
keep_polling = False
|
||||
self._finish(payload) # type: ignore[arg-type]
|
||||
elif kind == "error":
|
||||
keep_polling = False
|
||||
self._fail(payload) # type: ignore[arg-type]
|
||||
except Empty:
|
||||
pass
|
||||
if keep_polling and self.winfo_exists():
|
||||
self.after(30, self._poll_messages)
|
||||
|
||||
def _finish(self, result: StartupResult) -> None:
|
||||
self.progress.stop()
|
||||
self._set_status(f"Bereit · {len(result.findings)} Vorgänge benötigen Aufmerksamkeit")
|
||||
self.after(350, lambda: self._complete(result))
|
||||
|
||||
def _set_status(self, text: str) -> None:
|
||||
self.canvas.itemconfigure(self.status_item, text=text)
|
||||
|
||||
def _complete(self, result: StartupResult) -> None:
|
||||
self.destroy()
|
||||
self.on_complete(result)
|
||||
|
||||
def _fail(self, error: Exception) -> None:
|
||||
self.progress.stop()
|
||||
self.destroy()
|
||||
self.on_error(error)
|
||||
|
||||
|
||||
def centered_position(
|
||||
*,
|
||||
width: int,
|
||||
height: int,
|
||||
pointer_x: int,
|
||||
pointer_y: int,
|
||||
screen_x: int,
|
||||
screen_y: int,
|
||||
screen_width: int,
|
||||
screen_height: int,
|
||||
) -> tuple[int, int]:
|
||||
"""Center around the pointer and keep the complete window on the virtual desktop."""
|
||||
maximum_x = screen_x + max(0, screen_width - width)
|
||||
maximum_y = screen_y + max(0, screen_height - height)
|
||||
x = min(max(pointer_x - width // 2, screen_x), maximum_x)
|
||||
y = min(max(pointer_y - height // 2, screen_y), maximum_y)
|
||||
return x, y
|
||||
Reference in New Issue
Block a user