feat: added sound effects support to themes
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@ -9,12 +9,13 @@
|
|||||||
- [Metadata](#metadata)
|
- [Metadata](#metadata)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [Fonts and Icons](#fonts-and-icons)
|
- [Fonts and Icons](#fonts-and-icons)
|
||||||
|
- [Sound Effects](#sound-effects)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 Overview
|
## 📖 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`.
|
- `~/.local/share/PortProtonQT/themes`.
|
||||||
|
|
||||||
@ -34,6 +35,12 @@ Create a `styles.py` in the theme root. It should define variables or functions
|
|||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```python
|
```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):
|
def custom_button_style(color1, color2):
|
||||||
return f"""
|
return f"""
|
||||||
QPushButton {{
|
QPushButton {{
|
||||||
@ -69,3 +76,19 @@ Folder: `images/screenshots/` — place UI screenshots there.
|
|||||||
- Icons: `images/icons/*.svg/.png`
|
- 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
@ -9,12 +9,13 @@
|
|||||||
- [Метаинформация](#метаинформация)
|
- [Метаинформация](#метаинформация)
|
||||||
- [Скриншоты](#скриншоты)
|
- [Скриншоты](#скриншоты)
|
||||||
- [Шрифты и иконки](#шрифты-и-иконки)
|
- [Шрифты и иконки](#шрифты-и-иконки)
|
||||||
|
- [Звуковые эффекты](#звуковые-эффекты)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 Обзор
|
## 📖 Обзор
|
||||||
|
|
||||||
Темы в `PortProtonQT` позволяют изменить внешний вид интерфейса. Все темы хранятся в папке:
|
Темы в `PortProtonQT` позволяют изменить внешний вид интерфейса и звуковое оформление. Все темы хранятся в папке:
|
||||||
|
|
||||||
- `~/.local/share/PortProtonQT/themes`.
|
- `~/.local/share/PortProtonQT/themes`.
|
||||||
|
|
||||||
@ -32,8 +33,14 @@ mkdir -p ~/.local/share/PortProtonQT/themes/my_custom_theme
|
|||||||
|
|
||||||
Создайте `styles.py` в корне темы. В нём определите переменные и/или функции, возвращающие CSS-оформление.
|
Создайте `styles.py` в корне темы. В нём определите переменные и/или функции, возвращающие CSS-оформление.
|
||||||
|
|
||||||
**Пример функции:**
|
**Пример:**
|
||||||
```python
|
```python
|
||||||
|
# Карта звуковых эффектов
|
||||||
|
SOUNDS = {
|
||||||
|
"app_start": "app_start.wav", # Запуск приложения
|
||||||
|
"app_exit": "app_exit.wav", # Закрытие приложения
|
||||||
|
}
|
||||||
|
|
||||||
def custom_button_style(color1, color2):
|
def custom_button_style(color1, color2):
|
||||||
return f"""
|
return f"""
|
||||||
QPushButton {{
|
QPushButton {{
|
||||||
@ -69,3 +76,19 @@ description = Описание вашей темы.
|
|||||||
- Иконки: `images/icons/*.svg/.png`
|
- Иконки: `images/icons/*.svg/.png`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔊 Звуковые эффекты (опционально)
|
||||||
|
|
||||||
|
Папка: `sounds/` — звуковые эффекты интерфейса.
|
||||||
|
|
||||||
|
Поддерживаемые форматы:
|
||||||
|
- `.wav` - Wave аудио файлы
|
||||||
|
|
||||||
|
Доступные звуковые события:
|
||||||
|
- Звуки интерфейса:
|
||||||
|
- `app_start.wav` - Запуск приложения
|
||||||
|
- `app_exit.wav` - Закрытие приложения
|
||||||
|
|
||||||
|
Если звуковой файл отсутствует в пользовательской теме, будет использован звук из стандартной темы.
|
||||||
|
|
||||||
|
---
|
||||||
|
@ -220,6 +220,13 @@ class MainWindow(QMainWindow):
|
|||||||
self.resize(width, height)
|
self.resize(width, height)
|
||||||
else:
|
else:
|
||||||
self.showNormal()
|
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)
|
@Slot(list)
|
||||||
def on_games_loaded(self, games: list[tuple]):
|
def on_games_loaded(self, games: list[tuple]):
|
||||||
self.games = games
|
self.games = games
|
||||||
@ -2253,5 +2260,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.checkProcessTimer.deleteLater()
|
self.checkProcessTimer.deleteLater()
|
||||||
self.checkProcessTimer = None
|
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()
|
||||||
|
@ -3,6 +3,8 @@ import os
|
|||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from PySide6.QtSvg import QSvgRenderer
|
from PySide6.QtSvg import QSvgRenderer
|
||||||
from PySide6.QtGui import QIcon, QColor, QFontDatabase, QPixmap, QPainter
|
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
|
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):
|
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
|
return QIcon() if not as_path else None
|
||||||
|
|
||||||
if as_path:
|
if as_path:
|
||||||
@ -284,3 +286,67 @@ class ThemeManager:
|
|||||||
break
|
break
|
||||||
|
|
||||||
return image_path
|
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
|
||||||
|
@ -8,6 +8,12 @@ current_theme_name = read_theme_from_config()
|
|||||||
favoriteLabelSize = 48, 48
|
favoriteLabelSize = 48, 48
|
||||||
pixmapsScaledSize = 60, 60
|
pixmapsScaledSize = 60, 60
|
||||||
|
|
||||||
|
# ЗВУКОВЫЕ ЭФФЕКТЫ
|
||||||
|
SOUNDS = {
|
||||||
|
"app_start": "app_start.wav", # Запуск приложения
|
||||||
|
"app_exit": "app_exit.wav", # Закрытие приложения
|
||||||
|
}
|
||||||
|
|
||||||
GAME_CARD_ANIMATION = {
|
GAME_CARD_ANIMATION = {
|
||||||
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
|
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
|
||||||
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
|
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
|
||||||
|
@ -8,6 +8,12 @@ current_theme_name = read_theme_from_config()
|
|||||||
favoriteLabelSize = 48, 48
|
favoriteLabelSize = 48, 48
|
||||||
pixmapsScaledSize = 60, 60
|
pixmapsScaledSize = 60, 60
|
||||||
|
|
||||||
|
# ЗВУКОВЫЕ ЭФФЕКТЫ
|
||||||
|
SOUNDS = {
|
||||||
|
"app_start": "app_start.wav", # Запуск приложения
|
||||||
|
"app_exit": "app_exit.wav", # Закрытие приложения
|
||||||
|
}
|
||||||
|
|
||||||
# VARS
|
# VARS
|
||||||
font_family = "Play"
|
font_family = "Play"
|
||||||
font_size_a = "16px"
|
font_size_a = "16px"
|
||||||
|
Reference in New Issue
Block a user