La fenêtre de saisie ne s'ouvrait qu'une fois, juste après une connexion manuelle réussie. Un refus, une fenêtre fermée, une reconnexion automatique — qui n'ouvre volontairement aucune modale — ou l'expiration de l'autorisation au bout de 12 h laissaient un tunnel monté, un réseau muet et aucun élément d'interface pour ressaisir un code. Un bouton sous l'action de connexion et une entrée de systray apparaissent désormais dès qu'une saisie a un sens : identifiant VPN configuré, tunnel monté, accès non ouvert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
5.1 KiB
Python
133 lines
5.1 KiB
Python
from PyQt6.QtWidgets import QSystemTrayIcon, QMenu, QApplication
|
|
from PyQt6.QtGui import QAction
|
|
from PyQt6.QtCore import QTimer
|
|
|
|
from app.core.config import Config
|
|
from app.core import wireguard as wg_core
|
|
from app.ui import icons
|
|
|
|
|
|
class SystemTray(QSystemTrayIcon):
|
|
def __init__(self, config: Config, main_window, parent=None):
|
|
super().__init__(icons.icon_disconnected(), parent)
|
|
self._cfg = config
|
|
self._win = main_window
|
|
self._connected = False
|
|
self._build_menu()
|
|
self.setToolTip("WGSecure — Déconnecté")
|
|
self.activated.connect(self._on_activated)
|
|
|
|
self._poll = QTimer(parent)
|
|
self._poll.timeout.connect(self._update_status)
|
|
self._poll.start(5000)
|
|
self._update_status()
|
|
|
|
def _build_menu(self):
|
|
menu = QMenu()
|
|
|
|
# Ligne de statut (désactivée)
|
|
self._action_status = QAction("● Déconnecté", self)
|
|
self._action_status.setEnabled(False)
|
|
menu.addAction(self._action_status)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Afficher la fenêtre
|
|
self._action_show = QAction(icons.icon_app(), "Afficher la fenêtre", self)
|
|
self._action_show.triggered.connect(self._show_window)
|
|
menu.addAction(self._action_show)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Connexion / déconnexion
|
|
self._action_toggle = QAction(icons.icon_disconnected(), "Se connecter", self)
|
|
self._action_toggle.triggered.connect(self._toggle_connection)
|
|
menu.addAction(self._action_toggle)
|
|
|
|
# Saisie du code d'accès : le tunnel monte en quarantaine et
|
|
# l'autorisation expire d'elle-même, il faut donc pouvoir y revenir
|
|
# sans passer par une déconnexion. Masquée quand elle n'a pas lieu
|
|
# d'être — pas d'identifiant VPN, tunnel à l'arrêt, accès déjà ouvert.
|
|
self._action_access = QAction("🔐 Saisir le code d'accès", self)
|
|
self._action_access.triggered.connect(self._win.request_access_code)
|
|
self._action_access.setVisible(False)
|
|
menu.addAction(self._action_access)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Panneau de configuration
|
|
self._action_config = QAction(icons.icon_admin(), "Configuration", self)
|
|
self._action_config.triggered.connect(self._win._open_admin)
|
|
menu.addAction(self._action_config)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Quitter
|
|
self._action_quit = QAction("Quitter", self)
|
|
self._action_quit.triggered.connect(self._quit)
|
|
menu.addAction(self._action_quit)
|
|
|
|
self.setContextMenu(menu)
|
|
|
|
def _on_activated(self, reason: QSystemTrayIcon.ActivationReason):
|
|
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
|
self._show_window()
|
|
|
|
def _show_window(self):
|
|
self._win.show()
|
|
self._win.raise_()
|
|
self._win.activateWindow()
|
|
|
|
def _update_status(self):
|
|
prev = self._connected
|
|
try:
|
|
self._connected = wg_core.is_connected(self._cfg)
|
|
except Exception:
|
|
self._connected = False
|
|
|
|
# La fenêtre principale est seule à savoir si le serveur a ouvert
|
|
# l'accès : elle tient cet état, qu'aucune commande WireGuard ne
|
|
# rapporte — le tunnel est identique, ouvert ou en quarantaine.
|
|
self._action_access.setVisible(self._win.access_code_needed())
|
|
|
|
if self._connected:
|
|
self.setIcon(icons.icon_connected())
|
|
self.setToolTip("WGSecure — Connecté")
|
|
self._action_status.setText("● Connecté")
|
|
self._action_toggle.setIcon(icons.icon_connected())
|
|
self._action_toggle.setText("Se déconnecter")
|
|
if not prev:
|
|
srv = self._cfg.wg.get("server_endpoint", "")
|
|
self.showMessage(
|
|
"WGSecure — Connecté ✓",
|
|
f"Tunnel WireGuard actif{(' → ' + srv) if srv else ''}",
|
|
QSystemTrayIcon.MessageIcon.Information, 3000,
|
|
)
|
|
else:
|
|
self.setIcon(icons.icon_disconnected())
|
|
self.setToolTip("WGSecure — Déconnecté")
|
|
self._action_status.setText("● Déconnecté")
|
|
self._action_toggle.setIcon(icons.icon_disconnected())
|
|
self._action_toggle.setText("Se connecter")
|
|
if prev:
|
|
self.showMessage(
|
|
"WGSecure — Déconnecté",
|
|
"Le tunnel WireGuard a été désactivé.",
|
|
QSystemTrayIcon.MessageIcon.Warning, 3000,
|
|
)
|
|
|
|
def _toggle_connection(self):
|
|
self._win._on_connect()
|
|
# La (dé)connexion est désormais asynchrone : l'icône sera mise à jour
|
|
# par la scrutation périodique une fois l'opération terminée.
|
|
|
|
def _quit(self):
|
|
self._poll.stop()
|
|
self.hide()
|
|
# prepare_quit() arrête les minuteries, attend les threads en cours,
|
|
# démonte le tunnel et retire la configuration DNS qu'il avait posée.
|
|
# Sans cela, quitter en étant connecté laissait la machine sans
|
|
# résolution de noms.
|
|
self._win.prepare_quit()
|
|
QApplication.quit()
|