Refactor main_window into multiples files
This commit is contained in:
+103
-371
@@ -2,10 +2,10 @@ import webbrowser
|
|||||||
from os import environ, unlink
|
from os import environ, unlink
|
||||||
from sys import platform
|
from sys import platform
|
||||||
|
|
||||||
from PySide6.QtCore import QThread, Signal
|
from PySide6.QtCore import QTimer
|
||||||
from PySide6.QtCore import Qt, QTimer
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtGui import QIcon, QMouseEvent, QCursor, QGuiApplication
|
from PySide6.QtGui import QIcon, QMouseEvent, QGuiApplication
|
||||||
from PySide6.QtWidgets import QMainWindow, QSizePolicy
|
from PySide6.QtWidgets import QMainWindow
|
||||||
|
|
||||||
from config.config_manager import ConfigManager
|
from config.config_manager import ConfigManager
|
||||||
from config.constants import PlayerServerInfo, Urls
|
from config.constants import PlayerServerInfo, Urls
|
||||||
@@ -13,29 +13,33 @@ from controllers.audio_controller import AudioController
|
|||||||
from controllers.glow_animator import GlowAnimator
|
from controllers.glow_animator import GlowAnimator
|
||||||
from controllers.window_dragger import WindowDragger
|
from controllers.window_dragger import WindowDragger
|
||||||
from discord import discord_oauth
|
from discord import discord_oauth
|
||||||
from fake_patch_notes import patch_note
|
from fake_patch_notes import patch_note # <- TO REMOVE USE API
|
||||||
from fivemserver.auth_worker import AuthWorker
|
from fivemserver.auth_worker import AuthWorker
|
||||||
from fivemserver.fivemlauncher import FiveMLauncher
|
from fivemserver.fivemlauncher import FiveMLauncher
|
||||||
from fivemserver.get_server_token import GetServerTokenForDiscord
|
from fivemserver.get_server_token import GetServerTokenForDiscord
|
||||||
from fivemserver.queuemanager import QueueManager
|
|
||||||
from fivemserver.whitelistmanager import WhiteList
|
from fivemserver.whitelistmanager import WhiteList
|
||||||
from tools.http_client import ApiError
|
from tools.http_client import ApiError
|
||||||
from ui.error_dialog import show_qt_error
|
from ui.error_dialog import show_qt_error
|
||||||
from ui.hazard_stripes import HazardButton
|
|
||||||
from ui.ui_mainwindow_vertical_pager import Ui_MainWindow
|
from ui.ui_mainwindow_vertical_pager import Ui_MainWindow
|
||||||
|
from uitools.countdown_manager import CountdownManager
|
||||||
|
from uitools.queue_thread import QueueThread
|
||||||
|
from uitools.ui_builder import set_en_chantier, replace_with_hazard_button, hide_staff_btn_and_recenter
|
||||||
|
from uitools.window_utility import center_window
|
||||||
|
|
||||||
# For Linux Wayland to authorize moving window
|
# For Linux Wayland to authorize moving window
|
||||||
if platform.startswith('linux'):
|
if platform.startswith('linux'):
|
||||||
environ["QT_QPA_PLATFORM"] = "xcb"
|
environ["QT_QPA_PLATFORM"] = "xcb"
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
"""
|
"""
|
||||||
MainWindow class for managing the main application window and its functionalities.
|
MainWindow class for managing the main application window and its functionalities.
|
||||||
|
|
||||||
This class is responsible for managing the UI, user interactions, and application state.
|
This class is responsible for managing the UI, user interactions, and application state.
|
||||||
It provides mechanisms to handle user authentication, connection to external services, and queued tasks.
|
It provides mechanisms to handle user authentication, connection to external services,
|
||||||
The class is designed to be used with the PyQt framework and provides logic for dynamically updating
|
and queued tasks. The class is designed to be used with the PySide6 framework and
|
||||||
the UI elements and interacting with other components.
|
provides logic for dynamically updating the UI elements and interacting with other
|
||||||
|
components.
|
||||||
|
|
||||||
:ivar ui: The main UI object for managing visual components and layouts.
|
:ivar ui: The main UI object for managing visual components and layouts.
|
||||||
:type ui: Ui_MainWindow
|
:type ui: Ui_MainWindow
|
||||||
@@ -44,17 +48,10 @@ class MainWindow(QMainWindow):
|
|||||||
:ivar stored_user_id: The Discord user ID stored in the configuration.
|
:ivar stored_user_id: The Discord user ID stored in the configuration.
|
||||||
:type stored_user_id: str
|
:type stored_user_id: str
|
||||||
:ivar queue_thread: The thread handling queue operations.
|
:ivar queue_thread: The thread handling queue operations.
|
||||||
:type queue_thread: Optional[Thread]
|
:type queue_thread: Optional[QueueThread]
|
||||||
:ivar queue_position_value: The current queue position.
|
:ivar queue_position_value: The current queue position.
|
||||||
:type queue_position_value: Optional[int]
|
:type queue_position_value: Optional[int]
|
||||||
:ivar close_timer: Timer object for handling the final closure of the application.
|
|
||||||
:type close_timer: QTimer
|
|
||||||
:ivar countdown_timer: Timer object for updating visual countdown timers.
|
|
||||||
:type countdown_timer: QTimer
|
|
||||||
:ivar remaining_time: The remaining time for queue or countdown operations, in seconds.
|
|
||||||
:type remaining_time: int
|
|
||||||
"""
|
"""
|
||||||
#update = Signal(str) # Reçoit les callbacks de QueueManager
|
|
||||||
|
|
||||||
def __init__(self, bundle_dir: str, config_manager: ConfigManager):
|
def __init__(self, bundle_dir: str, config_manager: ConfigManager):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -62,53 +59,51 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui = Ui_MainWindow()
|
self.ui = Ui_MainWindow()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
self.auth_worker = None # <--- Pour stocker l'instance du thread
|
self.auth_worker = None
|
||||||
|
|
||||||
self.config = config_manager
|
self.config = config_manager
|
||||||
self.stored_user_id = self.config.get_discord_user()
|
self.stored_user_id = self.config.get_discord_user()
|
||||||
self.queue_thread = None
|
self.queue_thread = None
|
||||||
self.queue_position_value = None
|
self.queue_position_value = None
|
||||||
self.close_timer = None
|
|
||||||
self.countdown_timer = None
|
|
||||||
self.remaining_time = 0 # en secondes
|
|
||||||
|
|
||||||
# Préparation du timer de fermeture finale
|
# ------------------------------------------------------------------
|
||||||
self.close_timer = QTimer(self)
|
# Timers (owned here so they share the QObject tree of MainWindow)
|
||||||
self.close_timer.setSingleShot(True)
|
# ------------------------------------------------------------------
|
||||||
self.close_timer.timeout.connect(self.close)
|
close_timer = QTimer(self)
|
||||||
|
close_timer.setSingleShot(True)
|
||||||
|
close_timer.timeout.connect(self.close)
|
||||||
|
|
||||||
# Préparation du timer de mise à jour visuelle (1s)
|
countdown_timer = QTimer(self)
|
||||||
self.countdown_timer = QTimer(self)
|
|
||||||
self.countdown_timer.timeout.connect(self._update_countdown)
|
|
||||||
|
|
||||||
# UI
|
self._countdown = CountdownManager(
|
||||||
# self.ui = QUiLoader().load(f"{bundle_dir}/ui/mainwindow_vertical_pager.ui", self)
|
close_timer=close_timer,
|
||||||
#self.setCentralWidget(self.ui.centralWidget())
|
countdown_timer=countdown_timer,
|
||||||
|
label=self.ui.queue_lbl,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Window flags
|
||||||
|
# ------------------------------------------------------------------
|
||||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
self.setWindowIcon(QIcon(str(bundle_dir / "assets" / "Icon.ico")))
|
self.setWindowIcon(QIcon(str(bundle_dir / "assets" / "Icon.ico")))
|
||||||
|
|
||||||
# Par défaut on affiche la page normal pour la connexion au serveur
|
# ------------------------------------------------------------------
|
||||||
|
# Initial page / whitelist check
|
||||||
|
# ------------------------------------------------------------------
|
||||||
self.ui.stackedWidget.setCurrentIndex(0)
|
self.ui.stackedWidget.setCurrentIndex(0)
|
||||||
# On cache par défaut les infos liste d'attente
|
|
||||||
self.ui.queue_lbl.hide()
|
self.ui.queue_lbl.hide()
|
||||||
self.ui.queue_position.hide()
|
self.ui.queue_position.hide()
|
||||||
|
|
||||||
|
|
||||||
# Si l'id discord = "" ou des espace, alors on affiche la page comme quoi faut être connecté à discord.
|
|
||||||
if self.stored_user_id == "" or self.stored_user_id.isspace():
|
if self.stored_user_id == "" or self.stored_user_id.isspace():
|
||||||
self.ui.stackedWidget.setCurrentIndex(1)
|
self.ui.stackedWidget.setCurrentIndex(1)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# on vérifie si le joueur est whitelisté
|
|
||||||
WhiteList.check_whitelist(Urls.API_URL.value, self.stored_user_id)
|
WhiteList.check_whitelist(Urls.API_URL.value, self.stored_user_id)
|
||||||
except ApiError as exc:
|
except ApiError as exc:
|
||||||
show_qt_error(self, "La Tanière", f"Impossible de vérifier la whitelist.\n\n{exc}")
|
show_qt_error(self, "La Tanière", f"Impossible de vérifier la whitelist.\n\n{exc}")
|
||||||
PlayerServerInfo.is_whitelist = False
|
PlayerServerInfo.is_whitelist = False
|
||||||
PlayerServerInfo.is_staff = False
|
PlayerServerInfo.is_staff = False
|
||||||
|
|
||||||
# si on est whitelisté, on démarre la file d'attente
|
|
||||||
if PlayerServerInfo.is_whitelist:
|
if PlayerServerInfo.is_whitelist:
|
||||||
self.start_queue()
|
self.start_queue()
|
||||||
self.ui.queue_lbl.show()
|
self.ui.queue_lbl.show()
|
||||||
@@ -116,65 +111,50 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
self.ui.stackedWidget.setCurrentIndex(2)
|
self.ui.stackedWidget.setCurrentIndex(2)
|
||||||
|
|
||||||
# Test bouton en contruction
|
# ------------------------------------------------------------------
|
||||||
|
# Under-construction button (delegated to ui_builder)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
en_chantier = False
|
en_chantier = False
|
||||||
# on set la css du bouton en fonction de la valeur de la variable en_chantier
|
set_en_chantier(self.ui.connexion_btn, en_chantier)
|
||||||
self.set_en_chantier(en_chantier)
|
|
||||||
if en_chantier:
|
if en_chantier:
|
||||||
old_btn = self.ui.connexion_btn
|
new_btn = replace_with_hazard_button(
|
||||||
parent_layout = self.ui.verticalLayout_6 # layout direct du bouton dans le .ui
|
self.ui.connexion_btn,
|
||||||
|
self.ui.verticalLayout_6,
|
||||||
index = parent_layout.indexOf(old_btn)
|
)
|
||||||
|
|
||||||
new_btn = HazardButton(old_btn.parentWidget())
|
|
||||||
new_btn.setObjectName("connexion_btn")
|
|
||||||
new_btn.setText("EN MAINTENANCE")
|
|
||||||
new_btn.setIcon(old_btn.icon())
|
|
||||||
new_btn.setIconSize(old_btn.iconSize())
|
|
||||||
new_btn.setMinimumSize(old_btn.minimumSize())
|
|
||||||
new_btn.set_hazard(True)
|
|
||||||
|
|
||||||
parent_layout.takeAt(index)
|
|
||||||
old_btn.deleteLater()
|
|
||||||
parent_layout.insertWidget(index, new_btn)
|
|
||||||
|
|
||||||
self.ui.connexion_btn = new_btn
|
self.ui.connexion_btn = new_btn
|
||||||
self.ui.connexion_btn.clicked.connect(self._on_connexion)
|
self.ui.connexion_btn.clicked.connect(self._on_connexion)
|
||||||
|
|
||||||
# centrage vertical du bouton connexion
|
# ------------------------------------------------------------------
|
||||||
|
# Staff button / spacer adjustment (delegated to ui_builder)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
if not PlayerServerInfo.is_staff:
|
if not PlayerServerInfo.is_staff:
|
||||||
self.ui.staff_btn.hide()
|
hide_staff_btn_and_recenter(self.ui.staff_btn, self.ui.verticalLayout_6)
|
||||||
layout = self.ui.verticalLayout_6
|
|
||||||
# Trouver et modifier le spacer item
|
|
||||||
for i in range(layout.count()):
|
|
||||||
item = layout.itemAt(i)
|
|
||||||
if item.spacerItem(): # C'est un spacer
|
|
||||||
item.spacerItem().changeSize(20, 15, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
|
||||||
layout.invalidate() # Forcer le recalcul du layout
|
|
||||||
break
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Misc UI
|
||||||
|
# ------------------------------------------------------------------
|
||||||
self.ui.info_text.setMarkdown(patch_note)
|
self.ui.info_text.setMarkdown(patch_note)
|
||||||
|
|
||||||
# Sous-systèmes
|
# ------------------------------------------------------------------
|
||||||
|
# Sub-systems
|
||||||
|
# ------------------------------------------------------------------
|
||||||
self._audio = AudioController(self.config, self.ui.audio_volume_adjust, self.ui.mute_btn)
|
self._audio = AudioController(self.config, self.ui.audio_volume_adjust, self.ui.mute_btn)
|
||||||
self._glow = GlowAnimator(self.ui.connexion_btn)
|
self._glow = GlowAnimator(self.ui.connexion_btn)
|
||||||
self._dragger = WindowDragger(self)
|
self._dragger = WindowDragger(self)
|
||||||
|
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
self._center_window()
|
center_window(self) # delegated to window_utils
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Setup
|
# Setup
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _connect_signals(self) -> None:
|
def _connect_signals(self) -> None:
|
||||||
"""
|
"""
|
||||||
Establishes connections between UI elements and their corresponding
|
Establishes connections between UI elements and their corresponding handler methods.
|
||||||
functionality. Each signal emitted by a user interaction with a specific
|
|
||||||
UI button is linked to its respective handler method.
|
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
@@ -186,23 +166,6 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.discord_auth_btn.clicked.connect(self._on_discord_auth_btn)
|
self.ui.discord_auth_btn.clicked.connect(self._on_discord_auth_btn)
|
||||||
self.ui.no_whitelist_btn.clicked.connect(self.close)
|
self.ui.no_whitelist_btn.clicked.connect(self.close)
|
||||||
|
|
||||||
def _center_window(self) -> None:
|
|
||||||
"""
|
|
||||||
Centers the window on the screen that the cursor is currently located on.
|
|
||||||
If the cursor is not on any screen, it uses the primary screen for centering.
|
|
||||||
The window's geometry is adjusted and moved accordingly.
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.adjustSize()
|
|
||||||
screen = (
|
|
||||||
QGuiApplication.screenAt(QCursor.pos())
|
|
||||||
or QGuiApplication.primaryScreen()
|
|
||||||
)
|
|
||||||
rect = self.frameGeometry()
|
|
||||||
rect.moveCenter(screen.availableGeometry().center())
|
|
||||||
self.move(rect.topLeft())
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Button handlers
|
# Button handlers
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -210,33 +173,25 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_connexion(self) -> None:
|
def _on_connexion(self) -> None:
|
||||||
"""
|
"""
|
||||||
Handles the user connection process, including verification of prerequisites,
|
Handles the user connection process, including verification of prerequisites,
|
||||||
session validation, and background authentication for the current user. This
|
session validation, and background authentication for the current user.
|
||||||
method ensures a smooth user experience by implementing UI locking during
|
|
||||||
authentication and proceeding with appropriate actions if a valid session
|
|
||||||
already exists.
|
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# 1. Sécurités de base
|
|
||||||
if not self.stored_user_id or self.stored_user_id.isspace():
|
if not self.stored_user_id or self.stored_user_id.isspace():
|
||||||
show_qt_error(self, "Connexion impossible", "Identifiant Discord absent.")
|
show_qt_error(self, "Connexion impossible", "Identifiant Discord absent.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. Si on a déjà une session valide, on passe directement à la suite
|
|
||||||
if PlayerServerInfo.session_id:
|
if PlayerServerInfo.session_id:
|
||||||
self._proceed_to_queue_or_launch()
|
self._proceed_to_queue_or_launch()
|
||||||
return
|
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.setEnabled(False)
|
||||||
self.ui.connexion_btn.setText("Authentification...")
|
self.ui.connexion_btn.setText("Authentification...")
|
||||||
QGuiApplication.setOverrideCursor(Qt.WaitCursor) # Curseur de chargement
|
QGuiApplication.setOverrideCursor(Qt.WaitCursor)
|
||||||
|
|
||||||
# Création et lancement du worker
|
|
||||||
self.auth_worker = AuthWorker(self.stored_user_id)
|
self.auth_worker = AuthWorker(self.stored_user_id)
|
||||||
self.auth_worker.finished.connect(self._on_auth_finished)
|
self.auth_worker.finished.connect(self._on_auth_finished)
|
||||||
self.auth_worker.finished.connect(self.auth_worker.deleteLater) # Nettoyage automatique
|
self.auth_worker.finished.connect(self.auth_worker.deleteLater)
|
||||||
self.auth_worker.start()
|
self.auth_worker.start()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -244,18 +199,13 @@ class MainWindow(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
Opens the Discord URL in the default web browser.
|
Opens the Discord URL in the default web browser.
|
||||||
|
|
||||||
This method is a static utility function that launches the Discord URL defined
|
:return: None
|
||||||
in the Urls enumeration. It uses the `webbrowser.open` method to handle the
|
|
||||||
opening process.
|
|
||||||
|
|
||||||
:return: Nothing is returned
|
|
||||||
:rtype: None
|
|
||||||
"""
|
"""
|
||||||
webbrowser.open(Urls.DISCORD.value)
|
webbrowser.open(Urls.DISCORD.value)
|
||||||
|
|
||||||
def _on_intranet(self) -> None:
|
def _on_intranet(self) -> None:
|
||||||
"""
|
"""
|
||||||
Opens the intranet URL using the default web browser and starts the glow process.
|
Opens the intranet URL and starts the glow animation.
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
@@ -266,32 +216,22 @@ class MainWindow(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
Handles the authentication process for Discord when the auth button is clicked.
|
Handles the authentication process for Discord when the auth button is clicked.
|
||||||
|
|
||||||
This method retrieves the Discord user ID and token via OAuth, updates the
|
|
||||||
local configuration, and initiates a background worker to handle server
|
|
||||||
registration. The UI button is disabled and the cursor is set to a waiting
|
|
||||||
state during the network operations to prevent multiple attempts.
|
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. Récupération OAuth (souvent via navigateur, assez rapide)
|
|
||||||
test = discord_oauth.get_discord_user_id()
|
test = discord_oauth.get_discord_user_id()
|
||||||
user_id = test[0]
|
user_id = test[0]
|
||||||
token = test[1]
|
token = test[1]
|
||||||
|
|
||||||
# 2. Mise à jour locale immédiate
|
|
||||||
self.config.set_discord_user(user_id)
|
self.config.set_discord_user(user_id)
|
||||||
self.stored_user_id = user_id
|
self.stored_user_id = user_id
|
||||||
PlayerServerInfo.session_id = token
|
PlayerServerInfo.session_id = token
|
||||||
self.config.save()
|
self.config.save()
|
||||||
|
|
||||||
# 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)
|
self.ui.discord_auth_btn.setEnabled(False)
|
||||||
QGuiApplication.setOverrideCursor(Qt.WaitCursor)
|
QGuiApplication.setOverrideCursor(Qt.WaitCursor)
|
||||||
|
|
||||||
self.auth_worker = AuthWorker(self.stored_user_id)
|
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._on_discord_auth_finished)
|
||||||
self.auth_worker.finished.connect(self.auth_worker.deleteLater)
|
self.auth_worker.finished.connect(self.auth_worker.deleteLater)
|
||||||
self.auth_worker.start()
|
self.auth_worker.start()
|
||||||
@@ -305,17 +245,9 @@ class MainWindow(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
Handles the completion of the Discord authentication process.
|
Handles the completion of the Discord authentication process.
|
||||||
|
|
||||||
This method is triggered once the Discord authentication flow is completed.
|
|
||||||
It determines the success or failure state of the process and accordingly
|
|
||||||
triggers user interface updates and calls associated actions, such as verifying
|
|
||||||
the whitelist status.
|
|
||||||
|
|
||||||
:param success: Indicates whether the authentication was successful.
|
:param success: Indicates whether the authentication was successful.
|
||||||
:type success: bool
|
|
||||||
:param session_id: The session ID obtained after the authentication process.
|
:param session_id: The session ID obtained after the authentication process.
|
||||||
:type session_id: str
|
|
||||||
:param error_message: The error message returned if the process failed.
|
:param error_message: The error message returned if the process failed.
|
||||||
:type error_message: str
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
QGuiApplication.restoreOverrideCursor()
|
QGuiApplication.restoreOverrideCursor()
|
||||||
@@ -323,16 +255,15 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
try:
|
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)
|
WhiteList.check_whitelist(Urls.API_URL.value, self.stored_user_id)
|
||||||
|
|
||||||
if PlayerServerInfo.is_whitelist:
|
if PlayerServerInfo.is_whitelist:
|
||||||
self.start_queue()
|
self.start_queue()
|
||||||
self.ui.queue_lbl.show()
|
self.ui.queue_lbl.show()
|
||||||
self.ui.queue_position.show()
|
self.ui.queue_position.show()
|
||||||
self.ui.stackedWidget.setCurrentIndex(0) # Retour page principale
|
self.ui.stackedWidget.setCurrentIndex(0)
|
||||||
else:
|
else:
|
||||||
self.ui.stackedWidget.setCurrentIndex(2) # Page non-whitelisté
|
self.ui.stackedWidget.setCurrentIndex(2)
|
||||||
except ApiError as exc:
|
except ApiError as exc:
|
||||||
show_qt_error(self, "La Tanière", f"Impossible de vérifier la whitelist.\n\n{exc}")
|
show_qt_error(self, "La Tanière", f"Impossible de vérifier la whitelist.\n\n{exc}")
|
||||||
else:
|
else:
|
||||||
@@ -340,53 +271,20 @@ class MainWindow(QMainWindow):
|
|||||||
f"L'authentification a réussi mais l'enregistrement a échoué.\n\n{error_message}")
|
f"L'authentification a réussi mais l'enregistrement a échoué.\n\n{error_message}")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Mouse events → délégués au WindowDragger
|
# Mouse events → delegated to WindowDragger
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def mousePressEvent(self, event: QMouseEvent) -> None:
|
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||||
"""
|
|
||||||
Handles mouse press event for the widget.
|
|
||||||
|
|
||||||
This method is invoked when a mouse button is pressed over the widget. It
|
|
||||||
delegates the event to an internal dragger object for handling. If the event
|
|
||||||
is not marked as accepted by the dragger, the parent class implementation
|
|
||||||
of the mouse press event is called.
|
|
||||||
|
|
||||||
:param event: The QMouseEvent instance representing the mouse press event.
|
|
||||||
:type event: QMouseEvent
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self._dragger.mouse_press(event)
|
self._dragger.mouse_press(event)
|
||||||
# On ne remonte pas au parent si on a déjà "accepté" l'event
|
|
||||||
if not event.isAccepted():
|
if not event.isAccepted():
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
def mouseMoveEvent(self, event: QMouseEvent) -> None:
|
def mouseMoveEvent(self, event: QMouseEvent) -> None:
|
||||||
"""
|
|
||||||
Handle the mouse move event.
|
|
||||||
|
|
||||||
This method processes the mouse movement event and delegates the functionality
|
|
||||||
to the associated dragger. If the event is not accepted after processing, it
|
|
||||||
passes the event up to the superclass implementation.
|
|
||||||
|
|
||||||
:param event: The mouse event being handled.
|
|
||||||
:type event: QMouseEvent
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self._dragger.mouse_move(event)
|
self._dragger.mouse_move(event)
|
||||||
if not event.isAccepted():
|
if not event.isAccepted():
|
||||||
super().mouseMoveEvent(event)
|
super().mouseMoveEvent(event)
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
||||||
"""
|
|
||||||
Handles the mouse release event. This function processes the event, allowing
|
|
||||||
custom handling logic through a dragger object, and then delegates to the parent
|
|
||||||
class's handler if the event is not already accepted.
|
|
||||||
|
|
||||||
:param event: The mouse release event to be processed.
|
|
||||||
:type event: QMouseEvent
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self._dragger.mouse_release(event)
|
self._dragger.mouse_release(event)
|
||||||
if not event.isAccepted():
|
if not event.isAccepted():
|
||||||
super().mouseReleaseEvent(event)
|
super().mouseReleaseEvent(event)
|
||||||
@@ -397,39 +295,24 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def closeEvent(self, event) -> None:
|
def closeEvent(self, event) -> None:
|
||||||
"""
|
"""
|
||||||
Handles the close event of the application.
|
Ensures the background thread is stopped and configuration is saved before closing.
|
||||||
|
|
||||||
This method is executed when the application window is being closed. It ensures
|
|
||||||
the termination of any running threads, saves the application configuration, and
|
|
||||||
performs necessary cleanup routines before invoking the superclass's close event
|
|
||||||
handler.
|
|
||||||
|
|
||||||
:param event: The close event instance that provides event-related data and allows
|
|
||||||
control over the closing process.
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if self.queue_thread and self.queue_thread.isRunning():
|
if self.queue_thread and self.queue_thread.isRunning():
|
||||||
self.queue_thread.stop()
|
self.queue_thread.stop()
|
||||||
self.queue_thread.wait() # Attend que le thread se termine proprement
|
self.queue_thread.wait()
|
||||||
self.config.save()
|
self.config.save()
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self) -> None:
|
||||||
"""
|
"""
|
||||||
Stops all active timers and performs cleanup operations.
|
Stops all active timers and removes temporary files.
|
||||||
|
|
||||||
This method ensures that any active resources, such as timers or temporary
|
:return: None
|
||||||
files, are properly stopped or removed to avoid resource leaks. It is a
|
|
||||||
utility to clean up and reset components associated with the instance.
|
|
||||||
|
|
||||||
:raises OSError: If an error occurs during the removal of the temporary file.
|
|
||||||
"""
|
"""
|
||||||
if self.close_timer:
|
self._countdown.stop()
|
||||||
self.close_timer.stop()
|
|
||||||
|
|
||||||
if self.countdown_timer:
|
|
||||||
self.countdown_timer.stop()
|
|
||||||
|
|
||||||
if hasattr(self, '_sound'):
|
if hasattr(self, '_sound'):
|
||||||
self._sound.stop()
|
self._sound.stop()
|
||||||
@@ -440,120 +323,44 @@ class MainWindow(QMainWindow):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Change ui on runtime
|
# Scheduled close (delegated to CountdownManager)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def set_en_chantier(self, valeur: bool):
|
|
||||||
self.en_chantier = valeur # ta variable Python
|
|
||||||
self.ui.connexion_btn.setProperty("en_chantier", valeur) # propriété Qt
|
|
||||||
self.ui.connexion_btn.style().unpolish(self.ui.connexion_btn)
|
|
||||||
self.ui.connexion_btn.style().polish(self.ui.connexion_btn)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
def schedule_close(self, delay_ms: int = 60_000) -> None:
|
||||||
# Schedule de fermeture du launcher
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
def schedule_close(self, delay_ms: int = 60000):
|
|
||||||
"""
|
"""
|
||||||
Schedules a process to close after a specified delay in milliseconds. The countdown
|
Schedules the launcher to close after *delay_ms* milliseconds and starts
|
||||||
timer also starts to reflect the remaining time in seconds. Timers already running
|
the visual countdown.
|
||||||
are reset to prevent duplicate behavior, and the display is updated immediately.
|
|
||||||
|
|
||||||
:param delay_ms: The delay in milliseconds after which the process should
|
:param delay_ms: Delay in milliseconds (default: 60 000).
|
||||||
be closed. Defaults to 60000 milliseconds (1 minute) if not specified
|
|
||||||
or an invalid value is provided.
|
|
||||||
:type delay_ms: int
|
:type delay_ms: int
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# 1. Sécurité sur les entrées
|
self._countdown.schedule(delay_ms)
|
||||||
if not isinstance(delay_ms, int) or delay_ms <= 0:
|
|
||||||
delay_ms = 60000
|
|
||||||
|
|
||||||
# 2. Reset du temps restant (conversion ms -> s)
|
|
||||||
self.remaining_time = delay_ms // 1000
|
|
||||||
|
|
||||||
# 3. On stop les timers s'ils tournaient déjà (évite les doublons)
|
|
||||||
self.close_timer.stop()
|
|
||||||
self.countdown_timer.stop()
|
|
||||||
|
|
||||||
# 4. On lance
|
|
||||||
self.close_timer.start(delay_ms)
|
|
||||||
self.countdown_timer.start(1000)
|
|
||||||
|
|
||||||
# 5. Mise à jour immédiate de l'affichage (évite d'attendre 1s)
|
|
||||||
self._update_countdown_display()
|
|
||||||
|
|
||||||
def _update_countdown(self):
|
|
||||||
"""
|
|
||||||
Decreases the remaining time by one and updates the countdown display.
|
|
||||||
|
|
||||||
This method is responsible for decrementing the `remaining_time` attribute
|
|
||||||
on each invocation. If the remaining time reaches zero or below, the
|
|
||||||
`countdown_timer` is stopped immediately and no further action is
|
|
||||||
performed in this method. Otherwise, it proceeds to update the countdown
|
|
||||||
display to reflect the current remaining time.
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.remaining_time -= 1
|
|
||||||
|
|
||||||
if self.remaining_time <= 0:
|
|
||||||
self.countdown_timer.stop()
|
|
||||||
return
|
|
||||||
|
|
||||||
self._update_countdown_display()
|
|
||||||
|
|
||||||
def _update_countdown_display(self):
|
|
||||||
"""
|
|
||||||
Updates the countdown display on the user interface with the remaining time
|
|
||||||
in minutes and seconds format. The remaining time will always be formatted
|
|
||||||
as a positive value, even if it is less than zero.
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
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
|
# Queue management
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def start_queue(self):
|
|
||||||
|
def start_queue(self) -> None:
|
||||||
"""
|
"""
|
||||||
Starts a separate thread for managing a queue and connects it to the relevant
|
Initialises and starts the :class:`~tools.queue_thread.QueueThread`.
|
||||||
update handler. This method initializes and starts a `QueueThread` instance
|
|
||||||
using the stored user ID and links it with the update handling functionality
|
|
||||||
to ensure real-time queue updates are properly handled.
|
|
||||||
|
|
||||||
All threading and queue update mechanisms are encapsulated within this method.
|
|
||||||
|
|
||||||
:param self: Reference to the calling object.
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.queue_thread = QueueThread(self.stored_user_id, parent=self) # ← parent=self
|
self.queue_thread = QueueThread(self.stored_user_id, parent=self)
|
||||||
self.queue_thread.update.connect(self.handle_update)
|
self.queue_thread.update.connect(self.handle_update)
|
||||||
self.queue_thread.start()
|
self.queue_thread.start()
|
||||||
|
|
||||||
# 🧪 TEMP - Simule une position en queue pour tester l'UI
|
def handle_update(self, message: str) -> None:
|
||||||
# self.handle_update("position:3:10")
|
|
||||||
|
|
||||||
def handle_update(self, message: str):
|
|
||||||
"""
|
"""
|
||||||
Handles updates based on a given message and performs relevant UI actions.
|
Reacts to queue status messages and updates the relevant UI labels.
|
||||||
|
|
||||||
:param message: The message received, which determines the update logic. Accepts:
|
:param message: Status string – ``"ok"``, ``"ready"``, or
|
||||||
- "ok": Indicates that it's the user's turn, updating the UI accordingly.
|
``"position:<current>:<total>"``.
|
||||||
- "ready": Equivalent to "ok", indicating the user's turn.
|
:type message: str
|
||||||
- Messages starting with "position:": Provide the current position and total
|
|
||||||
in the queue in the format "position:<current>:<total>", updating the queue
|
|
||||||
information in the UI.
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if message == "ok":
|
if message in ("ok", "ready"):
|
||||||
self.ui.queue_lbl.setVisible(True)
|
|
||||||
self.ui.queue_position.setVisible(False)
|
|
||||||
self.ui.queue_lbl.setText("🚀 C'est votre tour !")
|
|
||||||
self.queue_position_value = 0
|
|
||||||
|
|
||||||
elif message == "ready":
|
|
||||||
self.ui.queue_lbl.setVisible(True)
|
self.ui.queue_lbl.setVisible(True)
|
||||||
self.ui.queue_position.setVisible(False)
|
self.ui.queue_position.setVisible(False)
|
||||||
self.ui.queue_lbl.setText("🚀 C'est votre tour !")
|
self.ui.queue_lbl.setText("🚀 C'est votre tour !")
|
||||||
@@ -569,43 +376,28 @@ class MainWindow(QMainWindow):
|
|||||||
def launch_fivem(self):
|
def launch_fivem(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Session helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _ensure_server_session(self) -> bool:
|
def _ensure_server_session(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Ensures the establishment of a server session, handling authentication
|
Ensures a valid server session exists, authenticating if necessary.
|
||||||
and session registration for a Discord user.
|
|
||||||
|
|
||||||
The method first checks if a valid stored user ID exists. If there is
|
:return: ``True`` if the session was established successfully.
|
||||||
no stored user ID or it is blank, the session creation will fail. If a
|
|
||||||
session token (`session_id`) is already present, it will return
|
|
||||||
immediately with success. Otherwise, it proceeds to authenticate with
|
|
||||||
the server to obtain a session token and registers the Discord user
|
|
||||||
against this session.
|
|
||||||
|
|
||||||
Errors during registration are caught and handled separately, and the
|
|
||||||
method logs any issues encountered during the process. If the
|
|
||||||
authentication and registration are successful, the session is
|
|
||||||
considered established.
|
|
||||||
|
|
||||||
:return: A boolean indicating whether the server session was successfully
|
|
||||||
established.
|
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
if not self.stored_user_id or self.stored_user_id.isspace():
|
if not self.stored_user_id or self.stored_user_id.isspace():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Si on a déjà la session, on ne fait rien
|
|
||||||
if PlayerServerInfo.session_id:
|
if PlayerServerInfo.session_id:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 2. Authentification (avec protection timeout)
|
|
||||||
# On suppose que authenticate renvoie le token
|
|
||||||
token = GetServerTokenForDiscord.authenticate(Urls.API_URL.value)
|
token = GetServerTokenForDiscord.authenticate(Urls.API_URL.value)
|
||||||
|
|
||||||
if token:
|
if token:
|
||||||
PlayerServerInfo.session_id = 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:
|
try:
|
||||||
GetServerTokenForDiscord.register_discord_user(
|
GetServerTokenForDiscord.register_discord_user(
|
||||||
self.stored_user_id,
|
self.stored_user_id,
|
||||||
@@ -623,107 +415,47 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _on_auth_finished(self, success: bool, session_id: str, error_message: str):
|
def _on_auth_finished(self, success: bool, session_id: str, error_message: str):
|
||||||
"""
|
"""
|
||||||
Handles the completion of the authentication process by updating the UI,
|
Restores the UI and proceeds after the background authentication completes.
|
||||||
cleaning up the worker, and proceeding with subsequent logic based on the
|
|
||||||
authentication result.
|
|
||||||
|
|
||||||
:param success: A boolean indicating whether the authentication was successful.
|
:param success: Whether authentication succeeded.
|
||||||
:param session_id: The session identifier returned upon successful authentication.
|
:param session_id: Session token returned on success.
|
||||||
:param error_message: A descriptive error message in case authentication failed.
|
:param error_message: Human-readable error on failure.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# 1. Restauration de l'UI
|
|
||||||
QGuiApplication.restoreOverrideCursor()
|
QGuiApplication.restoreOverrideCursor()
|
||||||
self.ui.connexion_btn.setEnabled(True)
|
self.ui.connexion_btn.setEnabled(True)
|
||||||
self.ui.connexion_btn.setText("SE CONNECTER") # Remettre le texte par défaut
|
self.ui.connexion_btn.setText("SE CONNECTER")
|
||||||
|
|
||||||
# Nettoyage du worker
|
|
||||||
if self.auth_worker:
|
if self.auth_worker:
|
||||||
self.auth_worker.deleteLater()
|
self.auth_worker.deleteLater()
|
||||||
self.auth_worker = None
|
self.auth_worker = None
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
# 2. Mise à jour des infos globales
|
|
||||||
PlayerServerInfo.session_id = session_id
|
PlayerServerInfo.session_id = session_id
|
||||||
# 3. On continue la logique normale
|
|
||||||
self._proceed_to_queue_or_launch()
|
self._proceed_to_queue_or_launch()
|
||||||
else:
|
else:
|
||||||
# 4. Affichage de l'erreur propre (le handshake SSL a sûrement timeout)
|
|
||||||
show_qt_error(self, "Erreur d'Authentification",
|
show_qt_error(self, "Erreur d'Authentification",
|
||||||
f"Impossible de se connecter au serveur.\n\n{error_message}")
|
f"Impossible de se connecter au serveur.\n\n{error_message}")
|
||||||
|
|
||||||
def _proceed_to_queue_or_launch(self):
|
def _proceed_to_queue_or_launch(self) -> None:
|
||||||
"""
|
"""
|
||||||
Handles the process of either starting the queue, updating the user's
|
Starts the queue if not yet entered, shows the position, or launches FiveM
|
||||||
position in the queue, or launching the FiveM application if all conditions
|
when the user reaches position 0.
|
||||||
are met. This function first checks the queue position. If there is none,
|
|
||||||
the queue is started. If the current position in the queue is not at the
|
|
||||||
front (position 0), an error message is displayed. If the user is at the
|
|
||||||
front of the queue, the application launches, and further connections are
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
:raises Exception: If an error occurs during the launching process.
|
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Si on n'a pas encore de position en file d'attente, on la lance
|
|
||||||
if self.queue_position_value is None:
|
if self.queue_position_value is None:
|
||||||
self.start_queue()
|
self.start_queue()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Si on est en file d'attente mais pas au début
|
|
||||||
if self.queue_position_value != 0:
|
if self.queue_position_value != 0:
|
||||||
show_qt_error(self, "Attente", f"Position actuelle : {self.queue_position_value}")
|
show_qt_error(self, "Attente", f"Position actuelle : {self.queue_position_value}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Si tout est OK (Position 0)
|
|
||||||
FiveMLauncher.launch()
|
FiveMLauncher.launch()
|
||||||
self.ui.connexion_btn.setEnabled(False)
|
self.ui.connexion_btn.setEnabled(False)
|
||||||
self.schedule_close()
|
self.schedule_close()
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
show_qt_error(self, "Erreur de Lancement", f"Détails : {exc}")
|
show_qt_error(self, "Erreur de Lancement", f"Détails : {exc}")
|
||||||
|
|
||||||
class QueueThread(QThread):
|
|
||||||
"""
|
|
||||||
QueueThread class.
|
|
||||||
|
|
||||||
Implements a thread that manages a queue operation using a `QueueManager`. The
|
|
||||||
class emits a signal to update external components whenever a relevant change
|
|
||||||
occurs in the queue. Designed to run processing tasks in a separate thread.
|
|
||||||
|
|
||||||
:ivar update: Signal emitted with a string message to notify about updates.
|
|
||||||
:type update: Signal
|
|
||||||
"""
|
|
||||||
update = Signal(str)
|
|
||||||
|
|
||||||
def __init__(self, user_id: str, parent=None): # ← parent=None
|
|
||||||
super().__init__(parent) # ← passé à QThread
|
|
||||||
self.manager = QueueManager(
|
|
||||||
user_id=user_id,
|
|
||||||
on_update=self.update.emit
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
Initiates the start operation for the associated manager. This method
|
|
||||||
is responsible for invoking the `start` method of the `manager` object,
|
|
||||||
ensuring that the necessary operations managed by the `manager` commence
|
|
||||||
execution.
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
:rtype: None
|
|
||||||
"""
|
|
||||||
self.manager.start()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
Stops the operation managed by the associated manager instance.
|
|
||||||
|
|
||||||
This method delegates the stop operation to the `manager` object, ensuring
|
|
||||||
the termination of any ongoing processes or operations it controls.
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.manager.stop()
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
from PySide6.QtWidgets import QLabel
|
||||||
|
|
||||||
|
|
||||||
|
class CountdownManager:
|
||||||
|
"""
|
||||||
|
Manages a visual countdown that triggers a closing callback when it expires.
|
||||||
|
|
||||||
|
Designed to be composed into a QMainWindow (or any QObject owner) rather than
|
||||||
|
subclassed. The owner must supply:
|
||||||
|
- a ``close_timer`` (QTimer, singleShot)
|
||||||
|
- a ``countdown_timer`` (QTimer, repeating 1 s)
|
||||||
|
- a ``queue_lbl`` (QLabel) on which the remaining time is displayed
|
||||||
|
- a ``on_timeout`` callable invoked when the close timer fires
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
self._countdown = CountdownManager(
|
||||||
|
close_timer=self.close_timer,
|
||||||
|
countdown_timer=self.countdown_timer,
|
||||||
|
label=self.ui.queue_lbl,
|
||||||
|
)
|
||||||
|
self.close_timer.timeout.connect(self.close) # wire your own close action
|
||||||
|
self._countdown.schedule(delay_ms=60_000)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, close_timer: QTimer, countdown_timer: QTimer, label: QLabel) -> None:
|
||||||
|
self._close_timer = close_timer
|
||||||
|
self._countdown_timer = countdown_timer
|
||||||
|
self._label = label
|
||||||
|
self.remaining_time: int = 0
|
||||||
|
|
||||||
|
self._countdown_timer.timeout.connect(self._tick)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public API
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def schedule(self, delay_ms: int = 60_000) -> None:
|
||||||
|
"""
|
||||||
|
Starts (or restarts) the countdown for *delay_ms* milliseconds.
|
||||||
|
|
||||||
|
:param delay_ms: Duration in milliseconds. Must be a positive integer;
|
||||||
|
invalid values silently default to 60 000 ms.
|
||||||
|
:type delay_ms: int
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if not isinstance(delay_ms, int) or delay_ms <= 0:
|
||||||
|
delay_ms = 60_000
|
||||||
|
|
||||||
|
self.remaining_time = delay_ms // 1000
|
||||||
|
|
||||||
|
# Reset running timers to avoid duplicates
|
||||||
|
self._close_timer.stop()
|
||||||
|
self._countdown_timer.stop()
|
||||||
|
|
||||||
|
self._close_timer.start(delay_ms)
|
||||||
|
self._countdown_timer.start(1_000)
|
||||||
|
|
||||||
|
# Immediate display refresh (avoids a 1 s blank)
|
||||||
|
self._refresh_display()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stops both timers without triggering the close callback."""
|
||||||
|
self._close_timer.stop()
|
||||||
|
self._countdown_timer.stop()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Internal
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _tick(self) -> None:
|
||||||
|
"""
|
||||||
|
Decrements the remaining time by one and updates the countdown timer's state.
|
||||||
|
|
||||||
|
If the remaining time reaches zero or below, the countdown timer stops
|
||||||
|
and no further operations are performed. If time remains, the display
|
||||||
|
is refreshed to reflect the updated state.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.remaining_time -= 1
|
||||||
|
|
||||||
|
if self.remaining_time <= 0:
|
||||||
|
self._countdown_timer.stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._refresh_display()
|
||||||
|
|
||||||
|
def _refresh_display(self) -> None:
|
||||||
|
"""
|
||||||
|
Updates the display label with the formatted remaining time.
|
||||||
|
|
||||||
|
The method calculates the remaining minutes and seconds from the total
|
||||||
|
remaining time and updates the label's text with a countdown timer
|
||||||
|
format.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
minutes = max(0, self.remaining_time // 60)
|
||||||
|
seconds = max(0, self.remaining_time % 60)
|
||||||
|
self._label.setText(f"⏳ Fermeture dans {minutes:02d}:{seconds:02d}")
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from PySide6.QtCore import QThread, Signal
|
||||||
|
|
||||||
|
from fivemserver.queuemanager import QueueManager
|
||||||
|
|
||||||
|
|
||||||
|
class QueueThread(QThread):
|
||||||
|
"""
|
||||||
|
A :class:`QThread` that drives a :class:`QueueManager` in a background thread
|
||||||
|
and forwards its updates via a Qt signal.
|
||||||
|
|
||||||
|
:ivar update: Emitted with a plain-text status string whenever the queue
|
||||||
|
state changes (e.g. ``"position:3:10"``, ``"ok"``).
|
||||||
|
:type update: Signal(str)
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
thread = QueueThread(user_id="123456789", parent=self)
|
||||||
|
thread.update.connect(self.handle_update)
|
||||||
|
thread.start()
|
||||||
|
# … later …
|
||||||
|
thread.stop()
|
||||||
|
thread.wait()
|
||||||
|
"""
|
||||||
|
|
||||||
|
update = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, user_id: str, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.manager = QueueManager(
|
||||||
|
user_id=user_id,
|
||||||
|
on_update=self.update.emit,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# QThread interface
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""
|
||||||
|
Entry point for the background thread: starts the queue manager loop.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.manager.start()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""
|
||||||
|
Requests a graceful stop of the queue manager.
|
||||||
|
|
||||||
|
Call :py:meth:`wait` afterwards to ensure the thread has fully exited
|
||||||
|
before destroying this object.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.manager.stop()
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""
|
||||||
|
uitools/ui_builder.py
|
||||||
|
-------------------
|
||||||
|
Helpers for dynamic UI mutations at runtime.
|
||||||
|
These functions operate on PySide6 widgets and layouts but carry no dependency
|
||||||
|
on MainWindow, making them independently testable and reusable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QAbstractButton, QLayout, QSizePolicy
|
||||||
|
|
||||||
|
from ui.hazard_stripes import HazardButton
|
||||||
|
|
||||||
|
|
||||||
|
def set_en_chantier(button: QAbstractButton, valeur: bool) -> None:
|
||||||
|
"""
|
||||||
|
Toggles the *en_chantier* (under-construction) Qt property on *button* and
|
||||||
|
forces a style refresh so QSS selectors relying on that property take effect
|
||||||
|
immediately.
|
||||||
|
|
||||||
|
:param button: The push-button whose visual state must be updated.
|
||||||
|
:type button: QAbstractButton
|
||||||
|
:param valeur: ``True`` to enable the under-construction style, ``False`` to
|
||||||
|
restore the normal style.
|
||||||
|
:type valeur: bool
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
button.setProperty("en_chantier", valeur)
|
||||||
|
button.style().unpolish(button)
|
||||||
|
button.style().polish(button)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_with_hazard_button(
|
||||||
|
old_btn: QAbstractButton,
|
||||||
|
parent_layout: QLayout,
|
||||||
|
label: str = "EN MAINTENANCE",
|
||||||
|
) -> HazardButton:
|
||||||
|
"""
|
||||||
|
Swaps *old_btn* in *parent_layout* for a :class:`HazardButton` that keeps the
|
||||||
|
same geometry, icon, and object name, then returns the new button.
|
||||||
|
|
||||||
|
The caller is responsible for connecting signals on the returned button.
|
||||||
|
|
||||||
|
:param old_btn: The existing button widget to replace.
|
||||||
|
:type old_btn: QAbstractButton
|
||||||
|
:param parent_layout: The layout that directly owns *old_btn*.
|
||||||
|
:type parent_layout: QLayout
|
||||||
|
:param label: Text displayed on the replacement button.
|
||||||
|
:type label: str
|
||||||
|
:return: The newly inserted :class:`HazardButton`.
|
||||||
|
:rtype: HazardButton
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
new_btn = replace_with_hazard_button(
|
||||||
|
self.ui.connexion_btn,
|
||||||
|
self.ui.verticalLayout_6,
|
||||||
|
)
|
||||||
|
self.ui.connexion_btn = new_btn
|
||||||
|
self.ui.connexion_btn.clicked.connect(self._on_connexion)
|
||||||
|
"""
|
||||||
|
index = parent_layout.indexOf(old_btn)
|
||||||
|
|
||||||
|
new_btn = HazardButton(old_btn.parentWidget())
|
||||||
|
new_btn.setObjectName(old_btn.objectName())
|
||||||
|
new_btn.setText(label)
|
||||||
|
new_btn.setIcon(old_btn.icon())
|
||||||
|
new_btn.setIconSize(old_btn.iconSize())
|
||||||
|
new_btn.setMinimumSize(old_btn.minimumSize())
|
||||||
|
new_btn.set_hazard(True)
|
||||||
|
|
||||||
|
parent_layout.takeAt(index)
|
||||||
|
old_btn.deleteLater()
|
||||||
|
parent_layout.insertWidget(index, new_btn)
|
||||||
|
|
||||||
|
return new_btn
|
||||||
|
|
||||||
|
|
||||||
|
def hide_staff_btn_and_recenter(staff_btn: QAbstractButton, layout: QLayout) -> None:
|
||||||
|
"""
|
||||||
|
Hides *staff_btn* and adjusts the first spacer found in *layout* to
|
||||||
|
vertically re-centre the remaining connexion button.
|
||||||
|
|
||||||
|
:param staff_btn: The staff-only button to hide.
|
||||||
|
:type staff_btn: QAbstractButton
|
||||||
|
:param layout: The vertical layout that contains both the spacer and
|
||||||
|
*staff_btn*.
|
||||||
|
:type layout: QLayout
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
staff_btn.hide()
|
||||||
|
for i in range(layout.count()):
|
||||||
|
item = layout.itemAt(i)
|
||||||
|
if item and item.spacerItem():
|
||||||
|
item.spacerItem().changeSize(
|
||||||
|
20, 15,
|
||||||
|
QSizePolicy.Policy.Fixed,
|
||||||
|
QSizePolicy.Policy.Fixed,
|
||||||
|
)
|
||||||
|
layout.invalidate()
|
||||||
|
break
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from PySide6.QtGui import QCursor, QGuiApplication
|
||||||
|
from PySide6.QtWidgets import QWidget
|
||||||
|
|
||||||
|
|
||||||
|
def center_window(window: QWidget) -> None:
|
||||||
|
"""
|
||||||
|
Centers the given window on the screen where the cursor is currently located.
|
||||||
|
Falls back to the primary screen if the cursor is not on any screen.
|
||||||
|
|
||||||
|
:param window: The Qt window/widget to center.
|
||||||
|
:type window: QWidget
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
window.adjustSize()
|
||||||
|
screen = (
|
||||||
|
QGuiApplication.screenAt(QCursor.pos())
|
||||||
|
or QGuiApplication.primaryScreen()
|
||||||
|
)
|
||||||
|
rect = window.frameGeometry()
|
||||||
|
rect.moveCenter(screen.availableGeometry().center())
|
||||||
|
window.move(rect.topLeft())
|
||||||
Reference in New Issue
Block a user