First working, tray version
This commit is contained in:
385
src/ui/tray_app.py
Normal file
385
src/ui/tray_app.py
Normal file
@@ -0,0 +1,385 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user