19 Commits
v0.1.5 ... main

Author SHA1 Message Date
4356e653b8 feat: added control hint
All checks were successful
Code check / Check code (push) Successful in 1m11s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-08 20:48:03 +05:00
4fc95511f1 docs(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m11s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-08 18:47:23 +05:00
4d4e14ea52 fix: Prevent fullscreen toggle on 'Select' button press during game launch
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-08 18:45:30 +05:00
c39f5ad83b chore(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m8s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-07 22:58:15 +05:00
f3325ca35f feat(theme-manager): implement singleton and caching for improved theme handling
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-07 22:54:25 +05:00
50645066dd chore(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m16s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-07 22:30:16 +05:00
7945dd8980 fix(input_manager): exclude ASRock LED controller from gamepad detection
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-07 22:28:34 +05:00
59c38f9c57 chore(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m15s
renovate / renovate (push) Successful in 28s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-06 12:44:43 +05:00
a2d5d28884 fix(cache): add cleanup of related cache files on JSON updates
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-06 12:43:22 +05:00
16af4b410a chore(renovate): disable almost python-version update
All checks were successful
Code check / Check code (push) Successful in 1m7s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-03 19:17:13 +05:00
e8e42b5a86 chore(renovate): disable python-version update
All checks were successful
Code check / Check code (push) Successful in 1m7s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-03 19:08:58 +05:00
d16e2cdf43 chore(renovate): dont update github-runners
All checks were successful
Code check / Check code (push) Successful in 1m44s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-02 22:56:03 +05:00
Renovate Bot
b60fd0d593 chore(deps): pin dependencies
All checks were successful
Code check / Check code (pull_request) Successful in 2m16s
Code check / Check code (push) Successful in 1m36s
2025-09-02 17:31:21 +00:00
d93f23fe8c chore(renovate): added GITHUB_TOKEN
All checks were successful
Code check / Check code (push) Successful in 1m15s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-02 22:28:10 +05:00
5423ada8f1 fix(theme-security): check standart theme too
All checks were successful
Code check / Check code (push) Successful in 1m12s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-02 17:05:35 +05:00
2547c7c78d chore(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m9s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-02 00:11:35 +05:00
2e93073446 feat(theme-security): add theme safety checks and unify loading via ThemeManager
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-01 23:58:38 +05:00
Gitea Actions
9657ff20d3 chore: update steam apps list 2025-09-01T15:10:40Z 2025-09-01 15:10:40 +00:00
849333c283 feat(dev-scripts): add import and function safety checks to theme pre-commit
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-01 11:42:06 +05:00
41 changed files with 12950 additions and 1047 deletions

View File

@@ -12,7 +12,7 @@ jobs:
name: Build AppImage
runs-on: ubuntu-22.04
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Install required dependencies
run: |
@@ -63,7 +63,7 @@ jobs:
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
- name: Checkout repo
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Copy fedora.spec
run: |
@@ -84,7 +84,7 @@ jobs:
name: Build Arch Package
runs-on: ubuntu-22.04
container:
image: archlinux:base-devel
image: archlinux:base-devel@sha256:8ccc930c28ab4f483ff9bc1b53957150fbe94afe48928ebb0b14a8af41c75023
volumes:
- /usr:/usr-host
- /opt:/opt-host
@@ -124,7 +124,7 @@ jobs:
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Upload Arch package
uses: https://gitea.com/actions/gitea-upload-artifact@v4

View File

@@ -15,10 +15,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set up Python
uses: https://gitea.com/actions/setup-python@v5
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version-file: "pyproject.toml"

View File

@@ -18,7 +18,7 @@ jobs:
fedora: ${{ steps.check.outputs.fedora }}
arch: ${{ steps.check.outputs.arch }}
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
fetch-depth: 0
@@ -63,7 +63,7 @@ jobs:
needs: changes
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Install required dependencies
run: |
@@ -115,7 +115,7 @@ jobs:
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
- name: Checkout repo
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Copy fedora-git.spec
run: |
@@ -138,7 +138,7 @@ jobs:
needs: changes
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
container:
image: archlinux:base-devel
image: archlinux:base-devel@sha256:8ccc930c28ab4f483ff9bc1b53957150fbe94afe48928ebb0b14a8af41c75023
volumes:
- /usr:/usr-host
- /opt:/opt-host
@@ -178,7 +178,7 @@ jobs:
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Upload Arch package
uses: https://gitea.com/actions/gitea-upload-artifact@v4

View File

@@ -20,10 +20,10 @@ jobs:
name: Check code
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set up Node.js
uses: https://gitea.com/actions/setup-node@v4
uses: https://gitea.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20

View File

@@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set up Python
uses: https://gitea.com/actions/setup-python@v5
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version-file: "pyproject.toml"

View File

@@ -8,12 +8,12 @@ on:
jobs:
renovate:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:latest
container: ghcr.io/renovatebot/renovate:latest@sha256:46b57bb9816dec6409e7be57e0e5f7b26d214281044f5aedd3b160be178475e2
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set up Node.js
uses: https://gitea.com/actions/setup-node@v4
uses: https://gitea.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
@@ -35,3 +35,4 @@ jobs:
RENOVATE_CONFIG_FILE: "/tmp/renovate-config/config.js"
LOG_LEVEL: "debug"
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}

View File

@@ -3,6 +3,24 @@
Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
- Кэширование шрифтов в load_theme_fonts для предотвращения повторной загрузки
- Проверка безопасности в theme_manager.py для всех сторонних тем, с проверкой на запрещённые модули и функции (подробности см. в коде theme_manager под полями FORBIDDEN_MODULES и FORBIDDEN_FUNCTIONS)
- Фильтрация ASRock LED контроллера, чтобы предотвратить его обнаружение как геймпада
### Changed
### Fixed
- Исправлена проблема с устаревшими кэш-файлами, вызывающими несоответствия при обновлении JSON
- Исправлено переключение в полноэкранный режим при нажатии кнопки "Select во время запущенной игры
### Contributors
---
## [0.1.5] - 2025-08-31
### Added

View File

@@ -1777,7 +1777,7 @@
},
{
"normalized_name": "supervive",
"status": "Denied"
"status": "Running"
},
{
"normalized_name": "splitgate 2",
@@ -4472,7 +4472,7 @@
"status": "Running"
},
{
"normalized_name": "f1 25",
"normalized_name": "battlefield 6",
"status": "Denied"
},
{
@@ -4482,5 +4482,65 @@
{
"normalized_name": "sword of justice",
"status": "Broken"
},
{
"normalized_name": "blade & soul neo",
"status": "Broken"
},
{
"normalized_name": "the finals (cn)",
"status": "Broken"
},
{
"normalized_name": "tom clancy's rainbow six siege x",
"status": "Denied"
},
{
"normalized_name": "dragonheir silent gods",
"status": "Broken"
},
{
"normalized_name": "the quinfall",
"status": "Running"
},
{
"normalized_name": "redmatch 2",
"status": "Broken"
},
{
"normalized_name": "blade & soul heroes",
"status": "Broken"
},
{
"normalized_name": "blue archive",
"status": "Running"
},
{
"normalized_name": "midnight murder club",
"status": "Broken"
},
{
"normalized_name": "dungeon done",
"status": "Broken"
},
{
"normalized_name": "project wraith",
"status": "Broken"
},
{
"normalized_name": "solo leveling arise",
"status": "Broken"
},
{
"normalized_name": "freedom wars",
"status": "Running"
},
{
"normalized_name": "open fortress",
"status": "Running"
},
{
"normalized_name": "no more room in hell 2",
"status": "Running"
}
]

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,4 +1,48 @@
[
{
"normalized_title": "astroneer",
"slug": "astroneer"
},
{
"normalized_title": "anno 2205",
"slug": "anno-2205"
},
{
"normalized_title": "anno 2070",
"slug": "anno-2070"
},
{
"normalized_title": "kompas 3d v23 / компас 3d v23",
"slug": "kompas-3d-v23-kompas-3d-v23"
},
{
"normalized_title": "ultrakill (early access)",
"slug": "ultrakill-early-access"
},
{
"normalized_title": "vintage story",
"slug": "vintage-story"
},
{
"normalized_title": "disco elysium the finul cut",
"slug": "disco-elysium-the-finul-cut"
},
{
"normalized_title": "warcraft iii reign of chaos",
"slug": "warcraft-iii-reign-of-chaos"
},
{
"normalized_title": "dying light",
"slug": "dying-light"
},
{
"normalized_title": "лихо одноглазое",
"slug": "liho-odnoglazoe"
},
{
"normalized_title": "indika",
"slug": "indika"
},
{
"normalized_title": "no sleep for kaname date from ai the somnium files",
"slug": "no-sleep-for-kaname-date-from-ai-the-somnium-files"
@@ -235,10 +279,6 @@
"normalized_title": "cardlife creative survival",
"slug": "cardlife-creative-survival"
},
{
"normalized_title": "kompas 3d v23 / компас 3d v23",
"slug": "kompas-3d-v23-kompas-3d-v23"
},
{
"normalized_title": "kompas 3d v24 / компас 3d v24 beta",
"slug": "kompas-3d-v24-kompas-3d-v24-beta"

Binary file not shown.

View File

@@ -3,8 +3,9 @@
import sys
from pathlib import Path
import re
import ast
# Запрещенные свойства
# Запрещенные QSS-свойства
FORBIDDEN_PROPERTIES = {
"box-shadow",
"backdrop-filter",
@@ -12,15 +13,55 @@ FORBIDDEN_PROPERTIES = {
"text-shadow",
}
# Запрещенные модули и функции
FORBIDDEN_MODULES = {
"os",
"subprocess",
"shutil",
"sys",
"socket",
"ctypes",
"pathlib",
"glob",
}
FORBIDDEN_FUNCTIONS = {
"exec",
"eval",
"open",
"__import__",
}
def check_qss_files():
has_errors = False
for qss_file in Path("portprotonqt/themes").glob("**/*.py"):
with open(qss_file, "r") as f:
content = f.read()
# Проверка на запрещённые QSS-свойства
for prop in FORBIDDEN_PROPERTIES:
if re.search(rf"{prop}\s*:", content, re.IGNORECASE):
print(f"ERROR: Unknown qss property found '{prop}' on file {qss_file}")
print(f"ERROR: Unknown QSS property found '{prop}' in file {qss_file}")
has_errors = True
# Проверка на опасные импорты и функции
try:
tree = ast.parse(content)
for node in ast.walk(tree):
# Проверка импортов
if isinstance(node, (ast.Import, ast.ImportFrom)):
for name in node.names:
if name.name in FORBIDDEN_MODULES:
print(f"ERROR: Forbidden module '{name.name}' found in file {qss_file}")
has_errors = True
# Проверка вызовов функций
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id in FORBIDDEN_FUNCTIONS:
print(f"ERROR: Forbidden function '{node.func.id}' found in file {qss_file}")
has_errors = True
except SyntaxError as e:
print(f"ERROR: Syntax error in file {qss_file}: {e}")
has_errors = True
return has_errors
if __name__ == "__main__":

View File

@@ -2,8 +2,10 @@ from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstra
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
from collections.abc import Callable
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.logger import get_logger
from portprotonqt.config_utils import read_theme_from_config
from portprotonqt.theme_manager import ThemeManager
logger = get_logger(__name__)
@@ -23,7 +25,8 @@ class SafeOpacityEffect(QGraphicsOpacityEffect):
class GameCardAnimations:
def __init__(self, game_card, theme=None):
self.game_card = game_card
self.theme = theme if theme is not None else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.thickness_anim: QPropertyAnimation | None = None
self.gradient_anim: QPropertyAnimation | None = None
self.scale_anim: QPropertyAnimation | None = None
@@ -232,7 +235,8 @@ class GameCardAnimations:
class DetailPageAnimations:
def __init__(self, main_window, theme=None):
self.main_window = main_window
self.theme = theme if theme is not None else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.animations = main_window._animations if hasattr(main_window, '_animations') else {}
def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable):

View File

@@ -9,10 +9,9 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
from icoextract import IconExtractor, IconExtractorError
from PIL import Image
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
from portprotonqt.localization import _
from portprotonqt.logger import get_logger
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.custom_widgets import AutoSizeButton
from portprotonqt.downloader import Downloader
@@ -22,6 +21,7 @@ if TYPE_CHECKING:
from portprotonqt.main_window import MainWindow
logger = get_logger(__name__)
theme_manager = ThemeManager()
def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
"""
@@ -94,8 +94,7 @@ class GameLaunchDialog(QDialog):
"""Modal dialog to indicate game launch progress, similar to Steam's launch dialog."""
def __init__(self, parent=None, game_name=None, theme=None, target_exe=None):
super().__init__(parent)
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
self.game_name = game_name
self.target_exe = target_exe # Store the target executable name
self.setWindowTitle(_("Launching {0}").format(self.game_name))
@@ -123,7 +122,7 @@ class GameLaunchDialog(QDialog):
layout.addWidget(self.progress_bar)
# Cancel button
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.cancel_button.clicked.connect(self.reject)
layout.addWidget(self.cancel_button, alignment=Qt.AlignmentFlag.AlignCenter)
@@ -173,8 +172,7 @@ class GameLaunchDialog(QDialog):
class FileExplorer(QDialog):
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
super().__init__(parent)
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
self.file_signal = FileSelectedSignal()
self.file_filter = file_filter # Store the file filter
self.directory_only = directory_only # Store the directory_only flag
@@ -272,8 +270,8 @@ class FileExplorer(QDialog):
# Кнопки
self.button_layout = QHBoxLayout()
self.button_layout.setSpacing(10)
self.select_button = AutoSizeButton(_("Select"), icon=self.theme_manager.get_icon("apply"))
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
self.select_button = AutoSizeButton(_("Select"), icon=theme_manager.get_icon("apply"))
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.button_layout.addWidget(self.select_button)
@@ -406,7 +404,7 @@ class FileExplorer(QDialog):
# Добавляем смонтированные диски
for drive in drives:
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
button = AutoSizeButton(drive_name, icon=self.theme_manager.get_icon("mount_point"))
button = AutoSizeButton(drive_name, icon=theme_manager.get_icon("mount_point"))
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
@@ -416,7 +414,7 @@ class FileExplorer(QDialog):
# Добавляем избранные папки
for folder in favorite_folders:
folder_name = os.path.basename(folder) or folder.split('/')[-1] or folder
button = AutoSizeButton(folder_name, icon=self.theme_manager.get_icon("folder"))
button = AutoSizeButton(folder_name, icon=theme_manager.get_icon("folder"))
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
button.clicked.connect(lambda checked, path=folder: self.change_drive(path))
@@ -484,7 +482,7 @@ class FileExplorer(QDialog):
try:
if self.current_path != "/":
item = QListWidgetItem("../")
folder_icon = self.theme_manager.get_icon("folder")
folder_icon = theme_manager.get_icon("folder")
# Ensure the icon is a QIcon
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
folder_icon = QIcon(folder_icon)
@@ -499,7 +497,7 @@ class FileExplorer(QDialog):
# Добавляем директории
for d in sorted(dirs):
item = QListWidgetItem(f"{d}/")
folder_icon = self.theme_manager.get_icon("folder")
folder_icon = theme_manager.get_icon("folder")
# Ensure the icon is a QIcon
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
folder_icon = QIcon(folder_icon)
@@ -590,8 +588,7 @@ class AddGameDialog(QDialog):
def __init__(self, parent=None, theme=None, edit_mode=False, game_name=None, exe_path=None, cover_path=None):
super().__init__(parent)
from portprotonqt.context_menu_manager import CustomLineEdit # Локальный импорт
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
self.edit_mode = edit_mode
self.original_name = game_name
self.last_exe_path = exe_path # Store last selected exe path
@@ -627,7 +624,7 @@ class AddGameDialog(QDialog):
if exe_path:
self.exeEdit.setText(exe_path)
exeBrowseButton = AutoSizeButton(_("Browse..."), icon=self.theme_manager.get_icon("search"))
exeBrowseButton = AutoSizeButton(_("Browse..."), icon=theme_manager.get_icon("search"))
exeBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
exeBrowseButton.clicked.connect(self.browseExe)
exeBrowseButton.setObjectName("exeBrowseButton") # Для поиска кнопки
@@ -649,7 +646,7 @@ class AddGameDialog(QDialog):
if cover_path:
self.coverEdit.setText(cover_path)
coverBrowseButton = AutoSizeButton(_("Browse..."), icon=self.theme_manager.get_icon("search"))
coverBrowseButton = AutoSizeButton(_("Browse..."), icon=theme_manager.get_icon("search"))
coverBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
coverBrowseButton.clicked.connect(self.browseCover)
coverBrowseButton.setObjectName("coverBrowseButton") # Для поиска кнопки
@@ -678,8 +675,8 @@ class AddGameDialog(QDialog):
# Dialog buttons
self.button_layout = QHBoxLayout()
self.button_layout.setSpacing(10)
self.select_button = AutoSizeButton(_("Apply"), icon=self.theme_manager.get_icon("apply"))
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
self.select_button = AutoSizeButton(_("Apply"), icon=theme_manager.get_icon("apply"))
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.button_layout.addWidget(self.select_button)

View File

@@ -2,12 +2,10 @@ from PySide6.QtGui import QPainter, QColor, QDesktopServices
from PySide6.QtCore import Signal, Property, Qt, QUrl
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
from collections.abc import Callable
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.image_utils import load_pixmap_async, round_corners
from portprotonqt.localization import _
from portprotonqt.config_utils import read_favorites, save_favorites, read_display_filter
from portprotonqt.config_utils import read_favorites, save_favorites, read_display_filter, read_theme_from_config
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.config_utils import read_theme_from_config
from portprotonqt.custom_widgets import ClickableLabel
from portprotonqt.portproton_api import PortProtonAPI
from portprotonqt.downloader import Downloader
@@ -56,7 +54,7 @@ class GameCard(QFrame):
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._show_context_menu)
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else default_styles
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.display_filter = read_display_filter()
self.current_theme_name = read_theme_from_config()

View File

@@ -3,7 +3,6 @@ from PySide6.QtGui import QPen, QColor, QPixmap, QPainter, QPainterPath
from PySide6.QtCore import Qt, QFile, QEvent, QByteArray, QEasingCurve, QPropertyAnimation
from PySide6.QtWidgets import QGraphicsItem, QToolButton, QFrame, QLabel, QGraphicsScene, QHBoxLayout, QWidget, QGraphicsView, QVBoxLayout, QSizePolicy
from PySide6.QtWidgets import QSpacerItem, QGraphicsPixmapItem, QDialog, QApplication
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.config_utils import read_theme_from_config
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.downloader import Downloader
@@ -177,7 +176,8 @@ class FullscreenDialog(QDialog):
self.images = images
self.current_index = current_index
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
@@ -378,7 +378,8 @@ class ImageCarousel(QGraphicsView):
self.images = images # Список кортежей: (QPixmap, caption)
self.image_items = []
self._animation = None
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
self.max_height = 300 # Default height for images
self.init_ui()
self.create_arrows()

View File

@@ -1061,6 +1061,10 @@ class InputManager(QObject):
try:
devices = [InputDevice(path) for path in list_devices()]
for device in devices:
# Skip ASRock LED controller (vendor ID: 26ce, product ID: 01a2)
if device.info.vendor == 0x26ce and device.info.product == 0x01a2:
logger.debug(f"Skipping ASRock LED controller: {device.name}")
continue
caps = device.capabilities()
if ecodes.EV_KEY in caps or ecodes.EV_ABS in caps:
return device
@@ -1081,6 +1085,8 @@ class InputManager(QObject):
now = time.time()
if event.type == ecodes.EV_KEY and event.value == 1:
if event.code in BUTTONS['menu'] and not self._is_gamescope_session:
# Проверяем, не запущена ли игра
if not getattr(self._parent, '_gameLaunched', False):
self.toggle_fullscreen.emit(not self._is_fullscreen)
else:
self.button_pressed.emit(event.code)

View File

@@ -4,10 +4,9 @@ import shutil
import signal
import subprocess
import sys
import portprotonqt.themes.standart.styles as default_styles
import psutil
from portprotonqt.logger import get_logger
from portprotonqt.dialogs import AddGameDialog, FileExplorer
from portprotonqt.game_card import GameCard
from portprotonqt.animations import DetailPageAnimations
@@ -31,7 +30,6 @@ from portprotonqt.config_utils import (
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config
)
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
from portprotonqt.logger import get_logger
from portprotonqt.howlongtobeat_api import HowLongToBeat
from portprotonqt.downloader import Downloader
from portprotonqt.tray_manager import TrayManager
@@ -60,15 +58,7 @@ class MainWindow(QMainWindow):
self.is_exiting = False
selected_theme = read_theme_from_config()
self.current_theme_name = selected_theme
try:
self.theme = self.theme_manager.apply_theme(selected_theme)
except FileNotFoundError:
logger.warning(f"Тема '{selected_theme}' не найдена, применяется стандартная тема 'standart'")
self.theme = self.theme_manager.apply_theme("standart")
self.current_theme_name = "standart"
save_theme_to_config("standart")
if not self.theme:
self.theme = default_styles
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
self.card_width = read_card_size()
self.setWindowTitle(app_name)
@@ -209,9 +199,15 @@ class MainWindow(QMainWindow):
self.createPortProtonTab() # вкладка 4
self.createThemeTab() # вкладка 5
self.controlHintsWidget = self.createControlHintsWidget()
mainLayout.addWidget(self.controlHintsWidget)
self.restore_state()
self.input_manager = InputManager(self)
# Connect InputManager gamepad connection/disconnection signals
self.input_manager.button_pressed.connect(self.updateControlHints)
self.input_manager.dpad_moved.connect(self.updateControlHints)
self.detail_animations = DetailPageAnimations(self, self.theme)
QTimer.singleShot(0, self.loadGames)
@@ -223,6 +219,98 @@ class MainWindow(QMainWindow):
self.resize(width, height)
else:
self.showNormal()
def createControlHintsWidget(self) -> QWidget:
from portprotonqt.localization import _
"""Creates a widget displaying control hints for gamepad and keyboard."""
logger.debug("Creating control hints widget")
hintsWidget = QWidget()
hintsWidget.setStyleSheet(self.theme.STATUS_BAR_STYLE)
hintsLayout = QHBoxLayout(hintsWidget)
gamepad_hints = [
("button_a", _("Select")),
("button_b", _("Back")),
("button_x", _("Add Game")),
("button_y", _("Previous Directory")),
("button_lb", _("Previous Tab")),
("button_rb", _("Next Tab")),
("button_start", _("Context Menu")),
("button_select", _("Toggle Fullscreen")),
("button_guide", _("System Overlay")),
("button_rt", _("Increase Size")),
("button_lt", _("Decrease Size")),
]
keyboard_hints = [
("key_enter", _("Select")),
("key_esc", _("Back")),
("key_e", _("Add Game")),
("key_left", _("Previous Tab")),
("key_right", _("Next Tab")),
("key_context", _("Context Menu")),
("key_insert", _("System Overlay")),
("key_f11", _("Toggle Fullscreen")),
]
self.hintsLabels = []
def makeHint(icon_name: str, action: str, visible: bool):
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(10)
# иконка
icon_label = QLabel()
icon_label.setFixedSize(48, 48)
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
pixmap = QPixmap()
icon_path = self.theme_manager.get_theme_image(icon_name, self.current_theme_name)
if not icon_path:
icon_path = self.theme_manager.get_theme_image("placeholder", self.current_theme_name)
if icon_path:
pixmap.load(str(icon_path))
if not pixmap.isNull():
icon_label.setPixmap(pixmap.scaled(48, 48, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
layout.addWidget(icon_label)
# текст
text_label = QLabel(action)
text_label.setStyleSheet(self.theme.LAST_LAUNCH_VALUE_STYLE)
text_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
layout.addWidget(text_label)
container.setVisible(visible)
self.hintsLabels.append((container, icon_name))
hintsLayout.addWidget(container)
for icon, action in gamepad_hints:
makeHint(icon, action, visible=False)
for icon, action in keyboard_hints:
makeHint(icon, action, visible=True)
hintsLayout.addStretch() # чтобы всё разъехалось на всю ширину
return hintsWidget
def updateControlHints(self, *args) -> None:
"""Updates control hints based on gamepad connection status."""
is_gamepad_connected = self.input_manager.gamepad is not None
logger.debug("Updating control hints, gamepad connected: %s", is_gamepad_connected)
for container, icon_name in self.hintsLabels:
if icon_name.startswith("button_"): # это геймпад
container.setVisible(is_gamepad_connected)
else: # это клавиатура
container.setVisible(not is_gamepad_connected)
@Slot(list)
def on_games_loaded(self, games: list[tuple]):
self.games = games

View File

@@ -22,6 +22,7 @@ import websocket
import requests
import random
import base64
import glob
downloader = Downloader()
logger = get_logger(__name__)
@@ -265,10 +266,20 @@ def get_exiftool_data(game_exe):
logger.error(f"An unexpected error occurred in get_exiftool_data for {game_exe}: {e}")
return {}
def delete_cached_app_files(cache_dir: str, pattern: str):
"""Deletes cached files matching the given pattern in the cache directory."""
try:
for file_path in glob.glob(os.path.join(cache_dir, pattern)):
os.remove(file_path)
logger.info(f"Deleted cached file: {file_path}")
except Exception as e:
logger.error(f"Failed to delete cached files matching {pattern}: {e}")
def load_steam_apps_async(callback: Callable[[list], None]):
"""
Asynchronously loads the list of Steam applications, using cache if available.
Calls the callback with the list of apps.
Deletes cached app detail files when downloading a new steam_apps.json.
"""
cache_dir = get_cache_dir()
cache_tar = os.path.join(cache_dir, "games_appid.tar.xz")
@@ -295,6 +306,8 @@ def load_steam_apps_async(callback: Callable[[list], None]):
if os.path.exists(cache_tar):
os.remove(cache_tar)
logger.info("Archive %s deleted after extraction", cache_tar)
# Delete all cached app detail files (steam_app_*.json)
delete_cached_app_files(cache_dir, "steam_app_*.json")
steam_apps = data if isinstance(data, list) else []
logger.info("Loaded %d apps from archive", len(steam_apps))
callback(steam_apps)
@@ -325,11 +338,15 @@ def load_steam_apps_async(callback: Callable[[list], None]):
app_list_url = (
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz"
)
# Delete cached app detail files before re-downloading
delete_cached_app_files(cache_dir, "steam_app_*.json")
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
else:
app_list_url = (
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz"
)
# Delete cached app detail files before downloading
delete_cached_app_files(cache_dir, "steam_app_*.json")
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
def build_index(steam_apps):
@@ -427,6 +444,7 @@ def load_weanticheatyet_data_async(callback: Callable[[list], None]):
"""
Asynchronously loads the list of WeAntiCheatYet data, using cache if available.
Calls the callback with the list of anti-cheat data.
Deletes cached anti-cheat files when downloading a new anticheat_games.json.
"""
cache_dir = get_cache_dir()
cache_tar = os.path.join(cache_dir, "anticheat_games.tar.xz")
@@ -483,11 +501,15 @@ def load_weanticheatyet_data_async(callback: Callable[[list], None]):
app_list_url = (
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz"
)
# Delete cached anti-cheat files before re-downloading
delete_cached_app_files(cache_dir, "anticheat_*.json") # Adjust pattern if app-specific files are added
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
else:
app_list_url = (
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz"
)
# Delete cached anti-cheat files before downloading
delete_cached_app_files(cache_dir, "anticheat_*.json") # Adjust pattern if app-specific files are added
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
def build_weanticheatyet_index(anti_cheat_data):

View File

@@ -1,9 +1,9 @@
import importlib.util
import os
import ast
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__)
@@ -14,6 +14,59 @@ THEMES_DIRS = [
os.path.join(xdg_data_home, "PortProtonQt", "themes"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
]
_loaded_theme = None
# Запрещенные модули и функции
FORBIDDEN_MODULES = {
"os",
"subprocess",
"shutil",
"sys",
"socket",
"ctypes",
"pathlib",
"glob",
}
FORBIDDEN_FUNCTIONS = {
"exec",
"eval",
"open",
"__import__",
}
def check_theme_safety(theme_file: str) -> bool:
"""
Проверяет файл темы на наличие запрещённых модулей и функций.
Возвращает True, если файл безопасен, иначе False.
"""
has_errors = False
try:
with open(theme_file) as f:
content = f.read()
# Проверка на опасные импорты и функции
try:
tree = ast.parse(content)
for node in ast.walk(tree):
# Проверка импортов
if isinstance(node, ast.Import | ast.ImportFrom):
for name in node.names:
if name.name in FORBIDDEN_MODULES:
logger.error(f"Forbidden module '{name.name}' found in file {theme_file}")
has_errors = True
# Проверка вызовов функций
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id in FORBIDDEN_FUNCTIONS:
logger.error(f"Forbidden function '{node.func.id}' found in file {theme_file}")
has_errors = True
except SyntaxError as e:
logger.error(f"Syntax error in file {theme_file}: {e}")
has_errors = True
except Exception as e:
logger.error(f"Failed to check theme safety for {theme_file}: {e}")
has_errors = True
return not has_errors
def list_themes():
"""
@@ -49,9 +102,13 @@ def load_theme_screenshots(theme_name):
def load_theme_fonts(theme_name):
"""
Загружает все шрифты выбранной темы.
:param theme_name: Имя темы.
Загружает все шрифты выбранной темы, если они ещё не были загружены.
"""
global _loaded_theme
if _loaded_theme == theme_name:
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
return
QFontDatabase.removeAllApplicationFonts()
fonts_folder = None
if theme_name == "standart":
@@ -66,7 +123,7 @@ def load_theme_fonts(theme_name):
break
if not fonts_folder or not os.path.exists(fonts_folder):
logger.error(f"Папка fonts не найдена для темы '{theme_name}'")
logger.error(f"Fonts folder not found for theme '{theme_name}'")
return
for filename in os.listdir(fonts_folder):
@@ -75,13 +132,17 @@ def load_theme_fonts(theme_name):
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
families = QFontDatabase.applicationFontFamilies(font_id)
logger.info(f"Шрифт {filename} успешно загружен: {families}")
logger.info(f"Font {filename} successfully loaded: {families}")
else:
logger.error(f"Ошибка загрузки шрифта: {filename}")
logger.error(f"Error loading font: {filename}")
_loaded_theme = theme_name
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")
@@ -90,7 +151,7 @@ def load_logo():
if file_extension == ".svg":
renderer = QSvgRenderer(logo_path)
if not renderer.isValid():
logger.error(f"Ошибка загрузки SVG логотипа: {logo_path}")
logger.error(f"Error loading SVG logo: {logo_path}")
return None
pixmap = QPixmap(128, 128)
pixmap.fill(QColor(0, 0, 0, 0))
@@ -109,69 +170,87 @@ class ThemeWrapper:
self.custom_theme = custom_theme
self.metainfo = metainfo or {}
self.screenshots = load_theme_screenshots(self.metainfo.get("name", ""))
self._default_theme = None # Lazy-loaded default theme
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)
if self._default_theme is None:
self._default_theme = load_theme("standart") # Dynamically load standard theme
return getattr(self._default_theme, 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):
# Проверяем безопасность темы перед загрузкой
if not check_theme_safety(styles_file):
logger.error(f"Theme '{theme_name}' is unsafe, falling back to 'standart'")
raise FileNotFoundError(f"Theme '{theme_name}' contains forbidden modules or functions")
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)
if theme_name == "standart":
return 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}'")
raise FileNotFoundError(f"Styles file not found for theme '{theme_name}'")
class ThemeManager:
"""
Класс для управления темами приложения.
Позволяет получить список доступных тем, загрузить и применить выбранную тему.
Реализует паттерн Singleton для единого экземпляра.
"""
def __init__(self):
self.current_theme_name = None
self.current_theme_module = None
_instance = None
def get_available_themes(self):
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.current_theme_name = None
cls._instance.current_theme_module = None
return cls._instance
def get_available_themes(self) -> list:
"""Возвращает список доступных тем."""
return list_themes()
def get_theme_logo(self):
"""Возвращает логотип для текущей или указанной темы."""
"""Возвращает логотип текущей темы."""
return load_logo()
def apply_theme(self, theme_name):
def apply_theme(self, theme_name: str):
"""
Применяет выбранную тему: загружает модуль стилей, шрифты и логотип.
Если загрузка прошла успешно, сохраняет выбранную тему в конфигурации.
:param theme_name: Имя темы.
:return: Загруженный модуль темы (или обёртка).
Применяет указанную тему, если она ещё не применена.
Возвращает модуль темы или обёртку.
"""
if self.current_theme_name == theme_name and self.current_theme_module is not None:
logger.debug(f"Theme '{theme_name}' is already applied, skipping")
return self.current_theme_module
try:
theme_module = load_theme(theme_name)
except FileNotFoundError:
logger.warning(f"Theme '{theme_name}' not found or unsafe, applying standard theme 'standart'")
theme_module = load_theme("standart")
theme_name = "standart"
save_theme_to_config("standart")
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}' успешно применена")
logger.info(f"Theme '{theme_name}' successfully applied")
return theme_module
def get_icon(self, icon_name, theme_name=None, as_path=False):
@@ -226,7 +305,7 @@ class ThemeManager:
# Если иконка всё равно не найдена
if not icon_path or not os.path.exists(icon_path):
logger.error(f"Предупреждение: иконка '{icon_name}' не найдена")
logger.error(f"Warning: icon '{icon_name}' not found")
return QIcon() if not as_path else None
if as_path:

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -9,7 +9,6 @@ from PySide6.QtGui import QIcon, QAction
from PySide6.QtCore import QTimer
from portprotonqt.logger import get_logger
from portprotonqt.theme_manager import ThemeManager
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.localization import _
from portprotonqt.config_utils import read_favorites, read_theme_from_config, save_theme_to_config
from portprotonqt.dialogs import GameLaunchDialog
@@ -31,15 +30,7 @@ class TrayManager:
self.theme_manager = ThemeManager()
selected_theme = read_theme_from_config()
self.current_theme_name = selected_theme
try:
self.theme = self.theme_manager.apply_theme(selected_theme)
except FileNotFoundError:
logger.warning(f"Тема '{selected_theme}' не найдена, применяется стандартная тема 'standart'")
self.theme = self.theme_manager.apply_theme("standart")
self.current_theme_name = "standart"
save_theme_to_config("standart")
if not self.theme:
self.theme = default_styles
self.main_window = main_window
self.tray_icon = QSystemTrayIcon(self.main_window)

View File

@@ -5,21 +5,25 @@
"lockFileMaintenance": {
"enabled": true
},
"pre-commit": {
"enabled": true
},
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
},
{
"matchDatasources": ["python-version"],
"enabled": false
"matchFileNames": [".gitea/workflows/build.yml"],
"enabled": false,
"description": "Disabled because download-artifact@v4 is not working"
},
{
"matchFileNames": [".python-version"],
"enabled": false
"enabled": false,
},
{
"matchManagers": ["github-actions", "pre-commit", "poetry"],
"matchManagers": ["poetry", "pyenv"],
"enabled": false
},
{
@@ -29,9 +33,14 @@
"groupName": "Python dependencies"
},
{
"matchPackageNames": ["numpy", "setuptools"],
"matchPackageNames": ["numpy", "setuptools", "python"],
"enabled": false,
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
}
},
{
"matchDatasources": ["github-runners", "python-version"],
"enabled": false,
"description": "Prevent Renovate from updating runs-on to unsupported ubuntu-24.04"
},
]
}