Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb2a0db609 | ||
|
|
30982e8a4d |
No files matched your search
@@ -1,5 +1,22 @@
|
|||||||
# Ä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
|
||||||
|
|
||||||
|
### 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
|
## 1.0.2 – 2026-09-24
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "syncvito"
|
name = "syncvito"
|
||||||
version = "1.0.2"
|
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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ try {{
|
|||||||
$bootstrapLog,
|
$bootstrapLog,
|
||||||
((Get-Date).ToString('o') + ' Starte verifizierten Installer.' + [Environment]::NewLine)
|
((Get-Date).ToString('o') + ' Starte verifizierten Installer.' + [Environment]::NewLine)
|
||||||
)
|
)
|
||||||
|
$env:PYINSTALLER_RESET_ENVIRONMENT = '1'
|
||||||
$setupArguments = @({setup_arguments})
|
$setupArguments = @({setup_arguments})
|
||||||
& {_powershell_literal(installer_path)} @setupArguments
|
& {_powershell_literal(installer_path)} @setupArguments
|
||||||
$setupExitCode = $LASTEXITCODE
|
$setupExitCode = $LASTEXITCODE
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -162,4 +162,7 @@ def test_update_watcher_waits_before_installer(monkeypatch: pytest.MonkeyPatch,
|
|||||||
script = base64.b64decode(encoded).decode("utf-16-le")
|
script = base64.b64decode(encoded).decode("utf-16-le")
|
||||||
assert "foreach ($processId in @(111, 222))" in script
|
assert "foreach ($processId in @(111, 222))" in script
|
||||||
assert script.index("while (Get-Process") < script.index(str(installer))
|
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
|
assert "'/VERYSILENT'" in script
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.0.2
|
1.0.5
|
||||||
Reference in new issue
Block a user