Files
Vault/src/ui/tray_app.py

386 lines
14 KiB
Python
Raw Normal View History

2026-01-30 07:07:11 +01:00
"""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())