feat: configure minimum splash duration

This commit is contained in:
Marcel Peterkau
2026-06-21 18:48:08 +02:00
parent dadcdb8b4a
commit e1d2b87ca1
7 changed files with 66 additions and 1 deletions
+8
View File
@@ -31,6 +31,14 @@ PYTHONPATH=src python -m ccma
On first start, select or create the central member-store directory. The On first start, select or create the central member-store directory. The
`VERSION` file is the single source for application and package versions. `VERSION` file is the single source for application and package versions.
The splash screen remains visible for at least five seconds by default. This
advanced setting is intentionally not exposed in the options dialog. It can be
changed directly in `config.json`, including disabling the minimum with `0`:
```json
"splash_minimum_seconds": 0
```
## Store layout ## Store layout
```text ```text
+1
View File
@@ -55,6 +55,7 @@ class CCMAApp(tk.Tk):
run_housekeeper=self.config_obj.run_housekeeper_on_startup, run_housekeeper=self.config_obj.run_housekeeper_on_startup,
monitor=self.startup_monitor, monitor=self.startup_monitor,
housekeeper_settings=self.config_obj.housekeeper_settings(), housekeeper_settings=self.config_obj.housekeeper_settings(),
minimum_display_seconds=self.config_obj.splash_minimum_seconds,
) )
except Exception as exc: except Exception as exc:
self._startup_failed(exc) self._startup_failed(exc)
+1
View File
@@ -27,6 +27,7 @@
"Dropdowns zeigen deutsche Begriffe bei weiterhin englischen Speicher-Keys; der Hausmeisterstatus liegt einheitlich in housekeeper.json.", "Dropdowns zeigen deutsche Begriffe bei weiterhin englischen Speicher-Keys; der Hausmeisterstatus liegt einheitlich in housekeeper.json.",
"Mehrstufiger Mahnworkflow mit Hausmeister-Regel, Entwurf, Versandbestätigung, Zahlungsfrist, optionaler Gebühr und Mahnsperre ergänzt.", "Mehrstufiger Mahnworkflow mit Hausmeister-Regel, Entwurf, Versandbestätigung, Zahlungsfrist, optionaler Gebühr und Mahnsperre ergänzt.",
"Splash-Screen auf das eingebettete CCMA-Hintergrundmotiv umgestellt und redundante Titeltexte entfernt.", "Splash-Screen auf das eingebettete CCMA-Hintergrundmotiv umgestellt und redundante Titeltexte entfernt.",
"Konfigurierbare Mindestanzeigezeit des Splash-Screens mit fünf Sekunden Standardwert ergänzt.",
"Hausmeister um konfigurierbare Geburtstags- und Mitgliedsjubiläumsmeldungen erweitert.", "Hausmeister um konfigurierbare Geburtstags- und Mitgliedsjubiläumsmeldungen erweitert.",
"Statusänderungen werden mit altem und neuem Klartextwert in der Mitgliederchronik protokolliert.", "Statusänderungen werden mit altem und neuem Klartextwert in der Mitgliederchronik protokolliert.",
"Fensterposition, normaler Fensterzustand und Maximierung werden gespeichert; der Splash startet auf dem zuletzt verwendeten Monitor.", "Fensterposition, normaler Fensterzustand und Maximierung werden gespeichert; der Splash startet auf dem zuletzt verwendeten Monitor.",
+12
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import json import json
import math
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -18,6 +19,7 @@ class AppConfig:
gnucash_path: str = "" gnucash_path: str = ""
theme_mode: str = "dark" theme_mode: str = "dark"
run_housekeeper_on_startup: bool = True run_housekeeper_on_startup: bool = True
splash_minimum_seconds: float = 5.0
birthday_days_before: int = 7 birthday_days_before: int = 7
birthday_days_after: int = 2 birthday_days_after: int = 2
anniversary_days_before: int = 14 anniversary_days_before: int = 14
@@ -40,6 +42,7 @@ class AppConfig:
"gnucash_path": self.gnucash_path, "gnucash_path": self.gnucash_path,
"theme_mode": self.theme_mode, "theme_mode": self.theme_mode,
"run_housekeeper_on_startup": self.run_housekeeper_on_startup, "run_housekeeper_on_startup": self.run_housekeeper_on_startup,
"splash_minimum_seconds": _non_negative_float(self.splash_minimum_seconds, 5.0),
"birthday_days_before": self.birthday_days_before, "birthday_days_before": self.birthday_days_before,
"birthday_days_after": self.birthday_days_after, "birthday_days_after": self.birthday_days_after,
"anniversary_days_before": self.anniversary_days_before, "anniversary_days_before": self.anniversary_days_before,
@@ -96,6 +99,7 @@ def load_config() -> AppConfig:
gnucash_path=str(data.get("gnucash_path", "")), gnucash_path=str(data.get("gnucash_path", "")),
theme_mode=str(data.get("theme_mode", "dark")), theme_mode=str(data.get("theme_mode", "dark")),
run_housekeeper_on_startup=bool(data.get("run_housekeeper_on_startup", True)), run_housekeeper_on_startup=bool(data.get("run_housekeeper_on_startup", True)),
splash_minimum_seconds=_non_negative_float(data.get("splash_minimum_seconds", 5.0), 5.0),
birthday_days_before=int(data.get("birthday_days_before", 7)), birthday_days_before=int(data.get("birthday_days_before", 7)),
birthday_days_after=int(data.get("birthday_days_after", 2)), birthday_days_after=int(data.get("birthday_days_after", 2)),
anniversary_days_before=int(data.get("anniversary_days_before", 14)), anniversary_days_before=int(data.get("anniversary_days_before", 14)),
@@ -113,3 +117,11 @@ def _legacy_config_directory() -> Path:
if os.name == "nt": if os.name == "nt":
return Path(os.environ.get("APPDATA", Path.home())) / "C3MA" return Path(os.environ.get("APPDATA", Path.home())) / "C3MA"
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "c3ma" return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "c3ma"
def _non_negative_float(value: object, default: float) -> float:
try:
parsed = float(value)
except (TypeError, ValueError):
return default
return max(0.0, parsed) if math.isfinite(parsed) else default
+25 -1
View File
@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import math
import threading import threading
import time
import tkinter as tk import tkinter as tk
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
@@ -33,6 +35,7 @@ class SplashScreen(tk.Toplevel):
run_housekeeper: bool = True, run_housekeeper: bool = True,
monitor: MonitorBounds | None = None, monitor: MonitorBounds | None = None,
housekeeper_settings: HousekeeperSettings | None = None, housekeeper_settings: HousekeeperSettings | None = None,
minimum_display_seconds: float = 5.0,
): ):
super().__init__(master) super().__init__(master)
self.repository = repository self.repository = repository
@@ -41,6 +44,10 @@ class SplashScreen(tk.Toplevel):
self.run_housekeeper = run_housekeeper self.run_housekeeper = run_housekeeper
self.monitor = monitor self.monitor = monitor
self.housekeeper_settings = housekeeper_settings self.housekeeper_settings = housekeeper_settings
self.minimum_display_seconds = (
max(0.0, float(minimum_display_seconds)) if math.isfinite(float(minimum_display_seconds)) else 5.0
)
self.shown_at = time.monotonic()
self._messages: Queue[tuple[str, object]] = Queue() self._messages: Queue[tuple[str, object]] = Queue()
self.overrideredirect(True) self.overrideredirect(True)
self.resizable(False, False) self.resizable(False, False)
@@ -145,7 +152,15 @@ class SplashScreen(tk.Toplevel):
def _finish(self, result: StartupResult) -> None: def _finish(self, result: StartupResult) -> None:
self.progress.stop() self.progress.stop()
self._set_status(f"Bereit · {len(result.findings)} Vorgänge benötigen Aufmerksamkeit") self._set_status(f"Bereit · {len(result.findings)} Vorgänge benötigen Aufmerksamkeit")
self.after(350, lambda: self._complete(result)) self._after_minimum(lambda: self._complete(result))
def _after_minimum(self, callback: Callable[[], None]) -> None:
remaining_ms = _remaining_minimum_ms(
self.shown_at,
self.minimum_display_seconds,
time.monotonic(),
)
self.after(remaining_ms, callback)
def _set_status(self, text: str) -> None: def _set_status(self, text: str) -> None:
self.canvas.itemconfigure(self.status_item, text=text) self.canvas.itemconfigure(self.status_item, text=text)
@@ -156,6 +171,10 @@ class SplashScreen(tk.Toplevel):
def _fail(self, error: Exception) -> None: def _fail(self, error: Exception) -> None:
self.progress.stop() self.progress.stop()
self._set_status("Start fehlgeschlagen")
self._after_minimum(lambda: self._complete_error(error))
def _complete_error(self, error: Exception) -> None:
self.destroy() self.destroy()
self.on_error(error) self.on_error(error)
@@ -177,3 +196,8 @@ def centered_position(
x = min(max(pointer_x - width // 2, screen_x), maximum_x) x = min(max(pointer_x - width // 2, screen_x), maximum_x)
y = min(max(pointer_y - height // 2, screen_y), maximum_y) y = min(max(pointer_y - height // 2, screen_y), maximum_y)
return x, y return x, y
def _remaining_minimum_ms(started_at: float, minimum_seconds: float, now: float) -> int:
remaining = max(0.0, minimum_seconds - max(0.0, now - started_at))
return int(remaining * 1000 + 0.999)
+11
View File
@@ -10,6 +10,7 @@ def test_paths_and_automation_settings_round_trip(tmp_path, monkeypatch) -> None
gnucash_path=str(tmp_path / "club.gnucash"), gnucash_path=str(tmp_path / "club.gnucash"),
theme_mode="light", theme_mode="light",
run_housekeeper_on_startup=False, run_housekeeper_on_startup=False,
splash_minimum_seconds=0,
birthday_days_before=10, birthday_days_before=10,
birthday_days_after=3, birthday_days_after=3,
anniversary_days_before=21, anniversary_days_before=21,
@@ -26,6 +27,16 @@ def test_paths_and_automation_settings_round_trip(tmp_path, monkeypatch) -> None
raw = json.loads(expected.path.read_text(encoding="utf-8")) raw = json.loads(expected.path.read_text(encoding="utf-8"))
assert raw["schema_version"] == 1 assert raw["schema_version"] == 1
assert raw["monitor_bounds"] == [-1920, 0, 1920, 1080] assert raw["monitor_bounds"] == [-1920, 0, 1920, 1080]
assert raw["splash_minimum_seconds"] == 0
def test_splash_minimum_defaults_to_five_and_is_clamped(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("CCMA_CONFIG_DIR", str(tmp_path / "config"))
assert load_config().splash_minimum_seconds == 5.0
config = AppConfig(splash_minimum_seconds=-10)
config.save()
assert load_config().splash_minimum_seconds == 0.0
def test_legacy_c3ma_environment_variables_are_still_read(tmp_path, monkeypatch) -> None: def test_legacy_c3ma_environment_variables_are_still_read(tmp_path, monkeypatch) -> None:
+8
View File
@@ -31,6 +31,14 @@ def test_splash_position_centers_on_pointer_and_stays_on_screen() -> None:
) == (0, 0) ) == (0, 0)
def test_splash_minimum_time_only_waits_for_remaining_duration() -> None:
from ccma.ui.splash import _remaining_minimum_ms
assert _remaining_minimum_ms(100.0, 5.0, 102.25) == 2750
assert _remaining_minimum_ms(100.0, 5.0, 106.0) == 0
assert _remaining_minimum_ms(100.0, 0.0, 100.0) == 0
def test_event_labels_hide_board_actor_but_keep_automatic_marker() -> None: def test_event_labels_hide_board_actor_but_keep_automatic_marker() -> None:
from ccma.domain.models import Event from ccma.domain.models import Event
from ccma.ui.member_tab import _event_label from ccma.ui.member_tab import _event_label