From e1b32688b47592eacf21dcbbd256137169ad0dea Mon Sep 17 00:00:00 2001 From: Xarkam Date: Fri, 13 Mar 2026 13:45:49 +0100 Subject: [PATCH] =?UTF-8?q?Refacto=20=C3=A0=20100%=20pour=20la=20maintenab?= =?UTF-8?q?ilit=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mainwindow.py | 393 +++++++++++++++++++++++++--------------------- 1 file changed, 217 insertions(+), 176 deletions(-) diff --git a/src/mainwindow.py b/src/mainwindow.py index c2236fe..b430bb8 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -1,5 +1,4 @@ import sys -import os import webbrowser from pathlib import Path from PySide6 import QtGui @@ -20,6 +19,24 @@ import resources as resources # This is generated from the .qrc file # noqa: # Remove this into final release from fake_patch_notes import patch_note +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +URLS = { + "discord": "https://discord.gg/A7eanmSkp2", + "intranet": "https://la-taniere.fun/connexion/", +} + +GLOW_COLOR = QColor(255, 140, 0, 255) +GLOW_BLUR_BASE = 15 +GLOW_BLUR_PEAK = 70 +GLOW_ANIM_DURATION = 1200 + +NO_STAFF = True + +# --------------------------------------------------------------------------- +# Bundle path resolution +# --------------------------------------------------------------------------- if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): bundle_dir = Path(sys._MEIPASS) else: @@ -28,9 +45,9 @@ else: # charger le fichier rcc compilé QResource.registerResource(f"{bundle_dir}/resources.py") -NO_STAFF = True -CURRENT = os.path.dirname(os.path.realpath(__file__)) - +# --------------------------------------------------------------------------- +# Font helper +# --------------------------------------------------------------------------- def load_custom_font() -> str: # Load font from Qt resource font_id = QFontDatabase.addApplicationFont(":/assets/Avocado-Cake-Demo.otf") @@ -43,6 +60,154 @@ def load_custom_font() -> str: raise RuntimeError("No font families found in the loaded font.") return font_families[0] +# --------------------------------------------------------------------------- +# AudioController +# --------------------------------------------------------------------------- + +class AudioController: + # Encapsule toute la logique audio : lecture, volume, mute. + + def __init__(self, config: ConfigManager, slider, mute_btn): + self._config = config + self._slider = slider + self._mute_btn = mute_btn + + # Lecteur + self._player = QMediaPlayer() + self._output = QAudioOutput() + self._player.setAudioOutput(self._output) + self._player.setLoops(-1) + + # Chargement du MP3 depuis les ressources Qt + mp3file = QFile(":/assets/the-beat-of-nature.mp3") + mp3file.open(QFile.ReadOnly) + mp3data = mp3file.readAll() + mp3file.close() + + self._buffer = QBuffer() + self._buffer.setData(QByteArray(mp3data)) + self._buffer.open(QIODevice.ReadOnly) + self._player.setSourceDevice(self._buffer) + + # État initial + volume = config.get_volume() + self._is_muted = volume == 0 + self._previous_volume = ( + volume if volume != 0 else config.get_default(VOLUME_KEY) + ) + + self._apply_volume(volume, save=False) + self._refresh_mute_btn() + self._player.play() + + # Connexions + self._slider.valueChanged.connect(self._on_slider_changed) + self._mute_btn.clicked.connect(self.toggle_mute) + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def toggle_mute(self) -> None: + if not self._is_muted: + self._previous_volume = self._slider.value() + self._apply_volume(0) + self._is_muted = True + else: + self._apply_volume(self._previous_volume) + self._is_muted = False + + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ + + def _on_slider_changed(self, value: int) -> None: + # Appelé quand l'utilisateur bouge le slider directement. + self._is_muted = (value == 0) + self._output.setVolume(value / 100.0) + self._config.set_volume(value) + self._refresh_mute_btn() + + def _apply_volume(self, value: int, save: bool = True) -> None: + # Applique le volume sur le slider, l'output et la config. + # Bloquer le signal pour éviter une double mise à jour + self._slider.blockSignals(True) + self._slider.setValue(value) + self._slider.blockSignals(False) + + self._output.setVolume(value / 100.0) + + if save: + self._config.set_volume(value) + + self._refresh_mute_btn() + + def _refresh_mute_btn(self) -> None: + muted = self._slider.value() == 0 + self._mute_btn.setProperty("muted", muted) + self._mute_btn.style().unpolish(self._mute_btn) + self._mute_btn.style().polish(self._mute_btn) + + +# --------------------------------------------------------------------------- +# GlowAnimator +# --------------------------------------------------------------------------- + +class GlowAnimator: + # Gère l'effet de lueur pulsée sur un widget. + + def __init__(self, widget): + self._widget = widget + + self._effect = QGraphicsDropShadowEffect(widget) + self._effect.setBlurRadius(GLOW_BLUR_BASE) + self._effect.setOffset(0, 0) + self._effect.setColor(GLOW_COLOR) + + self._anim = QPropertyAnimation(self._effect, b"blurRadius") + self._anim.setDuration(GLOW_ANIM_DURATION) + self._anim.setStartValue(GLOW_BLUR_BASE) + self._anim.setKeyValueAt(0.5, GLOW_BLUR_PEAK) + self._anim.setEndValue(GLOW_BLUR_BASE) + self._anim.setEasingCurve(QEasingCurve.InOutQuad) + self._anim.setLoopCount(-1) + + def start(self) -> None: + self._widget.setGraphicsEffect(self._effect) + self._anim.start() + + def stop(self) -> None: + self._anim.stop() + self._widget.setGraphicsEffect(None) + +# --------------------------------------------------------------------------- +# WindowDragger +# --------------------------------------------------------------------------- + +class WindowDragger: + # Permet de déplacer une fenêtre sans barre de titre. + + def __init__(self, window: QMainWindow): + self._window = window + self._drag_pos = None + + def mouse_press(self, event: QtGui.QMouseEvent) -> None: + if event.button() == Qt.MouseButton.LeftButton: + self._drag_pos = ( + event.globalPosition().toPoint() + - self._window.frameGeometry().topLeft() + ) + + def mouse_move(self, event: QtGui.QMouseEvent) -> None: + if event.buttons() & Qt.MouseButton.LeftButton and self._drag_pos is not None: + self._window.move(event.globalPosition().toPoint() - self._drag_pos) + + def mouse_release(self, _event) -> None: + self._drag_pos = None + +# --------------------------------------------------------------------------- +# MainWindow +# --------------------------------------------------------------------------- class MainWindow(QMainWindow): def __init__(self): super().__init__() @@ -50,195 +215,69 @@ class MainWindow(QMainWindow): # Initialisation de la configuration self.config = ConfigManager() - # Chargement de l'interface + # UI self.ui = QUiLoader().load(f"{bundle_dir}/ui/mainwindow.ui", self) self.setCentralWidget(self.ui.centralWidget()) - - # Remove the title bar and window frame self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window) - # Optional: Make background transparent (if you want rounded corners, etc.) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - # Initialize audio - self.media_player = QMediaPlayer() - self.audio_output = QAudioOutput() - self.media_player.setAudioOutput(self.audio_output) - self.media_player.setLoops(-1) - - # Using mp3 in resource - mp3file = QFile(":/assets/the-beat-of-nature.mp3") - mp3file.open(QFile.ReadOnly) - mp3data = mp3file.readAll() - mp3file.close() - - # Buffer mémoire - mp3buffer = QBuffer() - mp3buffer.setData(QByteArray(mp3data)) - mp3buffer.open(QIODevice.ReadOnly) - self.mp3buffer = mp3buffer - - self.media_player.setSourceDevice(self.mp3buffer) - - volume = self.config.get_volume() - - # Définir la valeur initiale du slider (ici: 10%) - self.ui.audio_volume_adjust.setValue(volume) - self.audio_output.setVolume(volume / 100) - - self.is_muted = volume == 0 - self.previous_volume = volume if volume != 0 else self.config.get_default(VOLUME_KEY) - - if self.is_muted: - self.ui.mute_btn.setProperty("muted", True) - - # Connexion du Slider - self.ui.audio_volume_adjust.valueChanged.connect(self.update_volume) - - # Lancer la lecture (par exemple ici, ou dans une fonction) - self.media_player.play() - - # Track mouse position for dragging - self._drag_pos = None - - if NO_STAFF : + if NO_STAFF: self.ui.staff_btn.hide() self.ui.spacer_substitution.hide() self.ui.info_text.setMarkdown(patch_note) - self.setup_button_animation() - self.connect_signals() + # Sous-systèmes + 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) - # On centre la fenêtre avant de l'afficher - self.center_window() + self._connect_signals() + self._center_window() self.show() - #------------------------------------------------------------------------------------------------------------ - - def setup_button_animation(self): - - # 1. Création de l'effet de lueur intense - glow = QGraphicsDropShadowEffect(self.ui.connexion_btn) - glow.setBlurRadius(20) # Point de départ - glow.setOffset(0, 0) # Centré - glow.setColor(QColor(255, 140, 0, 255)) # Orange pur (Alpha max) - - self.ui.connexion_btn.glow = glow - - # 2. Animation du "Pulse" avec une grande amplitude - anim = QPropertyAnimation(glow, b"blurRadius") - anim.setDuration(1200) # Un peu plus rapide pour le dynamisme - anim.setStartValue(15) # Lueur de base - anim.setKeyValueAt(0.5, 70) # Lueur d'explosion (très large) - anim.setEndValue(15) - - anim.setEasingCurve(QEasingCurve.InOutQuad) - anim.setLoopCount(-1) - - self.ui.connexion_btn.pulse_anim = anim - - def connect_signals(self): + # ------------------------------------------------------------------ + # Setup + # ------------------------------------------------------------------ + def _connect_signals(self) -> None: self.ui.close_btn.clicked.connect(self.close) self.ui.minimize_btn.clicked.connect(self.showMinimized) - self.ui.connexion_btn.clicked.connect(self.connexion_btn_link) - self.ui.discord_btn.clicked.connect(self.discord_btn_link) - self.ui.intranet_btn.clicked.connect(self.intranet_btn_link) - self.ui.mute_btn.clicked.connect(self.mute_btn_link) + self.ui.connexion_btn.clicked.connect(self._on_connexion) + self.ui.discord_btn.clicked.connect(self._on_discord) + self.ui.intranet_btn.clicked.connect(self._on_intranet) + # Le bouton mute est connecté dans la classe AudioController + #self.ui.mute_btn.clicked.connect(self.mute_btn_link) - def set_volume(self, value): - self.audio_output.setVolume(value / 100) - self.ui.audio_volume_adjust.setValue(value) - - muted = value == 0 - - self.ui.mute_btn.setProperty("muted", muted) - - self.ui.mute_btn.style().unpolish(self.ui.mute_btn) - self.ui.mute_btn.style().polish(self.ui.mute_btn) - - def update_volume(self, value): - # 'value' est l'entier venant du slider (ex: 0 à 100) - # On convertit en float pour QAudioOutput (0.0 à 1.0) - volume = value / 100.0 - self.audio_output.setVolume(volume) - self.config.set_volume(value) - - if value == 0: - self.ui.mute_btn.setProperty("muted", True) - else: - self.ui.mute_btn.setProperty("muted", False) - - self.ui.mute_btn.style().unpolish(self.ui.mute_btn) - self.ui.mute_btn.style().polish(self.ui.mute_btn) - - def mute_btn_link(self) -> None: - current_volume = self.ui.audio_volume_adjust.value() - - if current_volume == 0 and self.is_muted is False: - # volume déjà à 0 → restaurer le volume par défaut - default_volume = self.config.get_default(VOLUME_KEY) - - self.ui.audio_volume_adjust.setValue(default_volume) - self.audio_output.setVolume(default_volume / 100) - - self.config.set_volume(default_volume) - - self.ui.mute_btn.setStyleSheet("") - - return - - if not self.is_muted: - # --- PASSAGE EN MUTE --- - self.previous_volume = current_volume - - self.audio_output.setVolume(0.0) - self.ui.audio_volume_adjust.setValue(0) - - self.is_muted = True - self.config.set_volume(0) - - else: - # --- RESTAURATION --- - volume_float = self.previous_volume / 100 - - self.audio_output.setVolume(volume_float) - self.ui.audio_volume_adjust.setValue(self.previous_volume) - - self.ui.mute_btn.setStyleSheet("") - - self.is_muted = False - self.config.set_volume(self.previous_volume) - - @staticmethod - def connexion_btn_link(): - return None - - @staticmethod - def discord_btn_link(): - webbrowser.open('https://discord.gg/A7eanmSkp2') - - def intranet_btn_link(self): - webbrowser.open('https://la-taniere.fun/connexion/') - self.ui.connexion_btn.setGraphicsEffect(self.ui.connexion_btn.glow) - self.ui.connexion_btn.pulse_anim.start() - - def center_window(self): + def _center_window(self) -> None: # On s'assure que la fenêtre a calculé sa taille self.adjustSize() + screen = QtGui.QGuiApplication.screenAt(QtGui.QCursor.pos()) \ + or QtGui.QGuiApplication.primaryScreen() + rect = self.frameGeometry() + rect.moveCenter(screen.availableGeometry().center()) + self.move(rect.topLeft()) - screen = QtGui.QGuiApplication.screenAt(QtGui.QCursor.pos()) - if not screen: - screen = QtGui.QGuiApplication.primaryScreen() + # ------------------------------------------------------------------ + # Button handlers + # ------------------------------------------------------------------ - screen_geometry = screen.availableGeometry() + @staticmethod + def _on_connexion(): + pass - # On utilise frameGeometry() de la fenêtre elle-même - window_rect = self.frameGeometry() - window_rect.moveCenter(screen_geometry.center()) + @staticmethod + def _on_discord(): + webbrowser.open(URLS["discord"]) - self.move(window_rect.topLeft()) + def _on_intranet(self): + webbrowser.open(URLS["intranet"]) + self._glow.start() + + # ------------------------------------------------------------------ + # Mouse events → délégués au WindowDragger + # ------------------------------------------------------------------ # Mouse press event to start dragging def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: @@ -257,23 +296,25 @@ class MainWindow(QMainWindow): self._drag_pos = None super().mouseReleaseEvent(event) + # ------------------------------------------------------------------ + # Close + # ------------------------------------------------------------------ def closeEvent(self, event): self.config.save() super().closeEvent(event) +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- if __name__ == "__main__": app = QApplication(sys.argv) + # Set the stylesheet of the application with open(f"{bundle_dir}/styles/styles.qss", 'r') as f: - style = f.read() + app.setStyleSheet(f.read()) # Load and set the global font - custom_font = QFont(load_custom_font(), 16) - if custom_font: - app.setFont(custom_font) - - # Set the stylesheet of the application - app.setStyleSheet(style) + app.setFont(QFont(load_custom_font(), 16)) window = MainWindow()