Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
4356e653b8
|
|||
4fc95511f1
|
|||
4d4e14ea52
|
|||
c39f5ad83b
|
|||
f3325ca35f
|
|||
50645066dd
|
|||
7945dd8980
|
|||
59c38f9c57
|
|||
a2d5d28884
|
|||
16af4b410a
|
|||
e8e42b5a86
|
|||
d16e2cdf43
|
|||
|
b60fd0d593 | ||
d93f23fe8c
|
|||
5423ada8f1
|
|||
2547c7c78d
|
|||
2e93073446
|
|||
|
9657ff20d3 | ||
849333c283
|
@@ -12,7 +12,7 @@ jobs:
|
|||||||
name: Build AppImage
|
name: Build AppImage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@v4
|
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Install required dependencies
|
- name: Install required dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: https://gitea.com/actions/checkout@v4
|
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Copy fedora.spec
|
- name: Copy fedora.spec
|
||||||
run: |
|
run: |
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
name: Build Arch Package
|
name: Build Arch Package
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: archlinux:base-devel
|
image: archlinux:base-devel@sha256:8ccc930c28ab4f483ff9bc1b53957150fbe94afe48928ebb0b14a8af41c75023
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
@@ -124,7 +124,7 @@ jobs:
|
|||||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@v4
|
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Upload Arch package
|
- name: Upload Arch package
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||||
|
@@ -15,10 +15,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@v4
|
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: https://gitea.com/actions/setup-python@v5
|
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@ jobs:
|
|||||||
fedora: ${{ steps.check.outputs.fedora }}
|
fedora: ${{ steps.check.outputs.fedora }}
|
||||||
arch: ${{ steps.check.outputs.arch }}
|
arch: ${{ steps.check.outputs.arch }}
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@v4
|
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
|
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@v4
|
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Install required dependencies
|
- name: Install required dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: https://gitea.com/actions/checkout@v4
|
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Copy fedora-git.spec
|
- name: Copy fedora-git.spec
|
||||||
run: |
|
run: |
|
||||||
@@ -138,7 +138,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
container:
|
container:
|
||||||
image: archlinux:base-devel
|
image: archlinux:base-devel@sha256:8ccc930c28ab4f483ff9bc1b53957150fbe94afe48928ebb0b14a8af41c75023
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@v4
|
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Upload Arch package
|
- name: Upload Arch package
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||||
|
@@ -20,10 +20,10 @@ jobs:
|
|||||||
name: Check code
|
name: Check code
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@v4
|
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: https://gitea.com/actions/setup-node@v4
|
uses: https://gitea.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
@@ -11,10 +11,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@v4
|
uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: https://gitea.com/actions/setup-python@v5
|
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
|
|
||||||
|
@@ -8,12 +8,12 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
renovate:
|
renovate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ghcr.io/renovatebot/renovate:latest
|
container: ghcr.io/renovatebot/renovate:latest@sha256:46b57bb9816dec6409e7be57e0e5f7b26d214281044f5aedd3b160be178475e2
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@v4
|
- uses: https://gitea.com/actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: https://gitea.com/actions/setup-node@v4
|
uses: https://gitea.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
@@ -35,3 +35,4 @@ jobs:
|
|||||||
RENOVATE_CONFIG_FILE: "/tmp/renovate-config/config.js"
|
RENOVATE_CONFIG_FILE: "/tmp/renovate-config/config.js"
|
||||||
LOG_LEVEL: "debug"
|
LOG_LEVEL: "debug"
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
|
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
|
||||||
|
18
CHANGELOG.md
@@ -3,6 +3,24 @@
|
|||||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
Формат основан на [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
|
## [0.1.5] - 2025-08-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@@ -1777,7 +1777,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "supervive",
|
"normalized_name": "supervive",
|
||||||
"status": "Denied"
|
"status": "Running"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "splitgate 2",
|
"normalized_name": "splitgate 2",
|
||||||
@@ -4472,7 +4472,7 @@
|
|||||||
"status": "Running"
|
"status": "Running"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "f1 25",
|
"normalized_name": "battlefield 6",
|
||||||
"status": "Denied"
|
"status": "Denied"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4482,5 +4482,65 @@
|
|||||||
{
|
{
|
||||||
"normalized_name": "sword of justice",
|
"normalized_name": "sword of justice",
|
||||||
"status": "Broken"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
13412
data/games_appid.json
@@ -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",
|
"normalized_title": "no sleep for kaname date from ai the somnium files",
|
||||||
"slug": "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",
|
"normalized_title": "cardlife creative survival",
|
||||||
"slug": "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",
|
"normalized_title": "kompas 3d v24 / компас 3d v24 beta",
|
||||||
"slug": "kompas-3d-v24-kompas-3d-v24-beta"
|
"slug": "kompas-3d-v24-kompas-3d-v24-beta"
|
||||||
|
@@ -3,8 +3,9 @@
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
import ast
|
||||||
|
|
||||||
# Запрещенные свойства
|
# Запрещенные QSS-свойства
|
||||||
FORBIDDEN_PROPERTIES = {
|
FORBIDDEN_PROPERTIES = {
|
||||||
"box-shadow",
|
"box-shadow",
|
||||||
"backdrop-filter",
|
"backdrop-filter",
|
||||||
@@ -12,15 +13,55 @@ FORBIDDEN_PROPERTIES = {
|
|||||||
"text-shadow",
|
"text-shadow",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Запрещенные модули и функции
|
||||||
|
FORBIDDEN_MODULES = {
|
||||||
|
"os",
|
||||||
|
"subprocess",
|
||||||
|
"shutil",
|
||||||
|
"sys",
|
||||||
|
"socket",
|
||||||
|
"ctypes",
|
||||||
|
"pathlib",
|
||||||
|
"glob",
|
||||||
|
}
|
||||||
|
FORBIDDEN_FUNCTIONS = {
|
||||||
|
"exec",
|
||||||
|
"eval",
|
||||||
|
"open",
|
||||||
|
"__import__",
|
||||||
|
}
|
||||||
|
|
||||||
def check_qss_files():
|
def check_qss_files():
|
||||||
has_errors = False
|
has_errors = False
|
||||||
for qss_file in Path("portprotonqt/themes").glob("**/*.py"):
|
for qss_file in Path("portprotonqt/themes").glob("**/*.py"):
|
||||||
with open(qss_file, "r") as f:
|
with open(qss_file, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
|
# Проверка на запрещённые QSS-свойства
|
||||||
for prop in FORBIDDEN_PROPERTIES:
|
for prop in FORBIDDEN_PROPERTIES:
|
||||||
if re.search(rf"{prop}\s*:", content, re.IGNORECASE):
|
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
|
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
|
return has_errors
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@@ -2,8 +2,10 @@ from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstra
|
|||||||
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
|
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
|
||||||
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import portprotonqt.themes.standart.styles as default_styles
|
|
||||||
from portprotonqt.logger import get_logger
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -23,7 +25,8 @@ class SafeOpacityEffect(QGraphicsOpacityEffect):
|
|||||||
class GameCardAnimations:
|
class GameCardAnimations:
|
||||||
def __init__(self, game_card, theme=None):
|
def __init__(self, game_card, theme=None):
|
||||||
self.game_card = game_card
|
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.thickness_anim: QPropertyAnimation | None = None
|
||||||
self.gradient_anim: QPropertyAnimation | None = None
|
self.gradient_anim: QPropertyAnimation | None = None
|
||||||
self.scale_anim: QPropertyAnimation | None = None
|
self.scale_anim: QPropertyAnimation | None = None
|
||||||
@@ -232,7 +235,8 @@ class GameCardAnimations:
|
|||||||
class DetailPageAnimations:
|
class DetailPageAnimations:
|
||||||
def __init__(self, main_window, theme=None):
|
def __init__(self, main_window, theme=None):
|
||||||
self.main_window = main_window
|
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 {}
|
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):
|
def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable):
|
||||||
|
@@ -9,10 +9,9 @@ from PySide6.QtWidgets import (
|
|||||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
||||||
from icoextract import IconExtractor, IconExtractorError
|
from icoextract import IconExtractor, IconExtractorError
|
||||||
from PIL import Image
|
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.localization import _
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
import portprotonqt.themes.standart.styles as default_styles
|
|
||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.custom_widgets import AutoSizeButton
|
from portprotonqt.custom_widgets import AutoSizeButton
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
@@ -22,6 +21,7 @@ if TYPE_CHECKING:
|
|||||||
from portprotonqt.main_window import MainWindow
|
from portprotonqt.main_window import MainWindow
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
theme_manager = ThemeManager()
|
||||||
|
|
||||||
def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
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."""
|
"""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):
|
def __init__(self, parent=None, game_name=None, theme=None, target_exe=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.theme = theme if theme else default_styles
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
self.theme_manager = ThemeManager()
|
|
||||||
self.game_name = game_name
|
self.game_name = game_name
|
||||||
self.target_exe = target_exe # Store the target executable name
|
self.target_exe = target_exe # Store the target executable name
|
||||||
self.setWindowTitle(_("Launching {0}").format(self.game_name))
|
self.setWindowTitle(_("Launching {0}").format(self.game_name))
|
||||||
@@ -123,7 +122,7 @@ class GameLaunchDialog(QDialog):
|
|||||||
layout.addWidget(self.progress_bar)
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
# Cancel button
|
# 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.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.cancel_button.clicked.connect(self.reject)
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
layout.addWidget(self.cancel_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
layout.addWidget(self.cancel_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
@@ -173,8 +172,7 @@ class GameLaunchDialog(QDialog):
|
|||||||
class FileExplorer(QDialog):
|
class FileExplorer(QDialog):
|
||||||
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
|
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.theme = theme if theme else default_styles
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
self.theme_manager = ThemeManager()
|
|
||||||
self.file_signal = FileSelectedSignal()
|
self.file_signal = FileSelectedSignal()
|
||||||
self.file_filter = file_filter # Store the file filter
|
self.file_filter = file_filter # Store the file filter
|
||||||
self.directory_only = directory_only # Store the directory_only flag
|
self.directory_only = directory_only # Store the directory_only flag
|
||||||
@@ -272,8 +270,8 @@ class FileExplorer(QDialog):
|
|||||||
# Кнопки
|
# Кнопки
|
||||||
self.button_layout = QHBoxLayout()
|
self.button_layout = QHBoxLayout()
|
||||||
self.button_layout.setSpacing(10)
|
self.button_layout.setSpacing(10)
|
||||||
self.select_button = AutoSizeButton(_("Select"), icon=self.theme_manager.get_icon("apply"))
|
self.select_button = AutoSizeButton(_("Select"), icon=theme_manager.get_icon("apply"))
|
||||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||||
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.button_layout.addWidget(self.select_button)
|
self.button_layout.addWidget(self.select_button)
|
||||||
@@ -406,7 +404,7 @@ class FileExplorer(QDialog):
|
|||||||
# Добавляем смонтированные диски
|
# Добавляем смонтированные диски
|
||||||
for drive in drives:
|
for drive in drives:
|
||||||
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
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.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
|
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
|
||||||
@@ -416,7 +414,7 @@ class FileExplorer(QDialog):
|
|||||||
# Добавляем избранные папки
|
# Добавляем избранные папки
|
||||||
for folder in favorite_folders:
|
for folder in favorite_folders:
|
||||||
folder_name = os.path.basename(folder) or folder.split('/')[-1] or folder
|
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.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
button.clicked.connect(lambda checked, path=folder: self.change_drive(path))
|
button.clicked.connect(lambda checked, path=folder: self.change_drive(path))
|
||||||
@@ -484,7 +482,7 @@ class FileExplorer(QDialog):
|
|||||||
try:
|
try:
|
||||||
if self.current_path != "/":
|
if self.current_path != "/":
|
||||||
item = QListWidgetItem("../")
|
item = QListWidgetItem("../")
|
||||||
folder_icon = self.theme_manager.get_icon("folder")
|
folder_icon = theme_manager.get_icon("folder")
|
||||||
# Ensure the icon is a QIcon
|
# Ensure the icon is a QIcon
|
||||||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
||||||
folder_icon = QIcon(folder_icon)
|
folder_icon = QIcon(folder_icon)
|
||||||
@@ -499,7 +497,7 @@ class FileExplorer(QDialog):
|
|||||||
# Добавляем директории
|
# Добавляем директории
|
||||||
for d in sorted(dirs):
|
for d in sorted(dirs):
|
||||||
item = QListWidgetItem(f"{d}/")
|
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
|
# Ensure the icon is a QIcon
|
||||||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
||||||
folder_icon = QIcon(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):
|
def __init__(self, parent=None, theme=None, edit_mode=False, game_name=None, exe_path=None, cover_path=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
from portprotonqt.context_menu_manager import CustomLineEdit # Локальный импорт
|
from portprotonqt.context_menu_manager import CustomLineEdit # Локальный импорт
|
||||||
self.theme = theme if theme else default_styles
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
self.theme_manager = ThemeManager()
|
|
||||||
self.edit_mode = edit_mode
|
self.edit_mode = edit_mode
|
||||||
self.original_name = game_name
|
self.original_name = game_name
|
||||||
self.last_exe_path = exe_path # Store last selected exe path
|
self.last_exe_path = exe_path # Store last selected exe path
|
||||||
@@ -627,7 +624,7 @@ class AddGameDialog(QDialog):
|
|||||||
if exe_path:
|
if exe_path:
|
||||||
self.exeEdit.setText(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.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
exeBrowseButton.clicked.connect(self.browseExe)
|
exeBrowseButton.clicked.connect(self.browseExe)
|
||||||
exeBrowseButton.setObjectName("exeBrowseButton") # Для поиска кнопки
|
exeBrowseButton.setObjectName("exeBrowseButton") # Для поиска кнопки
|
||||||
@@ -649,7 +646,7 @@ class AddGameDialog(QDialog):
|
|||||||
if cover_path:
|
if cover_path:
|
||||||
self.coverEdit.setText(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.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
coverBrowseButton.clicked.connect(self.browseCover)
|
coverBrowseButton.clicked.connect(self.browseCover)
|
||||||
coverBrowseButton.setObjectName("coverBrowseButton") # Для поиска кнопки
|
coverBrowseButton.setObjectName("coverBrowseButton") # Для поиска кнопки
|
||||||
@@ -678,8 +675,8 @@ class AddGameDialog(QDialog):
|
|||||||
# Dialog buttons
|
# Dialog buttons
|
||||||
self.button_layout = QHBoxLayout()
|
self.button_layout = QHBoxLayout()
|
||||||
self.button_layout.setSpacing(10)
|
self.button_layout.setSpacing(10)
|
||||||
self.select_button = AutoSizeButton(_("Apply"), icon=self.theme_manager.get_icon("apply"))
|
self.select_button = AutoSizeButton(_("Apply"), icon=theme_manager.get_icon("apply"))
|
||||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||||
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.button_layout.addWidget(self.select_button)
|
self.button_layout.addWidget(self.select_button)
|
||||||
|
@@ -2,12 +2,10 @@ from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
|||||||
from PySide6.QtCore import Signal, Property, Qt, QUrl
|
from PySide6.QtCore import Signal, Property, Qt, QUrl
|
||||||
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
||||||
from collections.abc import Callable
|
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.image_utils import load_pixmap_async, round_corners
|
||||||
from portprotonqt.localization import _
|
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.theme_manager import ThemeManager
|
||||||
from portprotonqt.config_utils import read_theme_from_config
|
|
||||||
from portprotonqt.custom_widgets import ClickableLabel
|
from portprotonqt.custom_widgets import ClickableLabel
|
||||||
from portprotonqt.portproton_api import PortProtonAPI
|
from portprotonqt.portproton_api import PortProtonAPI
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
@@ -56,7 +54,7 @@ class GameCard(QFrame):
|
|||||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
self.customContextMenuRequested.connect(self._show_context_menu)
|
self.customContextMenuRequested.connect(self._show_context_menu)
|
||||||
self.theme_manager = ThemeManager()
|
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.display_filter = read_display_filter()
|
||||||
self.current_theme_name = read_theme_from_config()
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
@@ -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.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 QGraphicsItem, QToolButton, QFrame, QLabel, QGraphicsScene, QHBoxLayout, QWidget, QGraphicsView, QVBoxLayout, QSizePolicy
|
||||||
from PySide6.QtWidgets import QSpacerItem, QGraphicsPixmapItem, QDialog, QApplication
|
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.config_utils import read_theme_from_config
|
||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
@@ -177,7 +176,8 @@ class FullscreenDialog(QDialog):
|
|||||||
|
|
||||||
self.images = images
|
self.images = images
|
||||||
self.current_index = current_index
|
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.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
@@ -378,7 +378,8 @@ class ImageCarousel(QGraphicsView):
|
|||||||
self.images = images # Список кортежей: (QPixmap, caption)
|
self.images = images # Список кортежей: (QPixmap, caption)
|
||||||
self.image_items = []
|
self.image_items = []
|
||||||
self._animation = None
|
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.max_height = 300 # Default height for images
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
self.create_arrows()
|
self.create_arrows()
|
||||||
|
@@ -1061,6 +1061,10 @@ class InputManager(QObject):
|
|||||||
try:
|
try:
|
||||||
devices = [InputDevice(path) for path in list_devices()]
|
devices = [InputDevice(path) for path in list_devices()]
|
||||||
for device in 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()
|
caps = device.capabilities()
|
||||||
if ecodes.EV_KEY in caps or ecodes.EV_ABS in caps:
|
if ecodes.EV_KEY in caps or ecodes.EV_ABS in caps:
|
||||||
return device
|
return device
|
||||||
@@ -1081,7 +1085,9 @@ class InputManager(QObject):
|
|||||||
now = time.time()
|
now = time.time()
|
||||||
if event.type == ecodes.EV_KEY and event.value == 1:
|
if event.type == ecodes.EV_KEY and event.value == 1:
|
||||||
if event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
if event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
||||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
# Проверяем, не запущена ли игра
|
||||||
|
if not getattr(self._parent, '_gameLaunched', False):
|
||||||
|
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||||
else:
|
else:
|
||||||
self.button_pressed.emit(event.code)
|
self.button_pressed.emit(event.code)
|
||||||
elif event.type == ecodes.EV_ABS:
|
elif event.type == ecodes.EV_ABS:
|
||||||
|
@@ -4,10 +4,9 @@ import shutil
|
|||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import portprotonqt.themes.standart.styles as default_styles
|
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
||||||
from portprotonqt.game_card import GameCard
|
from portprotonqt.game_card import GameCard
|
||||||
from portprotonqt.animations import DetailPageAnimations
|
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
|
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.localization import _, get_egs_language, read_metadata_translations
|
||||||
from portprotonqt.logger import get_logger
|
|
||||||
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.tray_manager import TrayManager
|
from portprotonqt.tray_manager import TrayManager
|
||||||
@@ -60,15 +58,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.is_exiting = False
|
self.is_exiting = False
|
||||||
selected_theme = read_theme_from_config()
|
selected_theme = read_theme_from_config()
|
||||||
self.current_theme_name = selected_theme
|
self.current_theme_name = selected_theme
|
||||||
try:
|
self.theme = self.theme_manager.apply_theme(selected_theme)
|
||||||
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.tray_manager = TrayManager(self, app_name, self.current_theme_name)
|
||||||
self.card_width = read_card_size()
|
self.card_width = read_card_size()
|
||||||
self.setWindowTitle(app_name)
|
self.setWindowTitle(app_name)
|
||||||
@@ -209,9 +199,15 @@ class MainWindow(QMainWindow):
|
|||||||
self.createPortProtonTab() # вкладка 4
|
self.createPortProtonTab() # вкладка 4
|
||||||
self.createThemeTab() # вкладка 5
|
self.createThemeTab() # вкладка 5
|
||||||
|
|
||||||
|
self.controlHintsWidget = self.createControlHintsWidget()
|
||||||
|
mainLayout.addWidget(self.controlHintsWidget)
|
||||||
|
|
||||||
self.restore_state()
|
self.restore_state()
|
||||||
|
|
||||||
self.input_manager = InputManager(self)
|
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)
|
self.detail_animations = DetailPageAnimations(self, self.theme)
|
||||||
QTimer.singleShot(0, self.loadGames)
|
QTimer.singleShot(0, self.loadGames)
|
||||||
|
|
||||||
@@ -223,6 +219,98 @@ class MainWindow(QMainWindow):
|
|||||||
self.resize(width, height)
|
self.resize(width, height)
|
||||||
else:
|
else:
|
||||||
self.showNormal()
|
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)
|
@Slot(list)
|
||||||
def on_games_loaded(self, games: list[tuple]):
|
def on_games_loaded(self, games: list[tuple]):
|
||||||
self.games = games
|
self.games = games
|
||||||
|
@@ -22,6 +22,7 @@ import websocket
|
|||||||
import requests
|
import requests
|
||||||
import random
|
import random
|
||||||
import base64
|
import base64
|
||||||
|
import glob
|
||||||
|
|
||||||
downloader = Downloader()
|
downloader = Downloader()
|
||||||
logger = get_logger(__name__)
|
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}")
|
logger.error(f"An unexpected error occurred in get_exiftool_data for {game_exe}: {e}")
|
||||||
return {}
|
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]):
|
def load_steam_apps_async(callback: Callable[[list], None]):
|
||||||
"""
|
"""
|
||||||
Asynchronously loads the list of Steam applications, using cache if available.
|
Asynchronously loads the list of Steam applications, using cache if available.
|
||||||
Calls the callback with the list of apps.
|
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_dir = get_cache_dir()
|
||||||
cache_tar = os.path.join(cache_dir, "games_appid.tar.xz")
|
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):
|
if os.path.exists(cache_tar):
|
||||||
os.remove(cache_tar)
|
os.remove(cache_tar)
|
||||||
logger.info("Archive %s deleted after extraction", 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 []
|
steam_apps = data if isinstance(data, list) else []
|
||||||
logger.info("Loaded %d apps from archive", len(steam_apps))
|
logger.info("Loaded %d apps from archive", len(steam_apps))
|
||||||
callback(steam_apps)
|
callback(steam_apps)
|
||||||
@@ -325,11 +338,15 @@ def load_steam_apps_async(callback: Callable[[list], None]):
|
|||||||
app_list_url = (
|
app_list_url = (
|
||||||
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz"
|
"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)
|
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
|
||||||
else:
|
else:
|
||||||
app_list_url = (
|
app_list_url = (
|
||||||
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz"
|
"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)
|
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
|
||||||
|
|
||||||
def build_index(steam_apps):
|
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.
|
Asynchronously loads the list of WeAntiCheatYet data, using cache if available.
|
||||||
Calls the callback with the list of anti-cheat data.
|
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_dir = get_cache_dir()
|
||||||
cache_tar = os.path.join(cache_dir, "anticheat_games.tar.xz")
|
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 = (
|
app_list_url = (
|
||||||
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz"
|
"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)
|
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
|
||||||
else:
|
else:
|
||||||
app_list_url = (
|
app_list_url = (
|
||||||
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz"
|
"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)
|
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
|
||||||
|
|
||||||
def build_weanticheatyet_index(anti_cheat_data):
|
def build_weanticheatyet_index(anti_cheat_data):
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
|
import ast
|
||||||
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 portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
|
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -14,6 +14,59 @@ THEMES_DIRS = [
|
|||||||
os.path.join(xdg_data_home, "PortProtonQt", "themes"),
|
os.path.join(xdg_data_home, "PortProtonQt", "themes"),
|
||||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "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():
|
def list_themes():
|
||||||
"""
|
"""
|
||||||
@@ -49,9 +102,13 @@ def load_theme_screenshots(theme_name):
|
|||||||
|
|
||||||
def load_theme_fonts(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()
|
QFontDatabase.removeAllApplicationFonts()
|
||||||
fonts_folder = None
|
fonts_folder = None
|
||||||
if theme_name == "standart":
|
if theme_name == "standart":
|
||||||
@@ -66,7 +123,7 @@ def load_theme_fonts(theme_name):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not fonts_folder or not os.path.exists(fonts_folder):
|
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
|
return
|
||||||
|
|
||||||
for filename in os.listdir(fonts_folder):
|
for filename in os.listdir(fonts_folder):
|
||||||
@@ -75,13 +132,17 @@ def load_theme_fonts(theme_name):
|
|||||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||||
if font_id != -1:
|
if font_id != -1:
|
||||||
families = QFontDatabase.applicationFontFamilies(font_id)
|
families = QFontDatabase.applicationFontFamilies(font_id)
|
||||||
logger.info(f"Шрифт {filename} успешно загружен: {families}")
|
logger.info(f"Font {filename} successfully loaded: {families}")
|
||||||
else:
|
else:
|
||||||
logger.error(f"Ошибка загрузки шрифта: {filename}")
|
logger.error(f"Error loading font: {filename}")
|
||||||
|
|
||||||
|
_loaded_theme = theme_name
|
||||||
|
|
||||||
def load_logo():
|
def load_logo():
|
||||||
|
"""
|
||||||
|
Загружает логотип темы из стандартной папки.
|
||||||
|
"""
|
||||||
logo_path = None
|
logo_path = None
|
||||||
|
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
logo_path = os.path.join(base_dir, "themes", "standart", "images", "theme_logo.svg")
|
logo_path = os.path.join(base_dir, "themes", "standart", "images", "theme_logo.svg")
|
||||||
|
|
||||||
@@ -90,7 +151,7 @@ def load_logo():
|
|||||||
if file_extension == ".svg":
|
if file_extension == ".svg":
|
||||||
renderer = QSvgRenderer(logo_path)
|
renderer = QSvgRenderer(logo_path)
|
||||||
if not renderer.isValid():
|
if not renderer.isValid():
|
||||||
logger.error(f"Ошибка загрузки SVG логотипа: {logo_path}")
|
logger.error(f"Error loading SVG logo: {logo_path}")
|
||||||
return None
|
return None
|
||||||
pixmap = QPixmap(128, 128)
|
pixmap = QPixmap(128, 128)
|
||||||
pixmap.fill(QColor(0, 0, 0, 0))
|
pixmap.fill(QColor(0, 0, 0, 0))
|
||||||
@@ -109,69 +170,87 @@ class ThemeWrapper:
|
|||||||
self.custom_theme = custom_theme
|
self.custom_theme = custom_theme
|
||||||
self.metainfo = metainfo or {}
|
self.metainfo = metainfo or {}
|
||||||
self.screenshots = load_theme_screenshots(self.metainfo.get("name", ""))
|
self.screenshots = load_theme_screenshots(self.metainfo.get("name", ""))
|
||||||
|
self._default_theme = None # Lazy-loaded default theme
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if hasattr(self.custom_theme, name):
|
if hasattr(self.custom_theme, name):
|
||||||
return getattr(self.custom_theme, name)
|
return getattr(self.custom_theme, name)
|
||||||
import portprotonqt.themes.standart.styles as default_styles
|
if self._default_theme is None:
|
||||||
return getattr(default_styles, name)
|
self._default_theme = load_theme("standart") # Dynamically load standard theme
|
||||||
|
return getattr(self._default_theme, name)
|
||||||
|
|
||||||
def load_theme(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:
|
for themes_dir in THEMES_DIRS:
|
||||||
theme_folder = os.path.join(themes_dir, theme_name)
|
theme_folder = os.path.join(themes_dir, theme_name)
|
||||||
styles_file = os.path.join(theme_folder, "styles.py")
|
styles_file = os.path.join(theme_folder, "styles.py")
|
||||||
if os.path.exists(styles_file):
|
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)
|
spec = importlib.util.spec_from_file_location("theme_styles", styles_file)
|
||||||
if spec is None or spec.loader is None:
|
if spec is None or spec.loader is None:
|
||||||
continue
|
continue
|
||||||
custom_theme = importlib.util.module_from_spec(spec)
|
custom_theme = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(custom_theme)
|
spec.loader.exec_module(custom_theme)
|
||||||
|
if theme_name == "standart":
|
||||||
|
return custom_theme
|
||||||
meta = load_theme_metainfo(theme_name)
|
meta = load_theme_metainfo(theme_name)
|
||||||
wrapper = ThemeWrapper(custom_theme, metainfo=meta)
|
wrapper = ThemeWrapper(custom_theme, metainfo=meta)
|
||||||
wrapper.screenshots = load_theme_screenshots(theme_name)
|
wrapper.screenshots = load_theme_screenshots(theme_name)
|
||||||
return wrapper
|
return wrapper
|
||||||
raise FileNotFoundError(f"Файл стилей не найден для темы '{theme_name}'")
|
raise FileNotFoundError(f"Styles file not found for theme '{theme_name}'")
|
||||||
|
|
||||||
class ThemeManager:
|
class ThemeManager:
|
||||||
"""
|
"""
|
||||||
Класс для управления темами приложения.
|
Класс для управления темами приложения.
|
||||||
|
Реализует паттерн Singleton для единого экземпляра.
|
||||||
Позволяет получить список доступных тем, загрузить и применить выбранную тему.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
_instance = None
|
||||||
self.current_theme_name = None
|
|
||||||
self.current_theme_module = 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()
|
return list_themes()
|
||||||
|
|
||||||
def get_theme_logo(self):
|
def get_theme_logo(self):
|
||||||
"""Возвращает логотип для текущей или указанной темы."""
|
"""Возвращает логотип текущей темы."""
|
||||||
return load_logo()
|
return load_logo()
|
||||||
|
|
||||||
def apply_theme(self, theme_name):
|
def apply_theme(self, theme_name: str):
|
||||||
"""
|
"""
|
||||||
Применяет выбранную тему: загружает модуль стилей, шрифты и логотип.
|
Применяет указанную тему, если она ещё не применена.
|
||||||
Если загрузка прошла успешно, сохраняет выбранную тему в конфигурации.
|
Возвращает модуль темы или обёртку.
|
||||||
:param theme_name: Имя темы.
|
|
||||||
:return: Загруженный модуль темы (или обёртка).
|
|
||||||
"""
|
"""
|
||||||
theme_module = load_theme(theme_name)
|
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)
|
load_theme_fonts(theme_name)
|
||||||
self.current_theme_name = theme_name
|
self.current_theme_name = theme_name
|
||||||
self.current_theme_module = theme_module
|
self.current_theme_module = theme_module
|
||||||
save_theme_to_config(theme_name)
|
save_theme_to_config(theme_name)
|
||||||
logger.info(f"Тема '{theme_name}' успешно применена")
|
logger.info(f"Theme '{theme_name}' successfully applied")
|
||||||
return theme_module
|
return theme_module
|
||||||
|
|
||||||
def get_icon(self, icon_name, theme_name=None, as_path=False):
|
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):
|
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
|
return QIcon() if not as_path else None
|
||||||
|
|
||||||
if as_path:
|
if as_path:
|
||||||
|
BIN
portprotonqt/themes/standart/images/button_a.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
portprotonqt/themes/standart/images/button_b.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
portprotonqt/themes/standart/images/button_lb.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
portprotonqt/themes/standart/images/button_lt.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
portprotonqt/themes/standart/images/button_rb.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
portprotonqt/themes/standart/images/button_rt.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
portprotonqt/themes/standart/images/button_select.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
portprotonqt/themes/standart/images/button_start.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
portprotonqt/themes/standart/images/button_x.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
portprotonqt/themes/standart/images/button_y.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
portprotonqt/themes/standart/images/key_context.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
portprotonqt/themes/standart/images/key_e.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
portprotonqt/themes/standart/images/key_enter.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
portprotonqt/themes/standart/images/key_f11.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
portprotonqt/themes/standart/images/key_insert.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
portprotonqt/themes/standart/images/key_left.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
portprotonqt/themes/standart/images/key_right.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
@@ -9,7 +9,6 @@ from PySide6.QtGui import QIcon, QAction
|
|||||||
from PySide6.QtCore import QTimer
|
from PySide6.QtCore import QTimer
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
import portprotonqt.themes.standart.styles as default_styles
|
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.config_utils import read_favorites, read_theme_from_config, save_theme_to_config
|
from portprotonqt.config_utils import read_favorites, read_theme_from_config, save_theme_to_config
|
||||||
from portprotonqt.dialogs import GameLaunchDialog
|
from portprotonqt.dialogs import GameLaunchDialog
|
||||||
@@ -31,15 +30,7 @@ class TrayManager:
|
|||||||
self.theme_manager = ThemeManager()
|
self.theme_manager = ThemeManager()
|
||||||
selected_theme = read_theme_from_config()
|
selected_theme = read_theme_from_config()
|
||||||
self.current_theme_name = selected_theme
|
self.current_theme_name = selected_theme
|
||||||
try:
|
self.theme = self.theme_manager.apply_theme(selected_theme)
|
||||||
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.main_window = main_window
|
||||||
self.tray_icon = QSystemTrayIcon(self.main_window)
|
self.tray_icon = QSystemTrayIcon(self.main_window)
|
||||||
|
|
||||||
|
@@ -5,21 +5,25 @@
|
|||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
"pre-commit": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
"automerge": true
|
"automerge": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchDatasources": ["python-version"],
|
"matchFileNames": [".gitea/workflows/build.yml"],
|
||||||
"enabled": false
|
"enabled": false,
|
||||||
|
"description": "Disabled because download-artifact@v4 is not working"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchFileNames": [".python-version"],
|
"matchFileNames": [".python-version"],
|
||||||
"enabled": false
|
"enabled": false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchManagers": ["github-actions", "pre-commit", "poetry"],
|
"matchManagers": ["poetry", "pyenv"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -29,9 +33,14 @@
|
|||||||
"groupName": "Python dependencies"
|
"groupName": "Python dependencies"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchPackageNames": ["numpy", "setuptools"],
|
"matchPackageNames": ["numpy", "setuptools", "python"],
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
|
"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"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|