1 Commits
Author SHA1 Message Date
Davito bb2a0db609 Release SyncVito 1.0.5 2026-09-24 13:10:07 +02:00
6 changed files with 177 additions and 11 deletions

No files matched your search

+11
View File
@@ -1,5 +1,16 @@
# Änderungsprotokoll # Ä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 ## 1.0.3 – 2026-09-24
### Behoben ### Behoben
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "syncvito" name = "syncvito"
version = "1.0.3" version = "1.0.5"
description = "Windows-Desktopwerkzeug für lokale und NAS-Synchronisation" description = "Windows-Desktopwerkzeug für lokale und NAS-Synchronisation"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
+46 -9
View File
@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from datetime import datetime, timedelta from datetime import datetime
from pathlib import Path from pathlib import Path
from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSignal from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSignal
@@ -48,6 +48,9 @@ from syncvito.version import __version__
__all__ = ["MainWindow"] __all__ = ["MainWindow"]
UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1000
UPDATE_RETRY_INTERVAL_MS = 60 * 1000
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
"""Zentrale SyncVito-Benutzeroberfläche.""" """Zentrale SyncVito-Benutzeroberfläche."""
@@ -59,6 +62,7 @@ class MainWindow(QMainWindow):
self.config = config self.config = config
self.config_store = config_store self.config_store = config_store
self._allow_close = False self._allow_close = False
self._notified_update_version: str | None = None
self.setWindowTitle("SyncVito") self.setWindowTitle("SyncVito")
self.setWindowIcon(app_icon()) self.setWindowIcon(app_icon())
self.setMinimumSize(980, 680) self.setMinimumSize(980, 680)
@@ -71,6 +75,15 @@ class MainWindow(QMainWindow):
self._connect_coordinator() self._connect_coordinator()
self._refresh_tasks() self._refresh_tasks()
self._set_global_status(*self.coordinator.current_overall_status()) 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) QTimer.singleShot(3000, self._automatic_update_check)
cleanup_attempts = 6 if "--updated" in sys.argv else 1 cleanup_attempts = 6 if "--updated" in sys.argv else 1
QTimer.singleShot(3000, lambda: self._cleanup_update_installers(cleanup_attempts)) QTimer.singleShot(3000, lambda: self._cleanup_update_installers(cleanup_attempts))
@@ -159,6 +172,7 @@ class MainWindow(QMainWindow):
layout.addWidget(button) layout.addWidget(button)
self.nav_buttons.append(button) self.nav_buttons.append(button)
self.nav_buttons[0].setChecked(True) self.nav_buttons[0].setChecked(True)
self.settings_nav_button = self.nav_buttons[2]
layout.addStretch(1) layout.addStretch(1)
version = QLabel(f"Version {__version__}\nby Davito") version = QLabel(f"Version {__version__}\nby Davito")
@@ -340,7 +354,7 @@ class MainWindow(QMainWindow):
version_label = QLabel(f"Installierte Version: {__version__}") version_label = QLabel(f"Installierte Version: {__version__}")
version_label.setProperty("muted", True) version_label.setProperty("muted", True)
update_layout.addWidget(version_label) 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) self.update_check_box.setChecked(self.config.settings.check_updates_on_start)
update_layout.addWidget(self.update_check_box) update_layout.addWidget(self.update_check_box)
update_hint = QLabel( update_hint = QLabel(
@@ -511,6 +525,7 @@ class MainWindow(QMainWindow):
def _save_settings(self) -> None: def _save_settings(self) -> None:
settings = self.config.settings settings = self.config.settings
old_autostart = settings.autostart old_autostart = settings.autostart
old_update_check = settings.check_updates_on_start
settings.autostart = self.autostart_check.isChecked() settings.autostart = self.autostart_check.isChecked()
settings.font_family = self.font_combo.currentText() settings.font_family = self.font_combo.currentText()
settings.font_size = self.font_size_spin.value() settings.font_size = self.font_size_spin.value()
@@ -528,18 +543,22 @@ class MainWindow(QMainWindow):
return return
apply_theme(QApplication.instance(), settings) apply_theme(QApplication.instance(), settings)
self._append_log("Einstellungen", "Erfolg", "Darstellung und Systemstart wurden übernommen.") 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: def _automatic_update_check(self) -> None:
settings = self.config.settings 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 return
if settings.last_update_check: if self.coordinator.is_running or self.updater.busy:
try: if not self.update_retry_timer.isActive():
last_check = datetime.fromisoformat(settings.last_update_check) self.update_retry_timer.start()
except ValueError:
last_check = datetime.min
if datetime.now() - last_check < timedelta(hours=24):
return return
self.update_retry_timer.stop()
self.update_check_timer.start(UPDATE_CHECK_INTERVAL_MS)
self._check_updates(manual=False) self._check_updates(manual=False)
def _check_updates(self, manual: bool) -> None: def _check_updates(self, manual: bool) -> None:
@@ -558,12 +577,18 @@ class MainWindow(QMainWindow):
self._mark_update_checked() self._mark_update_checked()
self._update_progress(100, f"Version {info.version} ist verfügbar") self._update_progress(100, f"Version {info.version} ist verfügbar")
self._append_log("Update", "Hinweis", 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) dialog = UpdateDialog(__version__, info, getattr(sys, "frozen", False), self)
if dialog.exec() == UpdateDialog.DialogCode.Accepted: if dialog.exec() == UpdateDialog.DialogCode.Accepted:
self.updater.download(info) self.updater.download(info)
def _on_no_update(self, manual: bool) -> None: def _on_no_update(self, manual: bool) -> None:
self._mark_update_checked() self._mark_update_checked()
self._set_update_indicator(None)
self._notified_update_version = None
self._update_progress(100, "SyncVito ist aktuell") self._update_progress(100, "SyncVito ist aktuell")
self._append_log("Update", "Erfolg", "Die installierte Version ist aktuell.") self._append_log("Update", "Erfolg", "Die installierte Version ist aktuell.")
if manual: if manual:
@@ -579,6 +604,18 @@ class MainWindow(QMainWindow):
f"{message}\n\nBitte die Netzwerkverbindung prüfen und anschließend erneut versuchen.", 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: def _install_update(self, path: Path, info: UpdateInfo) -> None:
try: try:
self.updater.service.launch_installer(path, info.version) self.updater.service.launch_installer(path, info.version)
+6
View File
@@ -52,6 +52,12 @@ def _stylesheet(scale: float) -> str:
padding: {round(10 * scale)}px {round(12 * scale)}px; color: #9CACBF; }} padding: {round(10 * scale)}px {round(12 * scale)}px; color: #9CACBF; }}
QPushButton[nav="true"]:hover {{ background: #17212E; color: #E8EDF4; }} QPushButton[nav="true"]:hover {{ background: #17212E; color: #E8EDF4; }}
QPushButton[nav="true"]:checked {{ background: #1B2A3D; color: #FFFFFF; border-color: #2A568B; }} 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 {{ QLineEdit, QComboBox, QSpinBox, QListWidget, QTreeView, QTableWidget {{
background: #0E141D; border: 1px solid #304052; border-radius: {radius}px; background: #0E141D; border: 1px solid #304052; border-radius: {radius}px;
padding: {round(7 * scale)}px; color: #E8EDF4; padding: {round(7 * scale)}px; color: #E8EDF4;
+112
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
1.0.3 1.0.5