From bb2a0db609efdb9ea0764961e123e4dfba430952 Mon Sep 17 00:00:00 2001 From: Davito <1+davito@noreply.localhost> Date: Thu, 24 Sep 2026 13:10:07 +0200 Subject: [PATCH] Release SyncVito 1.0.5 --- CHANGELOG.md | 11 ++++ pyproject.toml | 2 +- src/syncvito/main_window.py | 57 ++++++++++++++--- src/syncvito/theme.py | 6 ++ tests/test_update_schedule.py | 112 ++++++++++++++++++++++++++++++++++ version.txt | 2 +- 6 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 tests/test_update_schedule.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ddb05..4bc1e0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Änderungsprotokoll +## 1.0.5 – 2026-09-24 + +### Neu + +- SyncVito prüft bei jedem Programmstart und während des Betriebs alle 30 Minuten automatisch auf Updates. +- Verfügbare Updates werden zusätzlich zum Hinweisfenster mit einer auffälligen „UPDATE“-Kennzeichnung am Eintrag „Einstellungen“ angezeigt. + +### Behoben + +- Wird eine automatische Prüfung durch eine laufende Synchronisation oder Updateaktion verhindert, wird sie nach einer Minute erneut versucht. + ## 1.0.3 – 2026-09-24 ### Behoben diff --git a/pyproject.toml b/pyproject.toml index 94d7146..3ba94a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "syncvito" -version = "1.0.3" +version = "1.0.5" description = "Windows-Desktopwerkzeug für lokale und NAS-Synchronisation" readme = "README.md" requires-python = ">=3.11" diff --git a/src/syncvito/main_window.py b/src/syncvito/main_window.py index 3b2325d..c60242c 100644 --- a/src/syncvito/main_window.py +++ b/src/syncvito/main_window.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys -from datetime import datetime, timedelta +from datetime import datetime from pathlib import Path from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSignal @@ -48,6 +48,9 @@ from syncvito.version import __version__ __all__ = ["MainWindow"] +UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1000 +UPDATE_RETRY_INTERVAL_MS = 60 * 1000 + class MainWindow(QMainWindow): """Zentrale SyncVito-Benutzeroberfläche.""" @@ -59,6 +62,7 @@ class MainWindow(QMainWindow): self.config = config self.config_store = config_store self._allow_close = False + self._notified_update_version: str | None = None self.setWindowTitle("SyncVito") self.setWindowIcon(app_icon()) self.setMinimumSize(980, 680) @@ -71,6 +75,15 @@ class MainWindow(QMainWindow): self._connect_coordinator() self._refresh_tasks() self._set_global_status(*self.coordinator.current_overall_status()) + + self.update_check_timer = QTimer(self) + self.update_check_timer.setInterval(UPDATE_CHECK_INTERVAL_MS) + self.update_check_timer.timeout.connect(self._automatic_update_check) + self.update_check_timer.start() + self.update_retry_timer = QTimer(self) + self.update_retry_timer.setSingleShot(True) + self.update_retry_timer.setInterval(UPDATE_RETRY_INTERVAL_MS) + self.update_retry_timer.timeout.connect(self._automatic_update_check) QTimer.singleShot(3000, self._automatic_update_check) cleanup_attempts = 6 if "--updated" in sys.argv else 1 QTimer.singleShot(3000, lambda: self._cleanup_update_installers(cleanup_attempts)) @@ -159,6 +172,7 @@ class MainWindow(QMainWindow): layout.addWidget(button) self.nav_buttons.append(button) self.nav_buttons[0].setChecked(True) + self.settings_nav_button = self.nav_buttons[2] layout.addStretch(1) version = QLabel(f"Version {__version__}\nby Davito") @@ -340,7 +354,7 @@ class MainWindow(QMainWindow): version_label = QLabel(f"Installierte Version: {__version__}") version_label.setProperty("muted", True) update_layout.addWidget(version_label) - self.update_check_box = QCheckBox("Beim Start automatisch nach Updates suchen") + self.update_check_box = QCheckBox("Beim Start und danach alle 30 Minuten automatisch nach Updates suchen") self.update_check_box.setChecked(self.config.settings.check_updates_on_start) update_layout.addWidget(self.update_check_box) update_hint = QLabel( @@ -511,6 +525,7 @@ class MainWindow(QMainWindow): def _save_settings(self) -> None: settings = self.config.settings old_autostart = settings.autostart + old_update_check = settings.check_updates_on_start settings.autostart = self.autostart_check.isChecked() settings.font_family = self.font_combo.currentText() settings.font_size = self.font_size_spin.value() @@ -528,18 +543,22 @@ class MainWindow(QMainWindow): return apply_theme(QApplication.instance(), settings) self._append_log("Einstellungen", "Erfolg", "Darstellung und Systemstart wurden übernommen.") + if settings.check_updates_on_start and not old_update_check: + QTimer.singleShot(0, self._automatic_update_check) + elif not settings.check_updates_on_start: + self.update_retry_timer.stop() def _automatic_update_check(self) -> None: settings = self.config.settings - if not settings.check_updates_on_start or self.coordinator.is_running: + if not settings.check_updates_on_start: + self.update_retry_timer.stop() return - if settings.last_update_check: - try: - last_check = datetime.fromisoformat(settings.last_update_check) - except ValueError: - last_check = datetime.min - if datetime.now() - last_check < timedelta(hours=24): - return + if self.coordinator.is_running or self.updater.busy: + if not self.update_retry_timer.isActive(): + self.update_retry_timer.start() + return + self.update_retry_timer.stop() + self.update_check_timer.start(UPDATE_CHECK_INTERVAL_MS) self._check_updates(manual=False) def _check_updates(self, manual: bool) -> None: @@ -558,12 +577,18 @@ class MainWindow(QMainWindow): self._mark_update_checked() self._update_progress(100, f"Version {info.version} ist verfügbar") self._append_log("Update", "Hinweis", f"Version {info.version} ist verfügbar.") + self._set_update_indicator(info.version) + if not manual and self._notified_update_version == info.version: + return + self._notified_update_version = info.version dialog = UpdateDialog(__version__, info, getattr(sys, "frozen", False), self) if dialog.exec() == UpdateDialog.DialogCode.Accepted: self.updater.download(info) def _on_no_update(self, manual: bool) -> None: self._mark_update_checked() + self._set_update_indicator(None) + self._notified_update_version = None self._update_progress(100, "SyncVito ist aktuell") self._append_log("Update", "Erfolg", "Die installierte Version ist aktuell.") if manual: @@ -579,6 +604,18 @@ class MainWindow(QMainWindow): f"{message}\n\nBitte die Netzwerkverbindung prüfen und anschließend erneut versuchen.", ) + def _set_update_indicator(self, version: str | None) -> None: + available = bool(version) + self.settings_nav_button.setText("Einstellungen • UPDATE" if available else "Einstellungen") + self.settings_nav_button.setProperty("updateAvailable", available) + description = f"SyncVito {version} ist verfügbar" if version else "" + self.settings_nav_button.setToolTip(description) + self.settings_nav_button.setAccessibleDescription(description) + style = self.settings_nav_button.style() + style.unpolish(self.settings_nav_button) + style.polish(self.settings_nav_button) + self.settings_nav_button.update() + def _install_update(self, path: Path, info: UpdateInfo) -> None: try: self.updater.service.launch_installer(path, info.version) diff --git a/src/syncvito/theme.py b/src/syncvito/theme.py index 84714d9..9aeedd0 100644 --- a/src/syncvito/theme.py +++ b/src/syncvito/theme.py @@ -52,6 +52,12 @@ def _stylesheet(scale: float) -> str: padding: {round(10 * scale)}px {round(12 * scale)}px; color: #9CACBF; }} QPushButton[nav="true"]:hover {{ background: #17212E; color: #E8EDF4; }} QPushButton[nav="true"]:checked {{ background: #1B2A3D; color: #FFFFFF; border-color: #2A568B; }} + QPushButton[nav="true"][updateAvailable="true"] {{ background: #3A2412; color: #FFF0DF; + border-color: #EA580C; }} + QPushButton[nav="true"][updateAvailable="true"]:hover {{ background: #4A2D14; color: #FFFFFF; + border-color: #FF7A2F; }} + QPushButton[nav="true"][updateAvailable="true"]:focus {{ border: 2px solid #FF9A5B; + padding: {round(9 * scale)}px {round(11 * scale)}px; }} QLineEdit, QComboBox, QSpinBox, QListWidget, QTreeView, QTableWidget {{ background: #0E141D; border: 1px solid #304052; border-radius: {radius}px; padding: {round(7 * scale)}px; color: #E8EDF4; diff --git a/tests/test_update_schedule.py b/tests/test_update_schedule.py new file mode 100644 index 0000000..4edb584 --- /dev/null +++ b/tests/test_update_schedule.py @@ -0,0 +1,112 @@ +from types import SimpleNamespace + +from syncvito.main_window import UPDATE_CHECK_INTERVAL_MS, UPDATE_RETRY_INTERVAL_MS, MainWindow + + +class FakeTimer: + def __init__(self, *, active: bool = False) -> None: + self.active = active + self.start_calls: list[int | None] = [] + self.stop_calls = 0 + + def isActive(self) -> bool: # noqa: N802 - Qt API + return self.active + + def start(self, interval: int | None = None) -> None: + self.active = True + self.start_calls.append(interval) + + def stop(self) -> None: + self.active = False + self.stop_calls += 1 + + +class FakeStyle: + def __init__(self) -> None: + self.unpolished = False + self.polished = False + + def unpolish(self, _button: object) -> None: + self.unpolished = True + + def polish(self, _button: object) -> None: + self.polished = True + + +class FakeButton: + def __init__(self) -> None: + self.text = "" + self.properties: dict[str, object] = {} + self.tooltip = "" + self.accessible_description = "" + self.button_style = FakeStyle() + self.updated = False + + def setText(self, text: str) -> None: # noqa: N802 - Qt API + self.text = text + + def setProperty(self, name: str, value: object) -> None: # noqa: N802 - Qt API + self.properties[name] = value + + def setToolTip(self, text: str) -> None: # noqa: N802 - Qt API + self.tooltip = text + + def setAccessibleDescription(self, text: str) -> None: # noqa: N802 - Qt API + self.accessible_description = text + + def style(self) -> FakeStyle: + return self.button_style + + def update(self) -> None: + self.updated = True + + +def _window(*, sync_running: bool = False, updater_busy: bool = False) -> SimpleNamespace: + checks: list[bool] = [] + return SimpleNamespace( + config=SimpleNamespace(settings=SimpleNamespace(check_updates_on_start=True)), + coordinator=SimpleNamespace(is_running=sync_running), + updater=SimpleNamespace(busy=updater_busy), + update_check_timer=FakeTimer(active=True), + update_retry_timer=FakeTimer(), + _check_updates=lambda manual: checks.append(manual), + checks=checks, + ) + + +def test_automatic_update_check_runs_without_daily_throttle() -> None: + window = _window() + + MainWindow._automatic_update_check(window) + + assert window.checks == [False] + assert window.update_check_timer.start_calls == [UPDATE_CHECK_INTERVAL_MS] + assert window.update_retry_timer.stop_calls == 1 + + +def test_automatic_update_check_retries_while_sync_is_running() -> None: + window = _window(sync_running=True) + + MainWindow._automatic_update_check(window) + + assert window.checks == [] + assert window.update_retry_timer.start_calls == [None] + assert UPDATE_RETRY_INTERVAL_MS == 60_000 + + +def test_periodic_update_interval_is_thirty_minutes() -> None: + assert UPDATE_CHECK_INTERVAL_MS == 30 * 60 * 1000 + + +def test_update_indicator_uses_text_and_accessible_description() -> None: + button = FakeButton() + window = SimpleNamespace(settings_nav_button=button) + + MainWindow._set_update_indicator(window, "1.0.5") + + assert button.text == "Einstellungen • UPDATE" + assert button.properties["updateAvailable"] is True + assert button.tooltip == "SyncVito 1.0.5 ist verfügbar" + assert button.accessible_description == button.tooltip + assert button.button_style.unpolished and button.button_style.polished + assert button.updated diff --git a/version.txt b/version.txt index 21e8796..90a27f9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.3 +1.0.5