diff --git a/src/controllers/window_dragger.py b/src/controllers/window_dragger.py index 07e7909..27dc7d3 100644 --- a/src/controllers/window_dragger.py +++ b/src/controllers/window_dragger.py @@ -4,22 +4,22 @@ from PySide6.QtWidgets import QMainWindow class WindowDragger: - # Permet de déplacer une fenêtre sans barre de titre. - def __init__(self, window: QMainWindow): - self._window = window + self._window = window self._drag_pos = None def mouse_press(self, event: QtGui.QMouseEvent) -> None: if event.button() == Qt.MouseButton.LeftButton: - self._drag_pos = ( - event.globalPosition().toPoint() - - self._window.frameGeometry().topLeft() - ) + # On stocke le vecteur entre le clic et le coin haut-gauche de la fenêtre + self._drag_pos = event.globalPosition().toPoint() - self._window.frameGeometry().topLeft() + event.accept() # On informe Qt que l'event est géré def mouse_move(self, event: QtGui.QMouseEvent) -> None: - if event.buttons() & Qt.MouseButton.LeftButton and self._drag_pos is not None: + # Vérification stricte du bouton gauche ET de l'existence du point d'ancrage + if (event.buttons() & Qt.MouseButton.LeftButton) and self._drag_pos is not None: self._window.move(event.globalPosition().toPoint() - self._drag_pos) + event.accept() - def mouse_release(self, _event) -> None: + def mouse_release(self, event: QtGui.QMouseEvent) -> None: self._drag_pos = None + event.accept() diff --git a/src/main.py b/src/main.py index 1cb570d..c78110f 100644 --- a/src/main.py +++ b/src/main.py @@ -3,9 +3,10 @@ import sys import os import tempfile +from pathlib import Path + from tools.utils import get_internal_dir -from PySide6.QtCore import QResource from PySide6.QtGui import QFontDatabase, QFont, QIcon from PySide6.QtWidgets import QApplication @@ -30,7 +31,6 @@ if getattr(sys, 'frozen', False): # Bundle path resolution # --------------------------------------------------------------------------- bundle_dir = get_internal_dir() -QResource.registerResource(f"{bundle_dir}/resources.py") # --------------------------------------------------------------------------- # Fix barre des tâches Windows @@ -38,6 +38,29 @@ QResource.registerResource(f"{bundle_dir}/resources.py") if sys.platform.startswith("win"): ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("LaTaniere.Launcher.1") + +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- +def setup_environment(app, bundle_dir): + # Utilisation de pathlib pour la robustesse + style_path = Path(bundle_dir) / "styles" / "styles.qss" + + # Tentative de chargement du style + if style_path.exists(): + try: + app.setStyleSheet(style_path.read_text(encoding="utf-8")) + except Exception as e: + print(f"⚠️ Impossible de lire le fichier de style: {e}") + + # Tentative de chargement de la police + try: + font_family = load_custom_font() + app.setFont(QFont(font_family, 16)) + except Exception as e: + print(f"⚠️ Police personnalisée non chargée, repli sur la police système: {e}") + app.setFont(QFont("Segoe UI", 11)) # Fallback standard + # --------------------------------------------------------------------------- # Font helper # --------------------------------------------------------------------------- diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 7732dc7..627be39 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -22,6 +22,7 @@ from fivemserver.whitelistmanager import WhiteList from fivemserver.fivemlauncher import FiveMLauncher from fivemserver.queuemanager import QueueManager from fivemserver.get_server_token import GetServerTokenForDiscord +from fivemserver.auth_worker import AuthWorker from fake_patch_notes import patch_note # For Linux Wayland to authorize moving window @@ -37,6 +38,8 @@ class MainWindow(QMainWindow): self.ui = Ui_MainWindow() self.ui.setupUi(self) + self.auth_worker = None # <--- Pour stocker l'instance du thread + self.config = config_manager self.stored_user_id = self.config.get_discord_user() self.queue_thread = None @@ -45,6 +48,15 @@ class MainWindow(QMainWindow): self.countdown_timer = None self.remaining_time = 0 # en secondes + # Préparation du timer de fermeture finale + self.close_timer = QTimer(self) + self.close_timer.setSingleShot(True) + self.close_timer.timeout.connect(self.close) + + # Préparation du timer de mise à jour visuelle (1s) + self.countdown_timer = QTimer(self) + self.countdown_timer.timeout.connect(self._update_countdown) + # UI # self.ui = QUiLoader().load(f"{bundle_dir}/ui/mainwindow_vertical_pager.ui", self) #self.setCentralWidget(self.ui.centralWidget()) @@ -158,46 +170,29 @@ class MainWindow(QMainWindow): # ------------------------------------------------------------------ def _on_connexion(self) -> None: - try: - if not self.stored_user_id or self.stored_user_id.isspace(): - show_qt_error( - self, - "Connexion impossible", - "Aucun identifiant Discord n'est disponible. Merci de passer par la connexion Discord." - ) - return + """Appelée lors du clic sur le bouton connexion.""" - self._ensure_server_session() + # 1. Sécurités de base + if not self.stored_user_id or self.stored_user_id.isspace(): + show_qt_error(self, "Connexion impossible", "Identifiant Discord absent.") + return - if not PlayerServerInfo.session_id: - PlayerServerInfo.session_id = GetServerTokenForDiscord.authenticate(Urls.API_URL.value) - GetServerTokenForDiscord.register_discord_user( - self.stored_user_id, - PlayerServerInfo.session_id, - ) + # 2. Si on a déjà une session valide, on passe directement à la suite + if PlayerServerInfo.session_id: + self._proceed_to_queue_or_launch() + return - if self.queue_position_value is None: - self.start_queue() - return + # 3. Sinon, on lance l'authentification en arrière-plan + # Verrouillage de l'UI pour éviter le double-clic + self.ui.connexion_btn.setEnabled(False) + self.ui.connexion_btn.setText("Authentification...") + QGuiApplication.setOverrideCursor(Qt.WaitCursor) # Curseur de chargement - if self.queue_position_value != 0: - show_qt_error( - self, - "Connexion en attente", - f"Tu dois attendre ton tour.\n\nPosition actuelle : {self.queue_position_value}" - ) - return - - FiveMLauncher.launch() - # on disable le bouton connexion une fois fivem lancé. - self.ui.connexion_btn.setEnabled(False) - - # fermer l'application après 60 secondes (60000 ms) - self.schedule_close() - - - except ApiError as exc: - show_qt_error(self, "Connexion impossible", f"Erreur lors de la connexion.\n\n{exc}") + # Création et lancement du worker + self.auth_worker = AuthWorker(self.stored_user_id) + self.auth_worker.finished.connect(self._on_auth_finished) + self.auth_worker.finished.connect(self.auth_worker.deleteLater) # Nettoyage automatique + self.auth_worker.start() @staticmethod def _on_discord() -> None: @@ -209,43 +204,76 @@ class MainWindow(QMainWindow): def _on_discord_auth_btn(self) -> None: try: + # 1. Récupération OAuth (souvent via navigateur, assez rapide) test = discord_oauth.get_discord_user_id() - self.config.set_discord_user(test[0]) - PlayerServerInfo.session_id = test[1] - self.stored_user_id = test[0] + user_id = test[0] + token = test[1] + + # 2. Mise à jour locale immédiate + self.config.set_discord_user(user_id) + self.stored_user_id = user_id + PlayerServerInfo.session_id = token self.config.save() - self._ensure_server_session() + # 3. Lancement du Worker pour la partie réseau (Enregistrement serveur) + # On verrouille l'UI pour éviter les clics multiples + self.ui.discord_auth_btn.setEnabled(False) + QGuiApplication.setOverrideCursor(Qt.WaitCursor) + self.auth_worker = AuthWorker(self.stored_user_id) + # On connecte le signal de fin à une nouvelle méthode dédiée + self.auth_worker.finished.connect(self._on_discord_auth_finished) + self.auth_worker.finished.connect(self.auth_worker.deleteLater) + self.auth_worker.start() + + except ApiError as exc: + show_qt_error(self, "Connexion Discord", f"Impossible de récupérer ton compte Discord.\n\n{exc}") + except Exception as exc: + show_qt_error(self, "Erreur", f"Une erreur inattendue est survenue : {exc}") + + def _on_discord_auth_finished(self, success: bool, session_id: str, error_message: str): + """Callback après l'enregistrement suite à une auth Discord.""" + QGuiApplication.restoreOverrideCursor() + self.ui.discord_auth_btn.setEnabled(True) + + if success: try: + # Maintenant que la session est OK côté serveur, on vérifie la Whitelist WhiteList.check_whitelist(Urls.API_URL.value, self.stored_user_id) + if PlayerServerInfo.is_whitelist: self.start_queue() self.ui.queue_lbl.show() self.ui.queue_position.show() - self.ui.stackedWidget.setCurrentIndex(0) + self.ui.stackedWidget.setCurrentIndex(0) # Retour page principale else: - self.ui.stackedWidget.setCurrentIndex(2) + self.ui.stackedWidget.setCurrentIndex(2) # Page non-whitelisté except ApiError as exc: show_qt_error(self, "La Tanière", f"Impossible de vérifier la whitelist.\n\n{exc}") + else: + show_qt_error(self, "Erreur Serveur", + f"L'authentification a réussi mais l'enregistrement a échoué.\n\n{error_message}") - except ApiError as exc: - show_qt_error(self, "Connexion Discord", f"Impossible de récupérer ton compte Discord.\n\n{exc}") # ------------------------------------------------------------------ # Mouse events → délégués au WindowDragger # ------------------------------------------------------------------ def mousePressEvent(self, event: QMouseEvent) -> None: + # On délègue au dragger self._dragger.mouse_press(event) - super().mousePressEvent(event) + # On ne remonte pas au parent si on a déjà "accepté" l'event + if not event.isAccepted(): + super().mousePressEvent(event) def mouseMoveEvent(self, event: QMouseEvent) -> None: self._dragger.mouse_move(event) - super().mouseMoveEvent(event) + if not event.isAccepted(): + super().mouseMoveEvent(event) def mouseReleaseEvent(self, event: QMouseEvent) -> None: self._dragger.mouse_release(event) - super().mouseReleaseEvent(event) + if not event.isAccepted(): + super().mouseReleaseEvent(event) # ------------------------------------------------------------------ # Close @@ -261,6 +289,12 @@ class MainWindow(QMainWindow): def cleanup(self): """À appeler lors de la fermeture de la fenêtre principale""" + if self.close_timer: + self.close_timer.stop() + + if self.countdown_timer: + self.countdown_timer.stop() + if hasattr(self, '_sound'): self._sound.stop() if hasattr(self, '_temp_mp3'): @@ -281,55 +315,46 @@ class MainWindow(QMainWindow): # ------------------------------------------------------------------ # Schedule de fermeture du launcher # ------------------------------------------------------------------ - def schedule_close(self, delay: int = 60000): - # Normalisation du délai - if not isinstance(delay, int) or delay <= 0: - delay = 60000 + def schedule_close(self, delay_ms: int = 60000): + """Lance ou redémarre le compte à rebours de fermeture.""" + # 1. Sécurité sur les entrées + if not isinstance(delay_ms, int) or delay_ms <= 0: + delay_ms = 60000 - # Conversion en secondes - self.remaining_time = delay // 1000 + # 2. Reset du temps restant (conversion ms -> s) + self.remaining_time = delay_ms // 1000 - # 🔁 Annule les timers existants - if self.close_timer: - self.close_timer.stop() - self.close_timer.deleteLater() + # 3. On stop les timers s'ils tournaient déjà (évite les doublons) + self.close_timer.stop() + self.countdown_timer.stop() - if self.countdown_timer: - self.countdown_timer.stop() - self.countdown_timer.deleteLater() - - # ⏱ Timer de fermeture - self.close_timer = QTimer(self) - self.close_timer.setSingleShot(True) - self.close_timer.timeout.connect(self.close) - self.close_timer.start(delay) - - # ⏳ Timer de countdown (1 seconde) - self.countdown_timer = QTimer(self) - self.countdown_timer.timeout.connect(self._update_countdown) + # 4. On lance + self.close_timer.start(delay_ms) self.countdown_timer.start(1000) - # Affichage initial - self._update_countdown() + # 5. Mise à jour immédiate de l'affichage (évite d'attendre 1s) + self._update_countdown_display() - # on affiche un compteur avant fermeture automatique def _update_countdown(self): + """Appelée toutes les secondes par le countdown_timer.""" + self.remaining_time -= 1 + if self.remaining_time <= 0: - if self.countdown_timer: - self.countdown_timer.stop() + self.countdown_timer.stop() return - minutes = self.remaining_time // 60 - seconds = self.remaining_time % 60 - self.ui.queue_lbl.setText(f"⏳ Fermeture dans {minutes:02d}:{seconds:02d}") + self._update_countdown_display() - self.remaining_time -= 1 + def _update_countdown_display(self): + """Met à jour le texte dans l'interface.""" + minutes = max(0, self.remaining_time // 60) + seconds = max(0, self.remaining_time % 60) + self.ui.queue_lbl.setText(f"⏳ Fermeture dans {minutes:02d}:{seconds:02d}") # ------------------------------------------------------------------ # Queue managment # ------------------------------------------------------------------ def start_queue(self): - self.queue_thread = QueueThread(self.stored_user_id) self.queue_thread = QueueThread(self.stored_user_id, parent=self) # ← parent=self self.queue_thread.update.connect(self.handle_update) self.queue_thread.start() @@ -360,22 +385,81 @@ class MainWindow(QMainWindow): def launch_fivem(self): pass - def _ensure_server_session(self) -> None: - """ - Garantit que le serveur connaît bien le session_id courant. - Le fait d'avoir une valeur locale ne suffit pas : il faut aussi - la resynchroniser côté serveur. - """ + def _ensure_server_session(self) -> bool: if not self.stored_user_id or self.stored_user_id.isspace(): - raise ApiError("Aucun identifiant Discord disponible.") + return False - if not PlayerServerInfo.session_id: - PlayerServerInfo.session_id = GetServerTokenForDiscord.authenticate(Urls.API_URL.value) + try: + # 1. Si on a déjà la session, on ne fait rien + if PlayerServerInfo.session_id: + return True + + # 2. Authentification (avec protection timeout) + # On suppose que authenticate renvoie le token + token = GetServerTokenForDiscord.authenticate(Urls.API_URL.value) + + if token: + PlayerServerInfo.session_id = token + # 3. L'enregistrement est souvent la partie qui bloque (SSL handshake) + # On l'entoure d'un try spécifique + try: + GetServerTokenForDiscord.register_discord_user( + self.stored_user_id, + PlayerServerInfo.session_id, + ) + return True + except Exception as reg_err: + print(f"Erreur lors du register: {reg_err}") + return False + + return False + except Exception as e: + print(f"Erreur globale session: {e}") + return False + + def _on_auth_finished(self, success: bool, session_id: str, error_message: str): + """Appelée quand le AuthWorker a terminé.""" + # 1. Restauration de l'UI + QGuiApplication.restoreOverrideCursor() + self.ui.connexion_btn.setEnabled(True) + self.ui.connexion_btn.setText("SE CONNECTER") # Remettre le texte par défaut + + # Nettoyage du worker + if self.auth_worker: + self.auth_worker.deleteLater() + self.auth_worker = None + + if success: + # 2. Mise à jour des infos globales + PlayerServerInfo.session_id = session_id + # 3. On continue la logique normale + self._proceed_to_queue_or_launch() + else: + # 4. Affichage de l'erreur propre (le handshake SSL a sûrement timeout) + show_qt_error(self, "Erreur d'Authentification", + f"Impossible de se connecter au serveur.\n\n{error_message}") + + def _proceed_to_queue_or_launch(self): + """Continuation de la logique après une session valide.""" + try: + # Si on n'a pas encore de position en file d'attente, on la lance + if self.queue_position_value is None: + self.start_queue() + return + + # Si on est en file d'attente mais pas au début + if self.queue_position_value != 0: + show_qt_error(self, "Attente", f"Position actuelle : {self.queue_position_value}") + return + + # Si tout est OK (Position 0) + FiveMLauncher.launch() + self.ui.connexion_btn.setEnabled(False) + self.schedule_close() + + except Exception as exc: + show_qt_error(self, "Erreur de Lancement", f"Détails : {exc}") - GetServerTokenForDiscord.register_discord_user( - self.stored_user_id, - PlayerServerInfo.session_id, - ) class QueueThread(QThread): update = Signal(str)