Release SyncVito 1.0.17

This commit is contained in:
Davito committed 2026-10-01 16:58:50 +02:00
1 parent 8218cfd9d5
commit f83a85ed40
10 files changed
+425 -29

No files matched your search

+145 -2
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import re
import sys
from datetime import datetime
from pathlib import Path
@@ -13,6 +14,7 @@ from PyQt6.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QFileDialog,
QFrame,
QHBoxLayout,
QHeaderView,
@@ -40,6 +42,7 @@ 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.task_transfer import TaskTransferError, export_task, export_tasks, import_task, import_tasks
from syncvito.theme import apply_theme
from syncvito.update_controller import UpdateController
from syncvito.update_dialog import UpdateDialog
@@ -251,6 +254,15 @@ class MainWindow(QMainWindow):
edit_button = QPushButton("Bearbeiten")
delete_button = QPushButton("Löschen")
delete_button.setProperty("danger", True)
transfer_button = QPushButton("Import / Export")
transfer_menu = QMenu(transfer_button)
transfer_menu.addSection("Exportieren")
transfer_menu.addAction("Ausgewählte Aufgabe exportieren …", self._export_selected_task)
transfer_menu.addAction("Alle Aufgaben exportieren …", self._export_all_tasks)
transfer_menu.addSection("Importieren")
transfer_menu.addAction("Einzelne Aufgabe laden …", self._import_single_task)
transfer_menu.addAction("Alle Aufgaben ersetzen …", self._import_all_tasks)
transfer_button.setMenu(transfer_menu)
self.sync_selected_button = QPushButton("Auswahl synchronisieren")
add_button.clicked.connect(self._add_task)
edit_button.clicked.connect(self._edit_task)
@@ -259,6 +271,7 @@ class MainWindow(QMainWindow):
toolbar.addWidget(add_button)
toolbar.addWidget(edit_button)
toolbar.addWidget(delete_button)
toolbar.addWidget(transfer_button)
toolbar.addStretch(1)
toolbar.addWidget(self.sync_selected_button)
layout.addLayout(toolbar)
@@ -567,6 +580,135 @@ class MainWindow(QMainWindow):
StateStore().remove(task.id)
self._persist_tasks("Aufgabe gelöscht")
def _export_selected_task(self) -> None:
task = self._selected_task()
if task is None:
QMessageBox.information(self, "Keine Auswahl", "Bitte zuerst eine Aufgabe auswählen.")
return
safe_name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", task.name).strip(" .")[:80] or "Aufgabe"
suggested_path = Path.home() / f"SyncVito-{safe_name}.json"
filename, _ = QFileDialog.getSaveFileName(
self,
"Aufgabe exportieren",
str(suggested_path),
"JSON-Dateien (*.json)",
)
if not filename:
return
export_path = self._json_export_path(filename)
try:
export_task(export_path, task)
except TaskTransferError as exc:
QMessageBox.critical(self, "Export fehlgeschlagen", str(exc))
self._append_log("Aufgabe exportiert", "Fehler", str(exc))
return
self._append_log("Aufgabe exportiert", "Erfolg", f"{task.name}: {export_path}")
QMessageBox.information(self, "Export abgeschlossen", f"Die Aufgabe wurde gespeichert unter:\n{export_path}")
def _export_all_tasks(self) -> None:
suggested_path = Path.home() / "SyncVito-Aufgaben.json"
filename, _ = QFileDialog.getSaveFileName(
self,
"Alle Aufgaben exportieren",
str(suggested_path),
"JSON-Dateien (*.json)",
)
if not filename:
return
export_path = self._json_export_path(filename)
try:
export_tasks(export_path, self.config.tasks)
except TaskTransferError as exc:
QMessageBox.critical(self, "Export fehlgeschlagen", str(exc))
self._append_log("Alle Aufgaben exportiert", "Fehler", str(exc))
return
detail = f"{len(self.config.tasks)} Aufgaben: {export_path}"
self._append_log("Alle Aufgaben exportiert", "Erfolg", detail)
QMessageBox.information(self, "Export abgeschlossen", f"Alle Aufgaben wurden gespeichert unter:\n{export_path}")
def _import_single_task(self) -> None:
filename, _ = QFileDialog.getOpenFileName(
self,
"Einzelne Aufgabe laden",
str(Path.home()),
"JSON-Dateien (*.json);;Alle Dateien (*)",
)
if not filename:
return
try:
task = import_task(Path(filename))
except TaskTransferError as exc:
QMessageBox.critical(self, "Import fehlgeschlagen", str(exc))
self._append_log("Aufgabe importiert", "Fehler", str(exc))
return
self.config.tasks.append(task)
if not self._persist_tasks("Aufgabe importiert"):
self.config.tasks.remove(task)
self._refresh_tasks()
return
QMessageBox.information(self, "Import abgeschlossen", f"„{task.name}“ wurde als neue Aufgabe hinzugefügt.")
def _import_all_tasks(self) -> None:
if self._has_pending_sync_work():
QMessageBox.information(
self,
"Synchronisation läuft",
"Alle Aufgaben können erst ersetzt werden, wenn keine Synchronisation läuft oder wartet.",
)
return
filename, _ = QFileDialog.getOpenFileName(
self,
"Alle Aufgaben laden",
str(Path.home()),
"JSON-Dateien (*.json);;Alle Dateien (*)",
)
if not filename:
return
try:
imported_tasks = import_tasks(Path(filename))
except TaskTransferError as exc:
QMessageBox.critical(self, "Import fehlgeschlagen", str(exc))
self._append_log("Alle Aufgaben importiert", "Fehler", str(exc))
return
answer = QMessageBox.question(
self,
"Alle Aufgaben ersetzen",
f"Die Datei enthält {len(imported_tasks)} Aufgaben. "
f"Damit werden alle {len(self.config.tasks)} vorhandenen Aufgaben ersetzt. Fortfahren?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if answer != QMessageBox.StandardButton.Yes:
return
if self._has_pending_sync_work():
QMessageBox.information(
self,
"Synchronisation läuft",
"Während des Imports wurde eine Synchronisation gestartet. Bitte den Import danach wiederholen.",
)
return
previous_tasks = self.config.tasks[:]
previous_queue = self.config.settings.pending_queue[:]
self.config.tasks[:] = imported_tasks
self.config.settings.pending_queue = []
if not self._persist_tasks("Alle Aufgaben importiert"):
self.config.tasks[:] = previous_tasks
self.config.settings.pending_queue = previous_queue
self._refresh_tasks()
return
QMessageBox.information(self, "Import abgeschlossen", f"{len(imported_tasks)} Aufgaben wurden geladen.")
@staticmethod
def _json_export_path(filename: str) -> Path:
path = Path(filename)
return path if path.suffix else path.with_suffix(".json")
def _has_pending_sync_work(self) -> bool:
return self.coordinator.is_running or bool(self.coordinator.queued_task_ids)
def _sync_selected(self) -> None:
task = self._selected_task()
if task is None:
@@ -576,16 +718,17 @@ class MainWindow(QMainWindow):
self.coordinator.set_paused(False)
self.coordinator.enqueue(task.id)
def _persist_tasks(self, message: str) -> None:
def _persist_tasks(self, message: str) -> bool:
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
return False
self._refresh_tasks()
self._append_log("Aufgaben", "Erfolg", message)
return True
def _save_settings(self) -> None:
settings = self.config.settings