3 Commits
Author SHA1 Message Date
Davito bb2a0db609 Release SyncVito 1.0.5 2026-09-24 13:10:07 +02:00
Davito 30982e8a4d Release SyncVito 1.0.3 2026-09-24 13:01:39 +02:00
Davito 8eb8cc1d0d Release SyncVito 1.0.2 2026-09-24 12:50:54 +02:00
8 changed files with 196 additions and 13 deletions

No files matched your search

+24
View File
@@ -1,5 +1,29 @@
# Ä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
- Der automatische Neustart nach einem internen Update setzt nun die geerbte PyInstaller-Laufzeitumgebung zurück und verhindert dadurch den Sicherheitsfehler beim Start der aktualisierten Anwendung.
## 1.0.2 – 2026-09-24
### Behoben
- Die Versionsanzeige in der Seitenleiste verwendet nun immer die zentrale Anwendungsversion.
- Unter der Versionsnummer wird jetzt „by Davito“ statt „Lokale Konfiguration“ angezeigt.
## 1.0.1 – 2026-09-23
### Behoben
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "syncvito"
version = "1.0.1"
version = "1.0.5"
description = "Windows-Desktopwerkzeug für lokale und NAS-Synchronisation"
readme = "README.md"
requires-python = ">=3.11"
+48 -11
View File
@@ -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,9 +172,10 @@ 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("Version 1.0.0\nLokale Konfiguration")
version = QLabel(f"Version {__version__}\nby Davito")
version.setProperty("muted", True)
layout.addWidget(version)
return sidebar
@@ -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)
+6
View File
@@ -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;
+1
View File
@@ -151,6 +151,7 @@ try {{
$bootstrapLog,
((Get-Date).ToString('o') + ' Starte verifizierten Installer.' + [Environment]::NewLine)
)
$env:PYINSTALLER_RESET_ENVIRONMENT = '1'
$setupArguments = @({setup_arguments})
& {_powershell_literal(installer_path)} @setupArguments
$setupExitCode = $LASTEXITCODE
+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
+3
View File
@@ -162,4 +162,7 @@ def test_update_watcher_waits_before_installer(monkeypatch: pytest.MonkeyPatch,
script = base64.b64decode(encoded).decode("utf-16-le")
assert "foreach ($processId in @(111, 222))" in script
assert script.index("while (Get-Process") < script.index(str(installer))
reset_environment = "$env:PYINSTALLER_RESET_ENVIRONMENT = '1'"
assert reset_environment in script
assert script.index(reset_environment) < script.index(f"& '{installer}'")
assert "'/VERYSILENT'" in script
+1 -1
View File
@@ -1 +1 @@
1.0.1
1.0.5