Refactor main_window into multiples files
This commit is contained in:
1 parent
b5820eb301
commit
457f7b11e5
5 files changed
+381
-371
No files matched your search
+103
-371
@@ -2,10 +2,10 @@ import webbrowser
|
||||
from os import environ, unlink
|
||||
from sys import platform
|
||||
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QIcon, QMouseEvent, QCursor, QGuiApplication
|
||||
from PySide6.QtWidgets import QMainWindow, QSizePolicy
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon, QMouseEvent, QGuiApplication
|
||||
from PySide6.QtWidgets import QMainWindow
|
||||
|
||||
from config.config_manager import ConfigManager
|
||||
from config.constants import PlayerServerInfo, Urls
|
||||
@@ -13,29 +13,33 @@ from controllers.audio_controller import AudioController
|
||||
from controllers.glow_animator import GlowAnimator
|
||||
from controllers.window_dragger import WindowDragger
|
||||
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.fivemlauncher import FiveMLauncher
|
||||
from fivemserver.get_server_token import GetServerTokenForDiscord
|
||||
from fivemserver.queuemanager import QueueManager
|
||||
from fivemserver.whitelistmanager import WhiteList
|
||||
from tools.http_client import ApiError
|
||||
from ui.error_dialog import show_qt_error
|
||||
from ui.hazard_stripes import HazardButton
|
||||
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
|
||||
if platform.startswith('linux'):
|
||||
environ["QT_QPA_PLATFORM"] = "xcb"
|
||||
environ["QT_QPA_PLATFORM"] = "xcb"
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""
|
||||
MainWindow class for managing the main application window and its functionalities.
|
||||
|
||||
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.
|
||||
The class is designed to be used with the PyQt framework and provides logic for dynamically updating
|
||||
the UI elements and interacting with other components.
|
||||
It provides mechanisms to handle user authentication, connection to external services,
|
||||
and queued tasks. The class is designed to be used with the PySide6 framework and
|
||||
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.
|
||||
:type ui: Ui_MainWindow
|
||||
@@ -44,17 +48,10 @@ class MainWindow(QMainWindow):
|
||||
:ivar stored_user_id: The Discord user ID stored in the configuration.
|
||||
:type stored_user_id: str
|
||||
: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.
|
||||
: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):
|
||||
super().__init__()
|
||||
@@ -62,53 +59,51 @@ class MainWindow(QMainWindow):
|
||||
self.ui = Ui_MainWindow()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.auth_worker = None # <--- Pour stocker l'instance du thread
|
||||
|
||||
self.auth_worker = None
|
||||
self.config = config_manager
|
||||
self.stored_user_id = self.config.get_discord_user()
|
||||
self.queue_thread = 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)
|
||||
self.close_timer.setSingleShot(True)
|
||||
self.close_timer.timeout.connect(self.close)
|
||||
# ------------------------------------------------------------------
|
||||
# Timers (owned here so they share the QObject tree of MainWindow)
|
||||
# ------------------------------------------------------------------
|
||||
close_timer = QTimer(self)
|
||||
close_timer.setSingleShot(True)
|
||||
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)
|
||||
countdown_timer = QTimer(self)
|
||||
|
||||
# UI
|
||||
# self.ui = QUiLoader().load(f"{bundle_dir}/ui/mainwindow_vertical_pager.ui", self)
|
||||
#self.setCentralWidget(self.ui.centralWidget())
|
||||
self._countdown = CountdownManager(
|
||||
close_timer=close_timer,
|
||||
countdown_timer=countdown_timer,
|
||||
label=self.ui.queue_lbl,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Window flags
|
||||
# ------------------------------------------------------------------
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
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)
|
||||
# On cache par défaut les infos liste d'attente
|
||||
self.ui.queue_lbl.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():
|
||||
self.ui.stackedWidget.setCurrentIndex(1)
|
||||
else:
|
||||
try:
|
||||
# on vérifie si le joueur est whitelisté
|
||||
WhiteList.check_whitelist(Urls.API_URL.value, self.stored_user_id)
|
||||
except ApiError as exc:
|
||||
show_qt_error(self, "La Tanière", f"Impossible de vérifier la whitelist.\n\n{exc}")
|
||||
PlayerServerInfo.is_whitelist = False
|
||||
PlayerServerInfo.is_staff = False
|
||||
|
||||
# si on est whitelisté, on démarre la file d'attente
|
||||
if PlayerServerInfo.is_whitelist:
|
||||
self.start_queue()
|
||||
self.ui.queue_lbl.show()
|
||||
@@ -116,65 +111,50 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
self.ui.stackedWidget.setCurrentIndex(2)
|
||||
|
||||
# Test bouton en contruction
|
||||
# ------------------------------------------------------------------
|
||||
# Under-construction button (delegated to ui_builder)
|
||||
# ------------------------------------------------------------------
|
||||
en_chantier = False
|
||||
# on set la css du bouton en fonction de la valeur de la variable en_chantier
|
||||
self.set_en_chantier(en_chantier)
|
||||
set_en_chantier(self.ui.connexion_btn, en_chantier)
|
||||
|
||||
if en_chantier:
|
||||
old_btn = self.ui.connexion_btn
|
||||
parent_layout = self.ui.verticalLayout_6 # layout direct du bouton dans le .ui
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# centrage vertical du bouton connexion
|
||||
# ------------------------------------------------------------------
|
||||
# Staff button / spacer adjustment (delegated to ui_builder)
|
||||
# ------------------------------------------------------------------
|
||||
if not PlayerServerInfo.is_staff:
|
||||
self.ui.staff_btn.hide()
|
||||
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
|
||||
hide_staff_btn_and_recenter(self.ui.staff_btn, self.ui.verticalLayout_6)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Misc UI
|
||||
# ------------------------------------------------------------------
|
||||
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._glow = GlowAnimator(self.ui.connexion_btn)
|
||||
self._dragger = WindowDragger(self)
|
||||
|
||||
self._connect_signals()
|
||||
self._center_window()
|
||||
center_window(self) # delegated to window_utils
|
||||
|
||||
self.show()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Setup
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _connect_signals(self) -> None:
|
||||
"""
|
||||
Establishes connections between UI elements and their corresponding
|
||||
functionality. Each signal emitted by a user interaction with a specific
|
||||
UI button is linked to its respective handler method.
|
||||
Establishes connections between UI elements and their corresponding handler methods.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
@@ -186,23 +166,6 @@ class MainWindow(QMainWindow):
|
||||
self.ui.discord_auth_btn.clicked.connect(self._on_discord_auth_btn)
|
||||
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
|
||||
# ------------------------------------------------------------------
|
||||
@@ -210,33 +173,25 @@ class MainWindow(QMainWindow):
|
||||
def _on_connexion(self) -> None:
|
||||
"""
|
||||
Handles the user connection process, including verification of prerequisites,
|
||||
session validation, and background authentication for the current user. This
|
||||
method ensures a smooth user experience by implementing UI locking during
|
||||
authentication and proceeding with appropriate actions if a valid session
|
||||
already exists.
|
||||
session validation, and background authentication for the current user.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
QGuiApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
|
||||
# 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.finished.connect(self.auth_worker.deleteLater)
|
||||
self.auth_worker.start()
|
||||
|
||||
@staticmethod
|
||||
@@ -244,18 +199,13 @@ class MainWindow(QMainWindow):
|
||||
"""
|
||||
Opens the Discord URL in the default web browser.
|
||||
|
||||
This method is a static utility function that launches the Discord URL defined
|
||||
in the Urls enumeration. It uses the `webbrowser.open` method to handle the
|
||||
opening process.
|
||||
|
||||
:return: Nothing is returned
|
||||
:rtype: None
|
||||
:return: None
|
||||
"""
|
||||
webbrowser.open(Urls.DISCORD.value)
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -266,32 +216,22 @@ class MainWindow(QMainWindow):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
try:
|
||||
# 1. Récupération OAuth (souvent via navigateur, assez rapide)
|
||||
test = discord_oauth.get_discord_user_id()
|
||||
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()
|
||||
|
||||
# 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()
|
||||
@@ -305,17 +245,9 @@ class MainWindow(QMainWindow):
|
||||
"""
|
||||
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.
|
||||
:type success: bool
|
||||
: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.
|
||||
:type error_message: str
|
||||
:return: None
|
||||
"""
|
||||
QGuiApplication.restoreOverrideCursor()
|
||||
@@ -323,16 +255,15 @@ class MainWindow(QMainWindow):
|
||||
|
||||
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) # Retour page principale
|
||||
self.ui.stackedWidget.setCurrentIndex(0)
|
||||
else:
|
||||
self.ui.stackedWidget.setCurrentIndex(2) # Page non-whitelisté
|
||||
self.ui.stackedWidget.setCurrentIndex(2)
|
||||
except ApiError as exc:
|
||||
show_qt_error(self, "La Tanière", f"Impossible de vérifier la whitelist.\n\n{exc}")
|
||||
else:
|
||||
@@ -340,53 +271,20 @@ class MainWindow(QMainWindow):
|
||||
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:
|
||||
"""
|
||||
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)
|
||||
# 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:
|
||||
"""
|
||||
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)
|
||||
if not event.isAccepted():
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
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)
|
||||
if not event.isAccepted():
|
||||
super().mouseReleaseEvent(event)
|
||||
@@ -397,39 +295,24 @@ class MainWindow(QMainWindow):
|
||||
|
||||
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
|
||||
"""
|
||||
if self.queue_thread and self.queue_thread.isRunning():
|
||||
self.queue_thread.stop()
|
||||
self.queue_thread.wait() # Attend que le thread se termine proprement
|
||||
self.queue_thread.wait()
|
||||
self.config.save()
|
||||
self.cleanup()
|
||||
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
|
||||
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.
|
||||
:return: None
|
||||
"""
|
||||
if self.close_timer:
|
||||
self.close_timer.stop()
|
||||
|
||||
if self.countdown_timer:
|
||||
self.countdown_timer.stop()
|
||||
self._countdown.stop()
|
||||
|
||||
if hasattr(self, '_sound'):
|
||||
self._sound.stop()
|
||||
@@ -440,120 +323,44 @@ class MainWindow(QMainWindow):
|
||||
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)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schedule de fermeture du launcher
|
||||
# ------------------------------------------------------------------
|
||||
def schedule_close(self, delay_ms: int = 60000):
|
||||
def schedule_close(self, delay_ms: int = 60_000) -> None:
|
||||
"""
|
||||
Schedules a process to close after a specified delay in milliseconds. The countdown
|
||||
timer also starts to reflect the remaining time in seconds. Timers already running
|
||||
are reset to prevent duplicate behavior, and the display is updated immediately.
|
||||
Schedules the launcher to close after *delay_ms* milliseconds and starts
|
||||
the visual countdown.
|
||||
|
||||
:param delay_ms: The delay in milliseconds after which the process should
|
||||
be closed. Defaults to 60000 milliseconds (1 minute) if not specified
|
||||
or an invalid value is provided.
|
||||
:param delay_ms: Delay in milliseconds (default: 60 000).
|
||||
:type delay_ms: int
|
||||
:return: None
|
||||
"""
|
||||
# 1. Sécurité sur les entrées
|
||||
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}")
|
||||
self._countdown.schedule(delay_ms)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 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
|
||||
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.
|
||||
Initialises and starts the :class:`~tools.queue_thread.QueueThread`.
|
||||
|
||||
All threading and queue update mechanisms are encapsulated within this method.
|
||||
|
||||
:param self: Reference to the calling object.
|
||||
: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.start()
|
||||
|
||||
# 🧪 TEMP - Simule une position en queue pour tester l'UI
|
||||
# self.handle_update("position:3:10")
|
||||
|
||||
def handle_update(self, message: str):
|
||||
def handle_update(self, message: str) -> None:
|
||||
"""
|
||||
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:
|
||||
- "ok": Indicates that it's the user's turn, updating the UI accordingly.
|
||||
- "ready": Equivalent to "ok", indicating the user's turn.
|
||||
- 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.
|
||||
:param message: Status string – ``"ok"``, ``"ready"``, or
|
||||
``"position:<current>:<total>"``.
|
||||
:type message: str
|
||||
:return: None
|
||||
"""
|
||||
if message == "ok":
|
||||
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":
|
||||
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 !")
|
||||
@@ -569,43 +376,28 @@ class MainWindow(QMainWindow):
|
||||
def launch_fivem(self):
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Session helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _ensure_server_session(self) -> bool:
|
||||
"""
|
||||
Ensures the establishment of a server session, handling authentication
|
||||
and session registration for a Discord user.
|
||||
Ensures a valid server session exists, authenticating if necessary.
|
||||
|
||||
The method first checks if a valid stored user ID exists. If there is
|
||||
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.
|
||||
:return: ``True`` if the session was established successfully.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not self.stored_user_id or self.stored_user_id.isspace():
|
||||
return False
|
||||
|
||||
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,
|
||||
@@ -623,107 +415,47 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _on_auth_finished(self, success: bool, session_id: str, error_message: str):
|
||||
"""
|
||||
Handles the completion of the authentication process by updating the UI,
|
||||
cleaning up the worker, and proceeding with subsequent logic based on the
|
||||
authentication result.
|
||||
Restores the UI and proceeds after the background authentication completes.
|
||||
|
||||
:param success: A boolean indicating whether the authentication was successful.
|
||||
:param session_id: The session identifier returned upon successful authentication.
|
||||
:param error_message: A descriptive error message in case authentication failed.
|
||||
:param success: Whether authentication succeeded.
|
||||
:param session_id: Session token returned on success.
|
||||
:param error_message: Human-readable error on failure.
|
||||
:return: None
|
||||
"""
|
||||
# 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
|
||||
self.ui.connexion_btn.setText("SE CONNECTER")
|
||||
|
||||
# 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}")
|
||||
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
|
||||
position in the queue, or launching the FiveM application if all conditions
|
||||
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.
|
||||
Starts the queue if not yet entered, shows the position, or launches FiveM
|
||||
when the user reaches position 0.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
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}")
|
||||
|
||||
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