386 lines
14 KiB
Python
386 lines
14 KiB
Python
"""System tray application for Vault.
|
|
|
|
Main entry point for the GUI application.
|
|
"""
|
|
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from loguru import logger
|
|
from PySide6.QtCore import QTimer
|
|
from PySide6.QtGui import QIcon
|
|
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon
|
|
|
|
from src.core.sync_manager import SyncStatus
|
|
from src.core.vault import Vault, VaultError, VaultState
|
|
from src.ui.dialogs.new_vault import NewVaultDialog
|
|
from src.ui.dialogs.open_vault import OpenVaultDialog
|
|
from src.ui.notifications import NotificationManager
|
|
|
|
|
|
class VaultTrayApp:
|
|
"""System tray application for managing Vault."""
|
|
|
|
def __init__(self) -> None:
|
|
self._app = QApplication(sys.argv)
|
|
self._app.setQuitOnLastWindowClosed(False)
|
|
self._app.setApplicationName("Vault")
|
|
|
|
self._vault = Vault(
|
|
on_state_change=self._on_vault_state_change,
|
|
on_sync_event=lambda e: self._on_sync_event(e),
|
|
)
|
|
self._notifications = NotificationManager()
|
|
self._space_warned = False
|
|
|
|
self._tray = QSystemTrayIcon()
|
|
self._setup_tray()
|
|
|
|
# Status check timer
|
|
self._status_timer = QTimer()
|
|
self._status_timer.timeout.connect(self._update_status)
|
|
self._status_timer.start(5000) # Check every 5 seconds
|
|
|
|
# Replica availability check timer
|
|
self._replica_timer = QTimer()
|
|
self._replica_timer.timeout.connect(self._check_replica_availability)
|
|
self._replica_timer.start(30000) # Check every 30 seconds
|
|
|
|
# Setup signal handlers for graceful shutdown
|
|
self._setup_signal_handlers()
|
|
|
|
def _setup_signal_handlers(self) -> None:
|
|
"""Setup signal handlers for graceful shutdown."""
|
|
def signal_handler(signum: int, frame: object) -> None:
|
|
logger.info(f"Received signal {signum}, shutting down gracefully...")
|
|
self._quit()
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
# Use a timer to allow signal processing in Qt event loop
|
|
self._signal_timer = QTimer()
|
|
self._signal_timer.timeout.connect(lambda: None) # Keep event loop responsive
|
|
self._signal_timer.start(500)
|
|
|
|
def _setup_tray(self) -> None:
|
|
"""Set up tray icon and menu."""
|
|
self._update_icon()
|
|
self._tray.setToolTip("Vault - Resilientní úložiště")
|
|
|
|
menu = QMenu()
|
|
|
|
# Status header
|
|
self._status_action = menu.addAction("Žádný vault otevřen")
|
|
self._status_action.setEnabled(False)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Open folder action
|
|
self._open_folder_action = menu.addAction("Otevřít složku")
|
|
self._open_folder_action.triggered.connect(self._open_folder)
|
|
self._open_folder_action.setEnabled(False)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Vault management
|
|
menu.addAction("Vytvořit nový vault...").triggered.connect(self._new_vault)
|
|
menu.addAction("Otevřít vault...").triggered.connect(self._open_vault)
|
|
self._close_action = menu.addAction("Zavřít vault")
|
|
self._close_action.triggered.connect(self._close_vault)
|
|
self._close_action.setEnabled(False)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Replica management
|
|
self._add_replica_action = menu.addAction("Přidat repliku...")
|
|
self._add_replica_action.triggered.connect(self._add_replica)
|
|
self._add_replica_action.setEnabled(False)
|
|
|
|
self._manage_replicas_action = menu.addAction("Spravovat repliky...")
|
|
self._manage_replicas_action.triggered.connect(self._manage_replicas)
|
|
self._manage_replicas_action.setEnabled(False)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Sync action
|
|
self._sync_action = menu.addAction("Synchronizovat")
|
|
self._sync_action.triggered.connect(self._manual_sync)
|
|
self._sync_action.setEnabled(False)
|
|
|
|
# Resize action
|
|
self._resize_action = menu.addAction("Zvětšit vault...")
|
|
self._resize_action.triggered.connect(self._resize_vault)
|
|
self._resize_action.setEnabled(False)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Quit
|
|
menu.addAction("Ukončit").triggered.connect(self._quit)
|
|
|
|
self._tray.setContextMenu(menu)
|
|
self._tray.activated.connect(self._on_tray_activated)
|
|
self._tray.show()
|
|
|
|
def _update_icon(self) -> None:
|
|
"""Update tray icon based on vault state."""
|
|
# Use built-in icons for now
|
|
if not self._vault.is_open:
|
|
# Gray - no vault open
|
|
icon = QIcon.fromTheme("folder-grey", QIcon.fromTheme("folder"))
|
|
elif self._vault.sync_status == SyncStatus.SYNCING:
|
|
# Blue - syncing
|
|
icon = QIcon.fromTheme("folder-sync", QIcon.fromTheme("folder-download"))
|
|
elif self._vault.sync_status == SyncStatus.ERROR:
|
|
# Red - error
|
|
icon = QIcon.fromTheme("folder-important", QIcon.fromTheme("dialog-error"))
|
|
elif self._vault.get_unavailable_replicas():
|
|
# Yellow - some replicas unavailable
|
|
icon = QIcon.fromTheme("folder-yellow", QIcon.fromTheme("folder-visiting"))
|
|
else:
|
|
# Green - all good
|
|
icon = QIcon.fromTheme("folder-green", QIcon.fromTheme("folder-open"))
|
|
|
|
self._tray.setIcon(icon)
|
|
|
|
def _update_status(self) -> None:
|
|
"""Update status display."""
|
|
self._update_icon()
|
|
|
|
if not self._vault.is_open:
|
|
self._status_action.setText("Žádný vault otevřen")
|
|
self._open_folder_action.setEnabled(False)
|
|
self._close_action.setEnabled(False)
|
|
self._add_replica_action.setEnabled(False)
|
|
self._manage_replicas_action.setEnabled(False)
|
|
self._sync_action.setEnabled(False)
|
|
self._resize_action.setEnabled(False)
|
|
else:
|
|
manifest = self._vault.manifest
|
|
name = manifest.vault_name if manifest else "Vault"
|
|
replicas = self._vault.replica_count
|
|
unavailable = self._vault.get_unavailable_replicas()
|
|
|
|
if unavailable:
|
|
online = replicas
|
|
total = replicas + len(unavailable)
|
|
status_text = f"{name} ({online}/{total} replik online)"
|
|
else:
|
|
status_text = f"{name} ({replicas} replik{'a' if replicas == 1 else 'y' if replicas < 5 else ''})"
|
|
|
|
if self._vault.sync_status == SyncStatus.SYNCING:
|
|
status_text += " - synchronizace..."
|
|
|
|
self._status_action.setText(status_text)
|
|
self._open_folder_action.setEnabled(True)
|
|
self._close_action.setEnabled(True)
|
|
self._add_replica_action.setEnabled(True)
|
|
self._manage_replicas_action.setEnabled(True)
|
|
self._sync_action.setEnabled(True)
|
|
self._resize_action.setEnabled(True)
|
|
|
|
# Check space warning
|
|
if self._vault.check_space_warning(90.0):
|
|
space_info = self._vault.get_space_info()
|
|
if space_info:
|
|
free_mb = space_info["free"] // (1024 * 1024)
|
|
if not hasattr(self, "_space_warned") or not self._space_warned:
|
|
self._notifications.notify(
|
|
"Vault téměř plný",
|
|
f"Zbývá pouze {free_mb} MB volného místa",
|
|
critical=True,
|
|
)
|
|
self._space_warned = True
|
|
else:
|
|
self._space_warned = False
|
|
|
|
def _check_replica_availability(self) -> None:
|
|
"""Check for replica availability and reconnect if possible."""
|
|
if not self._vault.is_open:
|
|
return
|
|
|
|
unavailable = self._vault.get_unavailable_replicas()
|
|
if unavailable:
|
|
# Try to reconnect
|
|
reconnected = self._vault.reconnect_unavailable_replicas()
|
|
if reconnected > 0:
|
|
self._notifications.notify(
|
|
"Repliky připojeny",
|
|
f"Připojeno {reconnected} replik{'a' if reconnected == 1 else 'y' if reconnected < 5 else ''}",
|
|
)
|
|
self._update_status()
|
|
|
|
def _on_vault_state_change(self, state: VaultState) -> None:
|
|
"""Handle vault state change."""
|
|
logger.debug(f"Vault state changed: {state}")
|
|
self._update_status()
|
|
|
|
if state == VaultState.OPEN:
|
|
self._notifications.notify(
|
|
"Vault otevřen",
|
|
f"Mount point: {self._vault.mount_point}",
|
|
)
|
|
elif state == VaultState.CLOSED:
|
|
self._notifications.notify("Vault zavřen", "")
|
|
elif state == VaultState.ERROR:
|
|
self._notifications.notify("Chyba", "Nastala chyba při práci s vault", critical=True)
|
|
|
|
def _on_sync_event(self, event: object) -> None:
|
|
"""Handle sync event."""
|
|
self._update_icon()
|
|
|
|
def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason) -> None:
|
|
"""Handle tray icon activation."""
|
|
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
|
self._open_folder()
|
|
|
|
def _open_folder(self) -> None:
|
|
"""Open vault mount point in file manager."""
|
|
if not self._vault.is_open or not self._vault.mount_point:
|
|
return
|
|
|
|
try:
|
|
# Try xdg-open (Linux)
|
|
subprocess.Popen(["xdg-open", str(self._vault.mount_point)])
|
|
except Exception as e:
|
|
logger.error(f"Failed to open folder: {e}")
|
|
self._notifications.notify("Chyba", f"Nepodařilo se otevřít složku: {e}", critical=True)
|
|
|
|
def _new_vault(self) -> None:
|
|
"""Show new vault dialog."""
|
|
dialog = NewVaultDialog()
|
|
if dialog.exec():
|
|
result = dialog.get_result()
|
|
try:
|
|
from src.core.image_manager import create_sparse_image
|
|
|
|
image_path = Path(result["path"])
|
|
create_sparse_image(image_path, result["size_mb"])
|
|
|
|
mount = self._vault.open(image_path)
|
|
|
|
# Update manifest with user-provided name
|
|
if self._vault.manifest:
|
|
self._vault.manifest.vault_name = result["name"]
|
|
self._vault.manifest.image_size_mb = result["size_mb"]
|
|
self._vault.manifest.save(mount)
|
|
|
|
self._update_status()
|
|
self._open_folder()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to create vault: {e}")
|
|
self._notifications.notify("Chyba", f"Nepodařilo se vytvořit vault: {e}", critical=True)
|
|
|
|
def _open_vault(self) -> None:
|
|
"""Show open vault dialog."""
|
|
dialog = OpenVaultDialog()
|
|
if dialog.exec():
|
|
vault_path = dialog.get_selected_path()
|
|
if vault_path:
|
|
try:
|
|
self._vault.open(Path(vault_path))
|
|
self._update_status()
|
|
self._open_folder()
|
|
except VaultError as e:
|
|
logger.error(f"Failed to open vault: {e}")
|
|
self._notifications.notify("Chyba", f"Nepodařilo se otevřít vault: {e}", critical=True)
|
|
|
|
def _close_vault(self) -> None:
|
|
"""Close current vault."""
|
|
if self._vault.is_open:
|
|
self._vault.close()
|
|
self._update_status()
|
|
|
|
def _add_replica(self) -> None:
|
|
"""Show add replica dialog."""
|
|
if not self._vault.is_open:
|
|
return
|
|
|
|
from PySide6.QtWidgets import QFileDialog
|
|
|
|
path, _ = QFileDialog.getSaveFileName(
|
|
None,
|
|
"Vyberte umístění pro novou repliku",
|
|
"",
|
|
"Vault soubory (*.vault)",
|
|
)
|
|
|
|
if path:
|
|
if not path.endswith(".vault"):
|
|
path += ".vault"
|
|
try:
|
|
self._vault.add_replica(Path(path))
|
|
self._notifications.notify("Replika přidána", f"Nová replika: {path}")
|
|
self._update_status()
|
|
except VaultError as e:
|
|
logger.error(f"Failed to add replica: {e}")
|
|
self._notifications.notify("Chyba", f"Nepodařilo se přidat repliku: {e}", critical=True)
|
|
|
|
def _manage_replicas(self) -> None:
|
|
"""Show replica management dialog."""
|
|
if not self._vault.is_open:
|
|
return
|
|
|
|
from src.ui.dialogs.manage_replicas import ManageReplicasDialog
|
|
|
|
dialog = ManageReplicasDialog(self._vault)
|
|
dialog.exec()
|
|
self._update_status()
|
|
|
|
def _manual_sync(self) -> None:
|
|
"""Trigger manual synchronization."""
|
|
if self._vault.is_open:
|
|
try:
|
|
self._vault.sync()
|
|
self._notifications.notify("Synchronizace dokončena", "")
|
|
except VaultError as e:
|
|
logger.error(f"Sync failed: {e}")
|
|
self._notifications.notify("Chyba synchronizace", str(e), critical=True)
|
|
|
|
def _resize_vault(self) -> None:
|
|
"""Show resize vault dialog."""
|
|
if not self._vault.is_open:
|
|
return
|
|
|
|
from src.ui.dialogs.resize_vault import ResizeVaultDialog
|
|
|
|
dialog = ResizeVaultDialog(self._vault)
|
|
if dialog.exec():
|
|
new_size = dialog.get_new_size()
|
|
if new_size:
|
|
try:
|
|
self._vault.resize(new_size)
|
|
self._notifications.notify(
|
|
"Vault zvětšen",
|
|
f"Nová velikost: {new_size} MB",
|
|
)
|
|
self._update_status()
|
|
except VaultError as e:
|
|
logger.error(f"Resize failed: {e}")
|
|
self._notifications.notify("Chyba", f"Nepodařilo se zvětšit vault: {e}", critical=True)
|
|
|
|
def _quit(self) -> None:
|
|
"""Quit the application."""
|
|
if self._vault.is_open:
|
|
self._vault.close()
|
|
self._tray.hide()
|
|
self._app.quit()
|
|
|
|
def run(self) -> int:
|
|
"""Run the application."""
|
|
logger.info("Vault tray application starting...")
|
|
return self._app.exec()
|
|
|
|
|
|
def main() -> int:
|
|
"""Main entry point for tray application."""
|
|
app = VaultTrayApp()
|
|
return app.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|