From d0ede2acd50800db5658c32dd4d99d7cc6c71bfd Mon Sep 17 00:00:00 2001 From: Xarkam Date: Fri, 13 Mar 2026 13:55:45 +0100 Subject: [PATCH] =?UTF-8?q?Refacto=20en=20sous=20fichiers=20pour=20la=20ma?= =?UTF-8?q?intenabilit=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants.py | 18 ++ src/controllers/audio_controller.py | 85 ++++++++ src/controllers/glow_animator.py | 32 +++ src/controllers/window_dragger.py | 25 +++ src/main.py | 52 +++++ src/mainwindow.py | 321 ---------------------------- src/ui/main_window.py | 101 +++++++++ 7 files changed, 313 insertions(+), 321 deletions(-) create mode 100644 src/constants.py create mode 100644 src/controllers/audio_controller.py create mode 100644 src/controllers/glow_animator.py create mode 100644 src/controllers/window_dragger.py create mode 100644 src/main.py delete mode 100644 src/mainwindow.py create mode 100644 src/ui/main_window.py diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..b0b843a --- /dev/null +++ b/src/constants.py @@ -0,0 +1,18 @@ +from PySide6.QtGui import QColor + +# --------------------------------------------------------------------------- +# 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 + +MP3_PATH = ":/assets/the-beat-of-nature.mp3" + +NO_STAFF = True diff --git a/src/controllers/audio_controller.py b/src/controllers/audio_controller.py new file mode 100644 index 0000000..bc5d054 --- /dev/null +++ b/src/controllers/audio_controller.py @@ -0,0 +1,85 @@ +from PySide6.QtCore import QFile, QBuffer, QByteArray, QIODevice +from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput + +from config.config_manager import ConfigManager, VOLUME_KEY + +from src.constants import MP3_PATH + +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(MP3_PATH) + 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: + 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: + 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) diff --git a/src/controllers/glow_animator.py b/src/controllers/glow_animator.py new file mode 100644 index 0000000..3d40a58 --- /dev/null +++ b/src/controllers/glow_animator.py @@ -0,0 +1,32 @@ +from PySide6.QtCore import QPropertyAnimation, QEasingCurve +from PySide6.QtWidgets import QGraphicsDropShadowEffect + +from src.constants import GLOW_COLOR, GLOW_BLUR_BASE, GLOW_BLUR_PEAK, GLOW_ANIM_DURATION + + +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) diff --git a/src/controllers/window_dragger.py b/src/controllers/window_dragger.py new file mode 100644 index 0000000..07e7909 --- /dev/null +++ b/src/controllers/window_dragger.py @@ -0,0 +1,25 @@ +from PySide6 import QtGui +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QMainWindow + + +class WindowDragger: + # Permet de déplacer une fenêtre sans barre de titre. + + def __init__(self, window: QMainWindow): + self._window = window + self._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 diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..709d543 --- /dev/null +++ b/src/main.py @@ -0,0 +1,52 @@ +import sys +from pathlib import Path + +from PySide6.QtCore import QResource +from PySide6.QtGui import QFontDatabase, QFont +from PySide6.QtWidgets import QApplication + +import resources as resources # noqa: F401 + +from ui.main_window import MainWindow + + +# --------------------------------------------------------------------------- +# Bundle path resolution +# --------------------------------------------------------------------------- + +if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + bundle_dir = Path(sys._MEIPASS) +else: + bundle_dir = Path(__file__).resolve().parent.parent + +QResource.registerResource(f"{bundle_dir}/resources.py") + + +# --------------------------------------------------------------------------- +# Font helper +# --------------------------------------------------------------------------- + +def load_custom_font() -> str: + font_id = QFontDatabase.addApplicationFont(":/assets/Avocado-Cake-Demo.otf") + if font_id == -1: + raise RuntimeError("Failed to load font from resources.") + font_families = QFontDatabase.applicationFontFamilies(font_id) + if not font_families: + raise RuntimeError("No font families found in the loaded font.") + return font_families[0] + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + app = QApplication(sys.argv) + + with open(f"{bundle_dir}/styles/styles.qss", 'r') as f: + app.setStyleSheet(f.read()) + + app.setFont(QFont(load_custom_font(), 16)) + + window = MainWindow(bundle_dir) + sys.exit(app.exec()) diff --git a/src/mainwindow.py b/src/mainwindow.py deleted file mode 100644 index b430bb8..0000000 --- a/src/mainwindow.py +++ /dev/null @@ -1,321 +0,0 @@ -import sys -import webbrowser -from pathlib import Path -from PySide6 import QtGui -from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QResource, QFile, QBuffer, QByteArray, QIODevice -from PySide6.QtGui import QFontDatabase, QFont, QColor -from PySide6.QtUiTools import QUiLoader -from PySide6.QtWidgets import QMainWindow, QApplication, QGraphicsDropShadowEffect -from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput - -from config.config_manager import ConfigManager, VOLUME_KEY - -# Compile resources.qrc into resources_rc.py -# rcc -g python .\resources.qrc -o .\src\resources_rc.py - -# import utilisé pour la font custom -import resources as resources # This is generated from the .qrc file # noqa: F401 - -# 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: - bundle_dir = Path(__file__).resolve().parent.parent - -# charger le fichier rcc compilé -QResource.registerResource(f"{bundle_dir}/resources.py") - -# --------------------------------------------------------------------------- -# Font helper -# --------------------------------------------------------------------------- -def load_custom_font() -> str: - # Load font from Qt resource - font_id = QFontDatabase.addApplicationFont(":/assets/Avocado-Cake-Demo.otf") - if font_id == -1: - raise RuntimeError("Failed to load font from resources.") - - # Get the family name of the loaded font - font_families = QFontDatabase.applicationFontFamilies(font_id) - if not font_families: - 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__() - - # Initialisation de la configuration - self.config = ConfigManager() - - # UI - self.ui = QUiLoader().load(f"{bundle_dir}/ui/mainwindow.ui", self) - self.setCentralWidget(self.ui.centralWidget()) - self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window) - self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - - if NO_STAFF: - self.ui.staff_btn.hide() - self.ui.spacer_substitution.hide() - - self.ui.info_text.setMarkdown(patch_note) - - # 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) - - self._connect_signals() - self._center_window() - self.show() - - # ------------------------------------------------------------------ - # 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._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 _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()) - - # ------------------------------------------------------------------ - # Button handlers - # ------------------------------------------------------------------ - - @staticmethod - def _on_connexion(): - pass - - @staticmethod - def _on_discord(): - webbrowser.open(URLS["discord"]) - - 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: - if event.button() == Qt.MouseButton.LeftButton: - self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft() - super().mousePressEvent(event) - - # Mouse move event to drag window - def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: - if event.buttons() & Qt.MouseButton.LeftButton and self._drag_pos is not None: - self.move(event.globalPosition().toPoint() - self._drag_pos) - super().mouseMoveEvent(event) - - # Mouse release event to stop dragging - def mouseReleaseEvent(self, event): - 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: - app.setStyleSheet(f.read()) - - # Load and set the global font - app.setFont(QFont(load_custom_font(), 16)) - - window = MainWindow() - - sys.exit(app.exec()) diff --git a/src/ui/main_window.py b/src/ui/main_window.py new file mode 100644 index 0000000..c0c1210 --- /dev/null +++ b/src/ui/main_window.py @@ -0,0 +1,101 @@ +import webbrowser + +from PySide6 import QtGui +from PySide6.QtCore import Qt +from PySide6.QtUiTools import QUiLoader +from PySide6.QtWidgets import QMainWindow + +from config.config_manager import ConfigManager +from src.constants import URLS, NO_STAFF +from controllers.audio_controller import AudioController +from controllers.glow_animator import GlowAnimator +from controllers.window_dragger import WindowDragger +from fake_patch_notes import patch_note + + +class MainWindow(QMainWindow): + def __init__(self, bundle_dir): + super().__init__() + + self.config = ConfigManager() + + # UI + self.ui = QUiLoader().load(f"{bundle_dir}/ui/mainwindow.ui", self) + self.setCentralWidget(self.ui.centralWidget()) + self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + if NO_STAFF: + self.ui.staff_btn.hide() + self.ui.spacer_substitution.hide() + + self.ui.info_text.setMarkdown(patch_note) + + # 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) + + self._connect_signals() + self._center_window() + self.show() + + # ------------------------------------------------------------------ + # 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._on_connexion) + self.ui.discord_btn.clicked.connect(self._on_discord) + self.ui.intranet_btn.clicked.connect(self._on_intranet) + + def _center_window(self) -> None: + 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()) + + # ------------------------------------------------------------------ + # Button handlers + # ------------------------------------------------------------------ + + def _on_connexion(self) -> None: + pass # à implémenter + + @staticmethod + def _on_discord() -> None: + webbrowser.open(URLS["discord"]) + + def _on_intranet(self) -> None: + webbrowser.open(URLS["intranet"]) + self._glow.start() + + # ------------------------------------------------------------------ + # Mouse events → délégués au WindowDragger + # ------------------------------------------------------------------ + + def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: + self._dragger.mouse_press(event) + super().mousePressEvent(event) + + def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: + self._dragger.mouse_move(event) + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: + self._dragger.mouse_release(event) + super().mouseReleaseEvent(event) + + # ------------------------------------------------------------------ + # Close + # ------------------------------------------------------------------ + + def closeEvent(self, event) -> None: + self.config.save() + super().closeEvent(event)