From 889ba4af2b7fe4c358e427ad616f761c89398ee7 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 16 Jul 2025 11:10:31 +0500 Subject: [PATCH] feat: added sound effects support to themes Signed-off-by: Boris Yumankulov --- documentation/theme_guide/README.md | 25 ++++++- documentation/theme_guide/README.ru.md | 27 +++++++- portprotonqt/main_window.py | 17 ++++- portprotonqt/theme_manager.py | 68 +++++++++++++++++++- portprotonqt/themes/standart-light/styles.py | 6 ++ portprotonqt/themes/standart/styles.py | 6 ++ 6 files changed, 143 insertions(+), 6 deletions(-) diff --git a/documentation/theme_guide/README.md b/documentation/theme_guide/README.md index d29dca5..e8960b2 100644 --- a/documentation/theme_guide/README.md +++ b/documentation/theme_guide/README.md @@ -9,12 +9,13 @@ - [Metadata](#metadata) - [Screenshots](#screenshots) - [Fonts and Icons](#fonts-and-icons) +- [Sound Effects](#sound-effects) --- ## πŸ“– Overview -Themes in `PortProtonQT` allow customizing the UI appearance. Themes are stored under: +Themes in `PortProtonQT` allow customizing the UI appearance and sounds. Themes are stored under: - `~/.local/share/PortProtonQT/themes`. @@ -34,6 +35,12 @@ Create a `styles.py` in the theme root. It should define variables or functions **Example:** ```python +# Sound effects mapping +SOUNDS = { + "app_start": "app_start.wav", # Application startup + "app_exit": "app_exit.wav", # Application exit +} + def custom_button_style(color1, color2): return f""" QPushButton {{ @@ -69,3 +76,19 @@ Folder: `images/screenshots/` β€” place UI screenshots there. - Icons: `images/icons/*.svg/.png` --- + +## πŸ”Š Sound Effects (optional) + +Folder: `sounds/` β€” place interface sound effects here. + +Supported formats: +- `.wav` - Wave audio files + +Available sound events: +- Interface sounds: + - `app_start.wav` - Application startup + - `app_exit.wav` - Application exit + +If a sound file is missing in a custom theme, the default sound from the standard theme will be used. + +--- diff --git a/documentation/theme_guide/README.ru.md b/documentation/theme_guide/README.ru.md index 1b59e61..7f0a2ff 100644 --- a/documentation/theme_guide/README.ru.md +++ b/documentation/theme_guide/README.ru.md @@ -9,12 +9,13 @@ - [ΠœΠ΅Ρ‚Π°ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡ](#мСтаинформация) - [Π‘ΠΊΡ€ΠΈΠ½ΡˆΠΎΡ‚Ρ‹](#ΡΠΊΡ€ΠΈΠ½ΡˆΠΎΡ‚Ρ‹) - [Π¨Ρ€ΠΈΡ„Ρ‚Ρ‹ ΠΈ ΠΈΠΊΠΎΠ½ΠΊΠΈ](#ΡˆΡ€ΠΈΡ„Ρ‚Ρ‹-ΠΈ-ΠΈΠΊΠΎΠ½ΠΊΠΈ) +- [Π—Π²ΡƒΠΊΠΎΠ²Ρ‹Π΅ эффСкты](#Π·Π²ΡƒΠΊΠΎΠ²Ρ‹Π΅-эффСкты) --- ## πŸ“– ΠžΠ±Π·ΠΎΡ€ -Π’Π΅ΠΌΡ‹ Π² `PortProtonQT` ΠΏΠΎΠ·Π²ΠΎΠ»ΡΡŽΡ‚ ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ внСшний Π²ΠΈΠ΄ интСрфСйса. ВсС Ρ‚Π΅ΠΌΡ‹ хранятся Π² ΠΏΠ°ΠΏΠΊΠ΅: +Π’Π΅ΠΌΡ‹ Π² `PortProtonQT` ΠΏΠΎΠ·Π²ΠΎΠ»ΡΡŽΡ‚ ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ внСшний Π²ΠΈΠ΄ интСрфСйса ΠΈ Π·Π²ΡƒΠΊΠΎΠ²ΠΎΠ΅ ΠΎΡ„ΠΎΡ€ΠΌΠ»Π΅Π½ΠΈΠ΅. ВсС Ρ‚Π΅ΠΌΡ‹ хранятся Π² ΠΏΠ°ΠΏΠΊΠ΅: - `~/.local/share/PortProtonQT/themes`. @@ -32,8 +33,14 @@ mkdir -p ~/.local/share/PortProtonQT/themes/my_custom_theme Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ `styles.py` Π² ΠΊΠΎΡ€Π½Π΅ Ρ‚Π΅ΠΌΡ‹. Π’ Π½Ρ‘ΠΌ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚Π΅ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ ΠΈ/ΠΈΠ»ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ, Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽΡ‰ΠΈΠ΅ CSS-ΠΎΡ„ΠΎΡ€ΠΌΠ»Π΅Π½ΠΈΠ΅. -**ΠŸΡ€ΠΈΠΌΠ΅Ρ€ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ:** +**ΠŸΡ€ΠΈΠΌΠ΅Ρ€:** ```python +# ΠšΠ°Ρ€Ρ‚Π° Π·Π²ΡƒΠΊΠΎΠ²Ρ‹Ρ… эффСктов +SOUNDS = { + "app_start": "app_start.wav", # Запуск прилоТСния + "app_exit": "app_exit.wav", # Π—Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ прилоТСния +} + def custom_button_style(color1, color2): return f""" QPushButton {{ @@ -69,3 +76,19 @@ description = ОписаниС вашСй Ρ‚Π΅ΠΌΡ‹. - Иконки: `images/icons/*.svg/.png` --- + +## πŸ”Š Π—Π²ΡƒΠΊΠΎΠ²Ρ‹Π΅ эффСкты (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ) + +Папка: `sounds/` β€” Π·Π²ΡƒΠΊΠΎΠ²Ρ‹Π΅ эффСкты интСрфСйса. + +ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹: +- `.wav` - Wave Π°ΡƒΠ΄ΠΈΠΎ Ρ„Π°ΠΉΠ»Ρ‹ + +ДоступныС Π·Π²ΡƒΠΊΠΎΠ²Ρ‹Π΅ события: +- Π—Π²ΡƒΠΊΠΈ интСрфСйса: + - `app_start.wav` - Запуск прилоТСния + - `app_exit.wav` - Π—Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ прилоТСния + +Если Π·Π²ΡƒΠΊΠΎΠ²ΠΎΠΉ Ρ„Π°ΠΉΠ» отсутствуСт Π² ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Ρ‚Π΅ΠΌΠ΅, Π±ΡƒΠ΄Π΅Ρ‚ использован Π·Π²ΡƒΠΊ ΠΈΠ· стандартной Ρ‚Π΅ΠΌΡ‹. + +--- diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 9c52662..2f09582 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -220,6 +220,13 @@ class MainWindow(QMainWindow): self.resize(width, height) else: self.showNormal() + + self._startup_sound = self.theme_manager.get_sound(default_styles.SOUNDS["app_start"]) + self._exit_sound = self.theme_manager.get_sound(default_styles.SOUNDS["app_exit"]) + + if self._startup_sound: + self._startup_sound.play() + @Slot(list) def on_games_loaded(self, games: list[tuple]): self.games = games @@ -2253,5 +2260,11 @@ class MainWindow(QMainWindow): self.checkProcessTimer.deleteLater() self.checkProcessTimer = None - QApplication.quit() - event.accept() + # Воспроизводим Π·Π²ΡƒΠΊ закрытия + if self._exit_sound: + self._exit_sound.play() + # НСбольшая Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠ° ΠΏΠ΅Ρ€Π΅Π΄ Π·Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π·Π²ΡƒΠΊ успСл ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Ρ‚ΡŒΡΡ + QTimer.singleShot(100, lambda: event.accept()) + event.ignore() + else: + event.accept() diff --git a/portprotonqt/theme_manager.py b/portprotonqt/theme_manager.py index 428b069..dbd6144 100644 --- a/portprotonqt/theme_manager.py +++ b/portprotonqt/theme_manager.py @@ -3,6 +3,8 @@ import os from portprotonqt.logger import get_logger from PySide6.QtSvg import QSvgRenderer from PySide6.QtGui import QIcon, QColor, QFontDatabase, QPixmap, QPainter +from PySide6.QtCore import QUrl +from PySide6.QtMultimedia import QSoundEffect from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo @@ -226,7 +228,7 @@ class ThemeManager: # Если ΠΈΠΊΠΎΠ½ΠΊΠ° всё Ρ€Π°Π²Π½ΠΎ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π° if not icon_path or not os.path.exists(icon_path): - logger.error(f"ΠŸΡ€Π΅Π΄ΡƒΠΏΡ€Π΅ΠΆΠ΄Π΅Π½ΠΈΠ΅: ΠΈΠΊΠΎΠ½ΠΊΠ° '{icon_name}' Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°") + logger.warning(f"icon '{icon_name}' not found") return QIcon() if not as_path else None if as_path: @@ -284,3 +286,67 @@ class ThemeManager: break return image_path + + def get_sound(self, sound_name, theme_name=None): + """ + Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ QSoundEffect для Π·Π²ΡƒΠΊΠ° ΠΈΠ· ΠΏΠ°ΠΏΠΊΠΈ sounds Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΉ Ρ‚Π΅ΠΌΡ‹. + Если Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½, провСряСт ΡΡ‚Π°Π½Π΄Π°Ρ€Ρ‚Π½ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ. + ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹ .wav + + :param sound_name: Имя Π·Π²ΡƒΠΊΠΎΠ²ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π° (с Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ΠΌ ΠΈΠ»ΠΈ Π±Π΅Π·) + :param theme_name: Имя Ρ‚Π΅ΠΌΡ‹ (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ) + :return: QSoundEffect ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ ΠΈΠ»ΠΈ None Ссли Π·Π²ΡƒΠΊ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ + """ + sound_path = None + theme_name = theme_name or self.current_theme_name + supported_extensions = ['.wav'] + + has_extension = any(sound_name.lower().endswith(ext) for ext in supported_extensions) + base_name = sound_name if has_extension else sound_name + + # Поиск Π·Π²ΡƒΠΊΠ° Π² ΠΏΠ°ΠΏΠΊΠ΅ Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΉ Ρ‚Π΅ΠΌΡ‹ + for themes_dir in THEMES_DIRS: + theme_folder = os.path.join(str(themes_dir), str(theme_name)) + sounds_folder = os.path.join(theme_folder, "sounds") + + # Если ΠΏΠ΅Ρ€Π΅Π΄Π°Π½ΠΎ имя с Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ΠΌ, провСряСм Ρ‚ΠΎΠ»ΡŒΠΊΠΎ этот Ρ„Π°ΠΉΠ» + if has_extension: + candidate = os.path.join(sounds_folder, str(base_name)) + if os.path.exists(candidate): + sound_path = candidate + break + else: + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ всС ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Π΅ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ + for ext in supported_extensions: + candidate = os.path.join(sounds_folder, str(base_name) + str(ext)) + if os.path.exists(candidate): + sound_path = candidate + break + if sound_path: + break + + # Если Π½Π΅ нашли – ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΡΡ‚Π°Π½Π΄Π°Ρ€Ρ‚Π½ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ + if not sound_path: + base_dir = os.path.dirname(os.path.abspath(__file__)) + standard_sounds_folder = os.path.join(base_dir, "themes", "standart", "sounds") + + # Аналогично провСряСм Π² стандартной Ρ‚Π΅ΠΌΠ΅ + if has_extension: + sound_path = os.path.join(standard_sounds_folder, base_name) + if not os.path.exists(sound_path): + sound_path = None + else: + for ext in supported_extensions: + candidate = os.path.join(standard_sounds_folder, base_name + ext) + if os.path.exists(candidate): + sound_path = candidate + break + + # Если Π·Π²ΡƒΠΊ всё Ρ€Π°Π²Π½ΠΎ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ + if not sound_path or not os.path.exists(sound_path): + logger.warning(f"Sound '{sound_name}' not found") + return None + + sound = QSoundEffect() + sound.setSource(QUrl.fromLocalFile(sound_path)) + return sound diff --git a/portprotonqt/themes/standart-light/styles.py b/portprotonqt/themes/standart-light/styles.py index 7d3c378..e6fcb35 100644 --- a/portprotonqt/themes/standart-light/styles.py +++ b/portprotonqt/themes/standart-light/styles.py @@ -8,6 +8,12 @@ current_theme_name = read_theme_from_config() favoriteLabelSize = 48, 48 pixmapsScaledSize = 60, 60 +# Π—Π’Π£ΠšΠžΠ’Π«Π• Π­Π€Π€Π•ΠšΠ’Π« +SOUNDS = { + "app_start": "app_start.wav", # Запуск прилоТСния + "app_exit": "app_exit.wav", # Π—Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ прилоТСния +} + GAME_CARD_ANIMATION = { # Π¨ΠΈΡ€ΠΈΠ½Π° ΠΎΠ±Π²ΠΎΠ΄ΠΊΠΈ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ Π² состоянии покоя (Π±Π΅Π· навСдСния ΠΈΠ»ΠΈ фокуса). # ВлияСт Π½Π° Ρ‚ΠΎΠ»Ρ‰ΠΈΠ½Ρƒ Ρ€Π°ΠΌΠΊΠΈ Π²ΠΎΠΊΡ€ΡƒΠ³ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ, ΠΊΠΎΠ³Π΄Π° ΠΎΠ½Π° Π½Π΅ Π²Ρ‹Π΄Π΅Π»Π΅Π½Π°. diff --git a/portprotonqt/themes/standart/styles.py b/portprotonqt/themes/standart/styles.py index 529e89f..622fe5f 100644 --- a/portprotonqt/themes/standart/styles.py +++ b/portprotonqt/themes/standart/styles.py @@ -8,6 +8,12 @@ current_theme_name = read_theme_from_config() favoriteLabelSize = 48, 48 pixmapsScaledSize = 60, 60 +# Π—Π’Π£ΠšΠžΠ’Π«Π• Π­Π€Π€Π•ΠšΠ’Π« +SOUNDS = { + "app_start": "app_start.wav", # Запуск прилоТСния + "app_exit": "app_exit.wav", # Π—Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ прилоТСния +} + # VARS font_family = "Play" font_size_a = "16px"