Initial SyncVito release with installer updater
This commit is contained in:
commit
b9e4c09f49
41 files changed
+4331
No files matched your search
@@ -0,0 +1,670 @@
|
||||
"""Hauptfenster mit Aufgaben-, Protokoll- und Einstellungsansicht."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSignal
|
||||
from PyQt6.QtGui import QAction, QCloseEvent, QFontDatabase
|
||||
from PyQt6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QProgressBar,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSlider,
|
||||
QSpinBox,
|
||||
QStackedWidget,
|
||||
QSystemTrayIcon,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from syncvito.autostart import set_autostart
|
||||
from syncvito.config_store import ConfigError, ConfigStore
|
||||
from syncvito.coordinator import SyncCoordinator
|
||||
from syncvito.dialogs import TaskDialog
|
||||
from syncvito.icons import app_icon, status_icon
|
||||
from syncvito.models import AppConfig, SyncDirection, SyncTask, TaskRuntimeState, TriggerMode
|
||||
from syncvito.state_store import StateStore
|
||||
from syncvito.theme import apply_theme
|
||||
from syncvito.update_controller import UpdateController
|
||||
from syncvito.update_dialog import UpdateDialog
|
||||
from syncvito.updater import UpdateInfo
|
||||
from syncvito.version import __version__
|
||||
|
||||
__all__ = ["MainWindow"]
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Zentrale SyncVito-Benutzeroberfläche."""
|
||||
|
||||
quit_requested = pyqtSignal()
|
||||
|
||||
def __init__(self, config: AppConfig, config_store: ConfigStore) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.config_store = config_store
|
||||
self._allow_close = False
|
||||
self.setWindowTitle("SyncVito")
|
||||
self.setWindowIcon(app_icon())
|
||||
self.setMinimumSize(980, 680)
|
||||
self.resize(1220, 800)
|
||||
|
||||
self.coordinator = SyncCoordinator(config, config_store, self)
|
||||
self.updater = UpdateController(__version__, self)
|
||||
self._build_ui()
|
||||
self._build_tray()
|
||||
self._connect_coordinator()
|
||||
self._refresh_tasks()
|
||||
self._set_global_status(*self.coordinator.current_overall_status())
|
||||
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))
|
||||
if "--updated" in sys.argv:
|
||||
QTimer.singleShot(900, self._show_update_completed)
|
||||
|
||||
def show_from_tray(self) -> None:
|
||||
"""Stellt das Fenster aus dem Tray wieder her."""
|
||||
self.showNormal()
|
||||
self.activateWindow()
|
||||
self.raise_()
|
||||
|
||||
def request_quit(self) -> None:
|
||||
"""Beendet Observer und Anwendung explizit über das Tray-Menü."""
|
||||
self._allow_close = True
|
||||
self.coordinator.stop()
|
||||
self.tray.hide()
|
||||
QApplication.instance().quit()
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 - Qt API
|
||||
if self._allow_close:
|
||||
event.accept()
|
||||
return
|
||||
event.ignore()
|
||||
self.hide()
|
||||
self.tray.showMessage(
|
||||
"SyncVito läuft weiter",
|
||||
"Das Fenster wurde in den Infobereich verschoben.",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
2500,
|
||||
)
|
||||
|
||||
def changeEvent(self, event: QEvent) -> None: # noqa: N802 - Qt API
|
||||
super().changeEvent(event)
|
||||
if event.type() == QEvent.Type.WindowStateChange and self.isMinimized():
|
||||
QApplication.processEvents()
|
||||
self.hide()
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
root = QWidget()
|
||||
self.setCentralWidget(root)
|
||||
root_layout = QHBoxLayout(root)
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
root_layout.setSpacing(0)
|
||||
root_layout.addWidget(self._build_sidebar())
|
||||
|
||||
right = QWidget()
|
||||
right_layout = QVBoxLayout(right)
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
right_layout.setSpacing(0)
|
||||
right_layout.addWidget(self._build_header())
|
||||
self.stack = QStackedWidget()
|
||||
self.tasks_page = self._build_tasks_page()
|
||||
self.log_page = self._build_log_page()
|
||||
self.settings_page = self._build_settings_page()
|
||||
self.stack.addWidget(self.tasks_page)
|
||||
self.stack.addWidget(self.log_page)
|
||||
self.stack.addWidget(self.settings_page)
|
||||
right_layout.addWidget(self.stack, 1)
|
||||
right_layout.addWidget(self._build_progress_dock())
|
||||
root_layout.addWidget(right, 1)
|
||||
|
||||
def _build_sidebar(self) -> QWidget:
|
||||
sidebar = QWidget()
|
||||
sidebar.setObjectName("Sidebar")
|
||||
sidebar.setFixedWidth(226)
|
||||
layout = QVBoxLayout(sidebar)
|
||||
layout.setContentsMargins(18, 22, 18, 18)
|
||||
layout.setSpacing(8)
|
||||
|
||||
eyebrow = QLabel("DATEISYNCHRONISATION")
|
||||
eyebrow.setObjectName("Eyebrow")
|
||||
brand = QLabel("SyncVito")
|
||||
brand.setObjectName("Brand")
|
||||
layout.addWidget(eyebrow)
|
||||
layout.addWidget(brand)
|
||||
layout.addSpacing(28)
|
||||
|
||||
self.nav_buttons: list[QPushButton] = []
|
||||
for title, index in (("Aufgaben", 0), ("Protokoll", 1), ("Einstellungen", 2)):
|
||||
button = QPushButton(title)
|
||||
button.setCheckable(True)
|
||||
button.setProperty("nav", True)
|
||||
button.setMinimumHeight(42)
|
||||
button.clicked.connect(lambda checked=False, page=index: self._select_page(page))
|
||||
layout.addWidget(button)
|
||||
self.nav_buttons.append(button)
|
||||
self.nav_buttons[0].setChecked(True)
|
||||
layout.addStretch(1)
|
||||
|
||||
version = QLabel("Version 1.0.0\nLokale Konfiguration")
|
||||
version.setProperty("muted", True)
|
||||
layout.addWidget(version)
|
||||
return sidebar
|
||||
|
||||
def _build_header(self) -> QWidget:
|
||||
header = QWidget()
|
||||
header.setObjectName("Header")
|
||||
header.setFixedHeight(82)
|
||||
layout = QHBoxLayout(header)
|
||||
layout.setContentsMargins(28, 0, 28, 0)
|
||||
title_area = QVBoxLayout()
|
||||
title_area.setSpacing(2)
|
||||
self.page_title = QLabel("Synchronisationsaufgaben")
|
||||
self.page_title.setObjectName("PageTitle")
|
||||
self.page_subtitle = QLabel("Lokale Ordner und NAS-Ziele zuverlässig im gleichen Stand halten")
|
||||
self.page_subtitle.setObjectName("PageSubtitle")
|
||||
title_area.addWidget(self.page_title)
|
||||
title_area.addWidget(self.page_subtitle)
|
||||
layout.addLayout(title_area)
|
||||
layout.addStretch(1)
|
||||
self.status_label = QLabel("Bereit")
|
||||
self.status_label.setObjectName("StatusPill")
|
||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.status_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
||||
layout.addWidget(self.status_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
self.sync_all_button = QPushButton("Alle jetzt synchronisieren")
|
||||
self.sync_all_button.setProperty("primary", True)
|
||||
self.sync_all_button.clicked.connect(self.coordinator.enqueue_all)
|
||||
layout.addWidget(self.sync_all_button, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
return header
|
||||
|
||||
def _build_tasks_page(self) -> QWidget:
|
||||
page = QWidget()
|
||||
layout = QVBoxLayout(page)
|
||||
layout.setContentsMargins(28, 24, 28, 24)
|
||||
layout.setSpacing(14)
|
||||
toolbar = QHBoxLayout()
|
||||
add_button = QPushButton("Neue Aufgabe")
|
||||
add_button.setProperty("primary", True)
|
||||
edit_button = QPushButton("Bearbeiten")
|
||||
delete_button = QPushButton("Löschen")
|
||||
delete_button.setProperty("danger", True)
|
||||
self.sync_selected_button = QPushButton("Auswahl synchronisieren")
|
||||
add_button.clicked.connect(self._add_task)
|
||||
edit_button.clicked.connect(self._edit_task)
|
||||
delete_button.clicked.connect(self._delete_task)
|
||||
self.sync_selected_button.clicked.connect(self._sync_selected)
|
||||
toolbar.addWidget(add_button)
|
||||
toolbar.addWidget(edit_button)
|
||||
toolbar.addWidget(delete_button)
|
||||
toolbar.addStretch(1)
|
||||
toolbar.addWidget(self.sync_selected_button)
|
||||
layout.addLayout(toolbar)
|
||||
|
||||
self.task_table = QTableWidget(0, 5)
|
||||
self.task_table.setHorizontalHeaderLabels(["Aufgabe", "Ordnerpaar", "Richtung", "Auslösung", "Status"])
|
||||
self.task_table.setAlternatingRowColors(True)
|
||||
self.task_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.task_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.task_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.task_table.verticalHeader().setVisible(False)
|
||||
self.task_table.verticalHeader().setDefaultSectionSize(58)
|
||||
header = self.task_table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.task_table.doubleClicked.connect(self._edit_task)
|
||||
layout.addWidget(self.task_table, 1)
|
||||
|
||||
self.lock_banner = QLabel()
|
||||
self.lock_banner.setObjectName("LockBanner")
|
||||
self.lock_banner.setWordWrap(True)
|
||||
self.lock_banner.hide()
|
||||
layout.addWidget(self.lock_banner)
|
||||
return page
|
||||
|
||||
def _build_log_page(self) -> QWidget:
|
||||
page = QWidget()
|
||||
layout = QVBoxLayout(page)
|
||||
layout.setContentsMargins(28, 24, 28, 24)
|
||||
layout.setSpacing(12)
|
||||
self.log_table = QTableWidget(0, 4)
|
||||
self.log_table.setHorizontalHeaderLabels(["Zeitpunkt", "Aktion", "Ergebnis", "Details"])
|
||||
self.log_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.log_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.log_table.setAlternatingRowColors(True)
|
||||
self.log_table.verticalHeader().setVisible(False)
|
||||
header = self.log_table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||
clear_button = QPushButton("Ansicht leeren")
|
||||
clear_button.clicked.connect(lambda: self.log_table.setRowCount(0))
|
||||
button_row = QHBoxLayout()
|
||||
button_row.addStretch(1)
|
||||
button_row.addWidget(clear_button)
|
||||
layout.addLayout(button_row)
|
||||
layout.addWidget(self.log_table, 1)
|
||||
return page
|
||||
|
||||
def _build_settings_page(self) -> QWidget:
|
||||
page = QWidget()
|
||||
outer = QVBoxLayout(page)
|
||||
outer.setContentsMargins(28, 24, 28, 24)
|
||||
outer.setSpacing(16)
|
||||
card = QFrame()
|
||||
card.setProperty("card", True)
|
||||
layout = QVBoxLayout(card)
|
||||
layout.setContentsMargins(22, 22, 22, 22)
|
||||
layout.setSpacing(16)
|
||||
heading = QLabel("Darstellung und Systemstart")
|
||||
heading.setObjectName("PageTitle")
|
||||
layout.addWidget(heading)
|
||||
|
||||
self.autostart_check = QCheckBox("SyncVito mit Windows starten")
|
||||
self.autostart_check.setChecked(self.config.settings.autostart)
|
||||
layout.addWidget(self.autostart_check)
|
||||
layout.addSpacing(6)
|
||||
|
||||
font_row = QHBoxLayout()
|
||||
font_row.addWidget(QLabel("Schriftart"))
|
||||
self.font_combo = QComboBox()
|
||||
self.font_combo.addItems(QFontDatabase.families())
|
||||
font_index = self.font_combo.findText(self.config.settings.font_family)
|
||||
self.font_combo.setCurrentIndex(max(0, font_index))
|
||||
font_row.addWidget(self.font_combo, 1)
|
||||
font_row.addWidget(QLabel("Größe"))
|
||||
self.font_size_spin = QSpinBox()
|
||||
self.font_size_spin.setRange(8, 18)
|
||||
self.font_size_spin.setValue(self.config.settings.font_size)
|
||||
font_row.addWidget(self.font_size_spin)
|
||||
font_row.addWidget(QLabel("Schnitt"))
|
||||
self.font_weight_combo = QComboBox()
|
||||
self.font_weight_combo.addItem("Regulär", "regular")
|
||||
self.font_weight_combo.addItem("Fett", "bold")
|
||||
self.font_weight_combo.setCurrentIndex(1 if self.config.settings.font_weight == "bold" else 0)
|
||||
font_row.addWidget(self.font_weight_combo)
|
||||
layout.addLayout(font_row)
|
||||
|
||||
scale_row = QHBoxLayout()
|
||||
scale_row.addWidget(QLabel("UI-Skalierung"))
|
||||
self.scale_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.scale_slider.setRange(80, 150)
|
||||
self.scale_slider.setTickInterval(10)
|
||||
self.scale_slider.setValue(self.config.settings.ui_scale)
|
||||
self.scale_value = QLabel(f"{self.config.settings.ui_scale} %")
|
||||
self.scale_value.setMinimumWidth(52)
|
||||
self.scale_slider.valueChanged.connect(lambda value: self.scale_value.setText(f"{value} %"))
|
||||
scale_row.addWidget(self.scale_slider, 1)
|
||||
scale_row.addWidget(self.scale_value)
|
||||
layout.addLayout(scale_row)
|
||||
|
||||
hint = QLabel("Änderungen an Schrift und Skalierung werden sofort angewendet und lokal gespeichert.")
|
||||
hint.setProperty("muted", True)
|
||||
layout.addWidget(hint)
|
||||
save_button = QPushButton("Einstellungen übernehmen")
|
||||
save_button.setProperty("primary", True)
|
||||
save_button.clicked.connect(self._save_settings)
|
||||
action_row = QHBoxLayout()
|
||||
action_row.addStretch(1)
|
||||
action_row.addWidget(save_button)
|
||||
layout.addLayout(action_row)
|
||||
outer.addWidget(card)
|
||||
|
||||
update_card = QFrame()
|
||||
update_card.setProperty("card", True)
|
||||
update_layout = QVBoxLayout(update_card)
|
||||
update_layout.setContentsMargins(22, 22, 22, 22)
|
||||
update_layout.setSpacing(12)
|
||||
update_heading = QLabel("Programmupdates")
|
||||
update_heading.setObjectName("PageTitle")
|
||||
update_layout.addWidget(update_heading)
|
||||
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.setChecked(self.config.settings.check_updates_on_start)
|
||||
update_layout.addWidget(self.update_check_box)
|
||||
update_hint = QLabel(
|
||||
"Updates werden ausschließlich aus den öffentlichen Releases des SyncVito-Gitea-Repositories geladen "
|
||||
"und vor der Installation per SHA-256 geprüft."
|
||||
)
|
||||
update_hint.setWordWrap(True)
|
||||
update_hint.setProperty("muted", True)
|
||||
update_layout.addWidget(update_hint)
|
||||
self.update_button = QPushButton("Nach Updates suchen")
|
||||
self.update_button.clicked.connect(lambda: self._check_updates(manual=True))
|
||||
update_actions = QHBoxLayout()
|
||||
update_actions.addStretch(1)
|
||||
update_actions.addWidget(self.update_button)
|
||||
update_layout.addLayout(update_actions)
|
||||
outer.addWidget(update_card)
|
||||
outer.addStretch(1)
|
||||
return page
|
||||
|
||||
def _build_progress_dock(self) -> QWidget:
|
||||
dock = QWidget()
|
||||
dock.setObjectName("ProgressDock")
|
||||
dock.setFixedHeight(66)
|
||||
layout = QHBoxLayout(dock)
|
||||
layout.setContentsMargins(28, 12, 28, 12)
|
||||
self.progress_text = QLabel("Bereit")
|
||||
self.progress_text.setMinimumWidth(250)
|
||||
self.progress_text.setProperty("muted", True)
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setFormat("%p %")
|
||||
layout.addWidget(self.progress_text)
|
||||
layout.addWidget(self.progress_bar, 1)
|
||||
return dock
|
||||
|
||||
def _build_tray(self) -> None:
|
||||
self.tray = QSystemTrayIcon(status_icon("active"), self)
|
||||
self.tray.setToolTip("SyncVito – Bereit")
|
||||
menu = QMenu()
|
||||
open_action = QAction("Öffnen", self)
|
||||
self.tray_sync_action = QAction("Sync jetzt", self)
|
||||
self.tray_update_action = QAction("Nach Updates suchen", self)
|
||||
self.pause_action = QAction("Pausieren", self)
|
||||
quit_action = QAction("Beenden", self)
|
||||
open_action.triggered.connect(self.show_from_tray)
|
||||
self.tray_sync_action.triggered.connect(self.coordinator.enqueue_all)
|
||||
self.tray_update_action.triggered.connect(lambda: self._check_updates(manual=True))
|
||||
self.pause_action.triggered.connect(self._toggle_pause)
|
||||
quit_action.triggered.connect(self.request_quit)
|
||||
menu.addAction(open_action)
|
||||
menu.addAction(self.tray_sync_action)
|
||||
menu.addAction(self.pause_action)
|
||||
menu.addAction(self.tray_update_action)
|
||||
menu.addSeparator()
|
||||
menu.addAction(quit_action)
|
||||
self.tray.setContextMenu(menu)
|
||||
self.tray.activated.connect(
|
||||
lambda reason: self.show_from_tray() if reason == QSystemTrayIcon.ActivationReason.DoubleClick else None
|
||||
)
|
||||
self.tray.show()
|
||||
self._update_pause_action()
|
||||
|
||||
def _connect_coordinator(self) -> None:
|
||||
self.coordinator.progress_changed.connect(self._update_progress)
|
||||
self.coordinator.log_created.connect(self._append_log)
|
||||
self.coordinator.overall_status_changed.connect(self._set_global_status)
|
||||
self.coordinator.task_state_changed.connect(self._set_task_state)
|
||||
self.coordinator.lock_changed.connect(self._set_lock_message)
|
||||
self.coordinator.idle_changed.connect(lambda _idle: self._refresh_action_availability())
|
||||
self.updater.update_available.connect(self._on_update_available)
|
||||
self.updater.no_update.connect(self._on_no_update)
|
||||
self.updater.error.connect(self._on_update_error)
|
||||
self.updater.progress.connect(self._update_progress)
|
||||
self.updater.install_ready.connect(self._install_update)
|
||||
self.updater.busy_changed.connect(lambda _busy: self._refresh_action_availability())
|
||||
|
||||
def _select_page(self, index: int) -> None:
|
||||
titles = [
|
||||
("Synchronisationsaufgaben", "Lokale Ordner und NAS-Ziele zuverlässig im gleichen Stand halten"),
|
||||
("Aktivitätsprotokoll", "Erfolge, Konflikte und Fehler transparent nachvollziehen"),
|
||||
("Einstellungen", "Darstellung und Windows-Integration anpassen"),
|
||||
]
|
||||
self.stack.setCurrentIndex(index)
|
||||
self.page_title.setText(titles[index][0])
|
||||
self.page_subtitle.setText(titles[index][1])
|
||||
for button_index, button in enumerate(self.nav_buttons):
|
||||
button.setChecked(button_index == index)
|
||||
|
||||
def _refresh_tasks(self) -> None:
|
||||
self.task_table.setRowCount(len(self.config.tasks))
|
||||
for row, task in enumerate(self.config.tasks):
|
||||
name = QTableWidgetItem(task.name)
|
||||
name.setData(Qt.ItemDataRole.UserRole, task.id)
|
||||
pair = QTableWidgetItem(f"{task.source}\n→ {task.target}")
|
||||
direction = QTableWidgetItem("Einweg" if task.direction is SyncDirection.ONE_WAY else "Bidirektional")
|
||||
trigger_text = {
|
||||
TriggerMode.MANUAL: "Manuell",
|
||||
TriggerMode.REALTIME: "Echtzeit",
|
||||
TriggerMode.INTERVAL: f"Alle {task.interval_minutes} Min.",
|
||||
}[task.trigger]
|
||||
trigger = QTableWidgetItem(trigger_text)
|
||||
status = QTableWidgetItem(self._runtime_state_text(self.coordinator.task_state(task.id)))
|
||||
for column, item in enumerate((name, pair, direction, trigger, status)):
|
||||
item.setToolTip(item.text())
|
||||
self.task_table.setItem(row, column, item)
|
||||
|
||||
def _selected_task(self) -> SyncTask | None:
|
||||
row = self.task_table.currentRow()
|
||||
if row < 0:
|
||||
return None
|
||||
item = self.task_table.item(row, 0)
|
||||
task_id = item.data(Qt.ItemDataRole.UserRole)
|
||||
return next((task for task in self.config.tasks if task.id == task_id), None)
|
||||
|
||||
def _add_task(self) -> None:
|
||||
dialog = TaskDialog(parent=self)
|
||||
if dialog.exec() == TaskDialog.DialogCode.Accepted:
|
||||
self.config.tasks.append(dialog.task())
|
||||
self._persist_tasks("Aufgabe angelegt")
|
||||
|
||||
def _edit_task(self) -> None:
|
||||
task = self._selected_task()
|
||||
if task is None:
|
||||
QMessageBox.information(self, "Keine Auswahl", "Bitte zuerst eine Aufgabe auswählen.")
|
||||
return
|
||||
dialog = TaskDialog(task, self)
|
||||
if dialog.exec() == TaskDialog.DialogCode.Accepted:
|
||||
updated = dialog.task()
|
||||
index = self.config.tasks.index(task)
|
||||
self.config.tasks[index] = updated
|
||||
self._persist_tasks("Aufgabe aktualisiert")
|
||||
|
||||
def _delete_task(self) -> None:
|
||||
task = self._selected_task()
|
||||
if task is None:
|
||||
QMessageBox.information(self, "Keine Auswahl", "Bitte zuerst eine Aufgabe auswählen.")
|
||||
return
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"Aufgabe löschen",
|
||||
f"Soll „{task.name}“ wirklich gelöscht werden? Dateien werden dabei nicht entfernt.",
|
||||
)
|
||||
if answer != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
self.config.tasks.remove(task)
|
||||
StateStore().remove(task.id)
|
||||
self._persist_tasks("Aufgabe gelöscht")
|
||||
|
||||
def _sync_selected(self) -> None:
|
||||
task = self._selected_task()
|
||||
if task is None:
|
||||
QMessageBox.information(self, "Keine Auswahl", "Bitte zuerst eine Aufgabe auswählen.")
|
||||
return
|
||||
self.coordinator.enqueue(task.id)
|
||||
|
||||
def _persist_tasks(self, message: str) -> None:
|
||||
try:
|
||||
self.config_store.save(self.config)
|
||||
self.coordinator.refresh_task_states()
|
||||
self.coordinator.rebuild_monitors()
|
||||
except ConfigError as exc:
|
||||
QMessageBox.critical(self, "Speicherfehler", str(exc))
|
||||
return
|
||||
self._refresh_tasks()
|
||||
self._append_log("Aufgaben", "Erfolg", message)
|
||||
|
||||
def _save_settings(self) -> None:
|
||||
settings = self.config.settings
|
||||
old_autostart = settings.autostart
|
||||
settings.autostart = self.autostart_check.isChecked()
|
||||
settings.font_family = self.font_combo.currentText()
|
||||
settings.font_size = self.font_size_spin.value()
|
||||
settings.font_weight = self.font_weight_combo.currentData()
|
||||
settings.ui_scale = self.scale_slider.value()
|
||||
settings.check_updates_on_start = self.update_check_box.isChecked()
|
||||
settings.validate()
|
||||
try:
|
||||
if old_autostart != settings.autostart:
|
||||
set_autostart(settings.autostart)
|
||||
self.config_store.save(self.config)
|
||||
except (ConfigError, OSError) as exc:
|
||||
QMessageBox.critical(self, "Einstellungen konnten nicht gespeichert werden", str(exc))
|
||||
settings.autostart = old_autostart
|
||||
return
|
||||
apply_theme(QApplication.instance(), settings)
|
||||
self._append_log("Einstellungen", "Erfolg", "Darstellung und Systemstart wurden übernommen.")
|
||||
|
||||
def _automatic_update_check(self) -> None:
|
||||
settings = self.config.settings
|
||||
if not settings.check_updates_on_start or self.coordinator.is_running:
|
||||
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
|
||||
self._check_updates(manual=False)
|
||||
|
||||
def _check_updates(self, manual: bool) -> None:
|
||||
if self.coordinator.is_running:
|
||||
if manual:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Synchronisation läuft",
|
||||
"Bitte die laufende Synchronisation abschließen lassen und die Updateprüfung erneut starten.",
|
||||
)
|
||||
return
|
||||
self._update_progress(0, "Update wird geprüft …")
|
||||
self.updater.check(manual)
|
||||
|
||||
def _on_update_available(self, info: UpdateInfo, manual: bool) -> None:
|
||||
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.")
|
||||
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._update_progress(100, "SyncVito ist aktuell")
|
||||
self._append_log("Update", "Erfolg", "Die installierte Version ist aktuell.")
|
||||
if manual:
|
||||
QMessageBox.information(self, "Keine Updates", "Du verwendest bereits die aktuelle SyncVito-Version.")
|
||||
|
||||
def _on_update_error(self, message: str, manual: bool) -> None:
|
||||
self._update_progress(0, "Updateprüfung fehlgeschlagen")
|
||||
self._append_log("Update", "Fehler", message)
|
||||
if manual:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Update nicht möglich",
|
||||
f"{message}\n\nBitte die Netzwerkverbindung prüfen und anschließend erneut versuchen.",
|
||||
)
|
||||
|
||||
def _install_update(self, path: Path, info: UpdateInfo) -> None:
|
||||
try:
|
||||
self.updater.service.launch_installer(path, info.version)
|
||||
except Exception as exc: # noqa: BLE001 - UI-Grenze verhindert einen Anwendungsabbruch.
|
||||
self._on_update_error(str(exc), True)
|
||||
return
|
||||
self._append_log("Update", "Erfolg", f"Version {info.version} wird installiert.")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Update wird installiert",
|
||||
"SyncVito wird jetzt geschlossen, aktualisiert und automatisch neu gestartet.",
|
||||
)
|
||||
self.request_quit()
|
||||
|
||||
def _show_update_completed(self) -> None:
|
||||
message = f"Update abgeschlossen – SyncVito {__version__} wurde erfolgreich gestartet."
|
||||
self._append_log("Update", "Erfolg", message)
|
||||
self.tray.showMessage("SyncVito aktualisiert", message, QSystemTrayIcon.MessageIcon.Information, 5000)
|
||||
|
||||
def _cleanup_update_installers(self, attempts_remaining: int = 1) -> None:
|
||||
_removed, pending = self.updater.service.cleanup_installers()
|
||||
if pending and attempts_remaining > 1:
|
||||
QTimer.singleShot(
|
||||
3000,
|
||||
lambda: self._cleanup_update_installers(attempts_remaining - 1),
|
||||
)
|
||||
|
||||
def _mark_update_checked(self) -> None:
|
||||
self.config.settings.last_update_check = datetime.now().isoformat(timespec="seconds")
|
||||
try:
|
||||
self.config_store.save(self.config)
|
||||
except ConfigError as exc:
|
||||
self._append_log("Update", "Fehler", str(exc))
|
||||
|
||||
def _refresh_action_availability(self) -> None:
|
||||
update_idle = not self.updater.busy
|
||||
sync_idle = not self.coordinator.is_running
|
||||
self.sync_all_button.setEnabled(update_idle and sync_idle)
|
||||
self.sync_selected_button.setEnabled(update_idle and sync_idle)
|
||||
self.tray_sync_action.setEnabled(update_idle and sync_idle)
|
||||
self.update_button.setEnabled(update_idle and sync_idle)
|
||||
self.tray_update_action.setEnabled(update_idle and sync_idle)
|
||||
|
||||
def _toggle_pause(self) -> None:
|
||||
self.coordinator.set_paused(not self.config.settings.paused)
|
||||
self._update_pause_action()
|
||||
|
||||
def _update_pause_action(self) -> None:
|
||||
self.pause_action.setText("Fortsetzen" if self.config.settings.paused else "Pausieren")
|
||||
|
||||
def _update_progress(self, value: int, detail: str) -> None:
|
||||
self.progress_bar.setValue(value)
|
||||
self.progress_text.setText(detail)
|
||||
|
||||
def _append_log(self, action: str, level: str, detail: str) -> None:
|
||||
row = self.log_table.rowCount()
|
||||
self.log_table.insertRow(row)
|
||||
values = (datetime.now().strftime("%d.%m.%Y %H:%M:%S"), action, level, detail)
|
||||
for column, value in enumerate(values):
|
||||
item = QTableWidgetItem(value)
|
||||
item.setToolTip(value)
|
||||
self.log_table.setItem(row, column, item)
|
||||
if self.log_table.rowCount() > 1000:
|
||||
self.log_table.removeRow(0)
|
||||
self.log_table.scrollToBottom()
|
||||
|
||||
def _set_global_status(self, status: str, text: str) -> None:
|
||||
colors = {"active": "#35C77A", "paused": "#E0525D", "error": "#F0B84B"}
|
||||
self.status_label.setText(text)
|
||||
self.status_label.setStyleSheet(f"color: {colors.get(status, '#E8EDF4')};")
|
||||
self.tray.setIcon(status_icon(status))
|
||||
self.tray.setToolTip(f"SyncVito – {text}")
|
||||
self._update_pause_action()
|
||||
|
||||
def _set_task_state(self, task_id: str, state: TaskRuntimeState) -> None:
|
||||
self._refresh_tasks()
|
||||
|
||||
def _set_lock_message(self, message: str) -> None:
|
||||
self.lock_banner.setText(message)
|
||||
self.lock_banner.setVisible(bool(message))
|
||||
|
||||
@staticmethod
|
||||
def _runtime_state_text(state: TaskRuntimeState) -> str:
|
||||
return {
|
||||
TaskRuntimeState.ACTIVE: "Aktiv",
|
||||
TaskRuntimeState.PAUSED: "Pausiert",
|
||||
TaskRuntimeState.WAITING_FOR_RELEASE: "Wartet auf Freigabe",
|
||||
TaskRuntimeState.ERROR: "Fehler",
|
||||
}[state]
|
||||
Reference in new issue
Block a user