Refacto en sous fichiers pour la maintenabilité

This commit is contained in:
2026-03-13 13:55:45 +01:00
parent e1b32688b4
commit d0ede2acd5
7 changed files with 313 additions and 321 deletions

18
src/constants.py Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

52
src/main.py Normal file
View File

@@ -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())

View File

@@ -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())

101
src/ui/main_window.py Normal file
View File

@@ -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)