Move repo from git to gitea
All checks were successful
Check Translations / check-translations (push) Successful in 15s
Code and build check / Check code (push) Successful in 1m21s
Code and build check / Build with uv (push) Successful in 47s

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2025-06-01 15:21:32 +05:00
parent aae1ce9c10
commit abec9bbef8
110 changed files with 545106 additions and 2 deletions

View File

@ -0,0 +1,286 @@
import importlib.util
import os
from portprotonqt.logger import get_logger
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtGui import QIcon, QColor, QFontDatabase, QPixmap, QPainter
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
logger = get_logger(__name__)
# Папка, где располагаются все дополнительные темы
xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
THEMES_DIRS = [
os.path.join(xdg_data_home, "PortProtonQT", "themes"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
]
def list_themes():
"""
Возвращает список доступных тем (названий папок) из каталогов THEMES_DIRS.
"""
themes = []
for themes_dir in THEMES_DIRS:
if os.path.exists(themes_dir):
for entry in os.listdir(themes_dir):
theme_path = os.path.join(themes_dir, entry)
if os.path.isdir(theme_path) and os.path.exists(os.path.join(theme_path, "styles.py")):
themes.append(entry)
return themes
def load_theme_screenshots(theme_name):
"""
Загружает все скриншоты из папки "screenshots", расположенной в папке темы.
Возвращает список кортежей (pixmap, filename).
Если папка отсутствует или пуста, возвращается пустой список.
"""
screenshots = []
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(themes_dir, theme_name)
screenshots_folder = os.path.join(theme_folder, "images", "screenshots")
if os.path.exists(screenshots_folder) and os.path.isdir(screenshots_folder):
for file in os.listdir(screenshots_folder):
screenshot_path = os.path.join(screenshots_folder, file)
if os.path.isfile(screenshot_path):
pixmap = QPixmap(screenshot_path)
if not pixmap.isNull():
screenshots.append((pixmap, file))
return screenshots
def load_theme_fonts(theme_name):
"""
Загружает все шрифты выбранной темы.
:param theme_name: Имя темы.
"""
QFontDatabase.removeAllApplicationFonts()
fonts_folder = None
if theme_name == "standart":
base_dir = os.path.dirname(os.path.abspath(__file__))
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
else:
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(themes_dir, theme_name)
possible_fonts_folder = os.path.join(theme_folder, "fonts")
if os.path.exists(possible_fonts_folder):
fonts_folder = possible_fonts_folder
break
if not fonts_folder or not os.path.exists(fonts_folder):
logger.error(f"Папка fonts не найдена для темы '{theme_name}'")
return
for filename in os.listdir(fonts_folder):
if filename.lower().endswith((".ttf", ".otf")):
font_path = os.path.join(fonts_folder, filename)
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
families = QFontDatabase.applicationFontFamilies(font_id)
logger.info(f"Шрифт {filename} успешно загружен: {families}")
else:
logger.error(f"Ошибка загрузки шрифта: {filename}")
def load_logo():
logo_path = None
base_dir = os.path.dirname(os.path.abspath(__file__))
logo_path = os.path.join(base_dir, "themes", "standart", "images", "theme_logo.svg")
file_extension = os.path.splitext(logo_path)[1].lower()
if file_extension == ".svg":
renderer = QSvgRenderer(logo_path)
if not renderer.isValid():
logger.error(f"Ошибка загрузки SVG логотипа: {logo_path}")
return None
pixmap = QPixmap(128, 128)
pixmap.fill(QColor(0, 0, 0, 0))
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return pixmap
class ThemeWrapper:
"""
Обёртка для кастомной темы с поддержкой метаинформации.
При обращении к атрибуту сначала ищется его наличие в кастомной теме,
если атрибут отсутствует, значение берётся из стандартного модуля стилей.
"""
def __init__(self, custom_theme, metainfo=None):
self.custom_theme = custom_theme
self.metainfo = metainfo or {}
self.screenshots = load_theme_screenshots(self.metainfo.get("name", ""))
def __getattr__(self, name):
if hasattr(self.custom_theme, name):
return getattr(self.custom_theme, name)
import portprotonqt.themes.standart.styles as default_styles
return getattr(default_styles, name)
def load_theme(theme_name):
"""
Динамически загружает модуль стилей выбранной темы и метаинформацию.
Если выбрана стандартная тема, импортируется оригинальный styles.py.
Для кастомных тем возвращается обёртка, которая подставляет недостающие атрибуты.
"""
if theme_name == "standart":
import portprotonqt.themes.standart.styles as default_styles
return default_styles
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(themes_dir, theme_name)
styles_file = os.path.join(theme_folder, "styles.py")
if os.path.exists(styles_file):
spec = importlib.util.spec_from_file_location("theme_styles", styles_file)
if spec is None or spec.loader is None:
continue
custom_theme = importlib.util.module_from_spec(spec)
spec.loader.exec_module(custom_theme)
meta = load_theme_metainfo(theme_name)
wrapper = ThemeWrapper(custom_theme, metainfo=meta)
wrapper.screenshots = load_theme_screenshots(theme_name)
return wrapper
raise FileNotFoundError(f"Файл стилей не найден для темы '{theme_name}'")
class ThemeManager:
"""
Класс для управления темами приложения.
Позволяет получить список доступных тем, загрузить и применить выбранную тему.
"""
def __init__(self):
self.current_theme_name = None
self.current_theme_module = None
def get_available_themes(self):
"""Возвращает список доступных тем."""
return list_themes()
def get_theme_logo(self):
"""Возвращает логотип для текущей или указанной темы."""
return load_logo()
def apply_theme(self, theme_name):
"""
Применяет выбранную тему: загружает модуль стилей, шрифты и логотип.
Если загрузка прошла успешно, сохраняет выбранную тему в конфигурации.
:param theme_name: Имя темы.
:return: Загруженный модуль темы (или обёртка).
"""
theme_module = load_theme(theme_name)
load_theme_fonts(theme_name)
self.current_theme_name = theme_name
self.current_theme_module = theme_module
save_theme_to_config(theme_name)
logger.info(f"Тема '{theme_name}' успешно применена")
return theme_module
def get_icon(self, icon_name, theme_name=None, as_path=False):
"""
Возвращает QIcon из папки icons текущей темы,
а если файл не найден, то из стандартной темы.
Если as_path=True, возвращает путь к иконке вместо QIcon.
"""
icon_path = None
theme_name = theme_name or self.current_theme_name
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
has_extension = any(icon_name.lower().endswith(ext) for ext in supported_extensions)
base_name = icon_name if has_extension else icon_name
# Поиск иконки в папке текущей темы
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(str(themes_dir), str(theme_name))
icons_folder = os.path.join(theme_folder, "images", "icons")
# Если передано имя с расширением, проверяем только этот файл
if has_extension:
candidate = os.path.join(icons_folder, str(base_name))
if os.path.exists(candidate):
icon_path = candidate
break
else:
# Проверяем все поддерживаемые расширения
for ext in supported_extensions:
candidate = os.path.join(icons_folder, str(base_name) + str(ext))
if os.path.exists(candidate):
icon_path = candidate
break
if icon_path:
break
# Если не нашли используем стандартную тему
if not icon_path:
base_dir = os.path.dirname(os.path.abspath(__file__))
standard_icons_folder = os.path.join(base_dir, "themes", "standart", "images", "icons")
# Аналогично проверяем в стандартной теме
if has_extension:
icon_path = os.path.join(standard_icons_folder, base_name)
if not os.path.exists(icon_path):
icon_path = None
else:
for ext in supported_extensions:
candidate = os.path.join(standard_icons_folder, base_name + ext)
if os.path.exists(candidate):
icon_path = candidate
break
# Если иконка всё равно не найдена
if not icon_path or not os.path.exists(icon_path):
logger.error(f"Предупреждение: иконка '{icon_name}' не найдена")
return QIcon() if not as_path else None
if as_path:
return icon_path
return QIcon(icon_path)
def get_theme_image(self, image_name, theme_name=None):
"""
Возвращает путь к изображению из папки текущей темы.
Если не найдено, проверяет стандартную тему.
Принимает название иконки без расширения и находит соответствующий файл
с поддерживаемым расширением (.svg, .png, .jpg и др.).
"""
image_path = None
theme_name = theme_name or self.current_theme_name
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
has_extension = any(image_name.lower().endswith(ext) for ext in supported_extensions)
base_name = image_name if has_extension else image_name
# Check theme-specific images
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(str(themes_dir), str(theme_name))
images_folder = os.path.join(theme_folder, "images")
if has_extension:
candidate = os.path.join(images_folder, str(base_name))
if os.path.exists(candidate):
image_path = candidate
break
else:
for ext in supported_extensions:
candidate = os.path.join(images_folder, str(base_name) + str(ext))
if os.path.exists(candidate):
image_path = candidate
break
if image_path:
break
# Check standard theme
if not image_path:
base_dir = os.path.dirname(os.path.abspath(__file__))
standard_images_folder = os.path.join(base_dir, "themes", "standart", "images")
if has_extension:
image_path = os.path.join(standard_images_folder, base_name)
if not os.path.exists(image_path):
image_path = None
else:
for ext in supported_extensions:
candidate = os.path.join(standard_images_folder, base_name + ext)
if os.path.exists(candidate):
image_path = candidate
break
return image_path