mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 11:14:52 +02:00
feat: configure minimum splash duration
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user