13 Commits

Author SHA1 Message Date
c7bed80570 chore(changelog): update
All checks were successful
renovate / renovate (push) Successful in 31s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-02 14:04:48 +05:00
6fde7c18db chore(documentation): fix anchors
All checks were successful
Code check / Check code (push) Successful in 1m39s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-02 11:57:01 +05:00
37782d4375 chore(documentation): mention animation
All checks were successful
Code check / Check code (push) Successful in 1m32s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-02 11:51:28 +05:00
0a8a7c538c added more animation to detail_page
All checks were successful
Code check / Check code (push) Successful in 1m37s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-02 11:35:23 +05:00
Gitea Actions
9cc4b8c51d chore: update steam apps list 2025-08-01T13:12:19Z 2025-08-01 13:12:19 +00:00
397dede2be feat: use devicePixelRatio for image scale
Some checks failed
Code check / Check code (push) Successful in 1m29s
Fetch Data / build (push) Failing after 49s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-29 12:15:22 +05:00
6a66f37ba1 fix: fix open context menu on gamepad
All checks were successful
Code check / Check code (push) Successful in 1m24s
renovate / renovate (push) Successful in 1m3s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-25 12:22:24 +05:00
4db1cce32c chore(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m29s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-25 11:44:43 +05:00
edaeca4f11 feat: set focus on first item of context menu
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-25 11:39:40 +05:00
11d44f091d fix(egs): prevent legendary list call when user.json is missing
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-25 11:32:14 +05:00
09d9c6510a chore: reduced duration of card opening animation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-25 11:13:54 +05:00
272be51bb0 feat(dev-script): added appimage cleaner script
All checks were successful
Build Check - AppImage, Arch, Fedora / Build AppImage (pull_request) Successful in 2m22s
Build Check - AppImage, Arch, Fedora / Build Fedora RPM (41) (pull_request) Has been skipped
Build Check - AppImage, Arch, Fedora / Build Fedora RPM (rawhide) (pull_request) Has been skipped
Build Check - AppImage, Arch, Fedora / Build Arch Package (pull_request) Has been skipped
Code check / Check code (push) Successful in 1m28s
Build Check - AppImage, Arch, Fedora / changes (pull_request) Successful in 28s
Code check / Check code (pull_request) Successful in 1m32s
Build Check - AppImage, Arch, Fedora / Build Fedora RPM (42) (pull_request) Has been skipped
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-22 14:17:16 +05:00
63933172f9 chore: pulse dropped from autoinstals
All checks were successful
Code check / Check code (push) Successful in 1m27s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-21 19:22:05 +05:00
22 changed files with 8554 additions and 1224 deletions

View File

@@ -17,11 +17,11 @@ jobs:
- name: Install required dependencies
run: |
sudo apt update
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync git
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync git zstd
- name: Install tools
run: |
pip3 install git+https://github.com/Frederic98/appimage-builder.git
pip3 install git+https://github.com/Boria138/appimage-builder.git
pip3 install uv
- name: Build AppImage

View File

@@ -23,11 +23,11 @@ jobs:
- name: Install required dependencies
run: |
sudo apt update
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync git
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync git zstd
- name: Install tools
run: |
pip3 install git+https://github.com/Frederic98/appimage-builder.git
pip3 install git+https://github.com/Boria138/appimage-builder.git
pip3 install uv
- name: Build AppImage
@@ -159,6 +159,7 @@ jobs:
mkdir -p extracted
find release/ -name '*.zip' -exec unzip -o {} -d extracted/ \;
find extracted/ -type f -exec mv {} release/ \;
find release/ -name '*.zip' -delete
rm -rf extracted/
- name: Extract changelog for version

View File

@@ -68,11 +68,11 @@ jobs:
- name: Install required dependencies
run: |
sudo apt update
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync git
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync zstd git
- name: Install tools
run: |
pip3 install git+https://github.com/Frederic98/appimage-builder.git
pip3 install git+https://github.com/Boria138/appimage-builder.git
pip3 install uv
- name: Build AppImage

View File

@@ -3,6 +3,25 @@
Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
- Больше типов анимаций при открытии карточки игры (за подробностями в документацию)
### Changed
- Уменьшена длительность анимации открытия карточки с 800 до 350мс
- Контекстное меню при открытие теперь сразу фокусируется на первом элементе
- Анимации теперь можно настраивать через темы (за подробностями в документацию)
### Fixed
- legendary list теперь не вызывается если вход в EGS не был произведён
- Скриншоты тем теперь не теряют в качестве при масштабе отличном от 100%
### Contributors
---
## [0.1.4] - 2025-07-21
### Added

View File

@@ -13,9 +13,9 @@ script:
# 5) чистим от ненужных модулей и бинарников
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3D*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtHelp*,QtMultimedia*,QtNetwork*,QtOpenGL*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialPort*,QtSql*,QtStateMachine*,QtTest*,QtWeb*,QtXml*}
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetwork*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
- shopt -s extglob
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6Gui*|libQt6Widgets*|libQt6OpenGL*|libQt6XcbQpa*|libQt6Wayland*|libQt6Egl*|libicudata*|libicuuc*|libicui18n*|libQt6DBus*|libQt6Svg*|libQt6Qml*|libQt6Network*)
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
AppDir:
path: ./AppDir
after_bundle:
@@ -82,5 +82,4 @@ AppDir:
PERLLIB: '${APPDIR}/usr/share/perl5:${APPDIR}/usr/lib/x86_64-linux-gnu/perl/5.34:${APPDIR}/usr/share/perl/5.34'
AppImage:
sign-key: None
comp: xz
arch: x86_64

View File

@@ -765,7 +765,7 @@
},
{
"normalized_name": "lost ark",
"status": "Broken"
"status": "Running"
},
{
"normalized_name": "archeage unchained",
@@ -4426,5 +4426,61 @@
{
"normalized_name": "carx street",
"status": "Broken"
},
{
"normalized_name": "warcos 2",
"status": "Broken"
},
{
"normalized_name": "karos classic",
"status": "Broken"
},
{
"normalized_name": "dead island riptide",
"status": "Running"
},
{
"normalized_name": "lineage",
"status": "Broken"
},
{
"normalized_name": "day of dragons",
"status": "Running"
},
{
"normalized_name": "sonic rumble",
"status": "Broken"
},
{
"normalized_name": "black stigma",
"status": "Broken"
},
{
"normalized_name": "umamusume pretty derby",
"status": "Running"
},
{
"normalized_name": "dirt rally",
"status": "Supported"
},
{
"normalized_name": "minifighter",
"status": "Broken"
},
{
"normalized_name": "hide & hold out h2o",
"status": "Running"
},
{
"normalized_name": "f1 25",
"status": "Denied"
},
{
"normalized_name": "ghost of tsushima director's cut",
"status": "Denied"
},
{
"normalized_name": "sword of justice",
"status": "Broken"
}
]

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,12 +1,56 @@
[
{
"normalized_title": "return alive",
"slug": "return-alive"
"normalized_title": "no sleep for kaname date from ai the somnium files",
"slug": "no-sleep-for-kaname-date-from-ai-the-somnium-files"
},
{
"normalized_title": "dead island 2",
"slug": "dead-island-2"
},
{
"normalized_title": "dead island",
"slug": "dead-island-definitive-edition"
},
{
"normalized_title": "wuchang fallen feathers",
"slug": "wuchang-fallen-feathers"
},
{
"normalized_title": "mindseye",
"slug": "mindseye"
},
{
"normalized_title": "alan wake",
"slug": "alan-wake"
},
{
"normalized_title": "s.t.a.l.k.e.r. anomaly g.a.m.m.a",
"slug": "s-t-a-l-k-e-r-anomaly-g-a-m-m-a"
},
{
"normalized_title": "fifa 18",
"slug": "fifa-18"
},
{
"normalized_title": "eriksholm the stolen dream",
"slug": "eriksholm-the-stolen-dream"
},
{
"normalized_title": "caravan sandwitch",
"slug": "caravan-sandwitch"
},
{
"normalized_title": "expeditions a mudrunner game",
"slug": "expeditions-a-mudrunner-game"
},
{
"normalized_title": "#drive rally",
"slug": "drive-rally"
},
{
"normalized_title": "return alive",
"slug": "return-alive"
},
{
"normalized_title": "recore",
"slug": "recore-definitive-edition"

Binary file not shown.

378
dev-scripts/appimage_clean.py Executable file
View File

@@ -0,0 +1,378 @@
#!/usr/bin/env python3
"""
PySide6 Dependencies Analyzer with ldd support
Анализирует зависимости PySide6 модулей используя ldd для определения
реальных зависимостей скомпилированных библиотек.
"""
import ast
import os
import sys
import subprocess
import re
from pathlib import Path
from typing import Set, Dict, List
import argparse
import json
class PySide6DependencyAnalyzer:
def __init__(self):
# Системные библиотеки, которые нужно всегда оставлять
self.system_libs = {
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
}
self.real_dependencies = {}
self.used_modules_code = set()
self.used_modules_ldd = set()
self.all_required_modules = set()
def find_python_files(self, directory: Path) -> List[Path]:
"""Находит все Python файлы в директории"""
python_files = []
for root, dirs, files in os.walk(directory):
dirs[:] = [d for d in dirs if d not in {'.venv', '__pycache__', '.git'}]
for file in files:
if file.endswith('.py'):
python_files.append(Path(root) / file)
return python_files
def find_pyside6_libs(self, base_path: Path) -> Dict[str, Path]:
"""Находит все PySide6 библиотеки (.so файлы)"""
libs = {}
# Поиск в единственной локации
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
print(f"Поиск PySide6 библиотек в: {search_path}")
if search_path.exists():
# Ищем .so файлы модулей
for so_file in search_path.glob("Qt*.*.so"):
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
if module_name.startswith('Qt'):
libs[module_name] = so_file
# Также ищем в подпапках
for subdir in search_path.iterdir():
if subdir.is_dir() and subdir.name.startswith('Qt'):
for so_file in subdir.glob("*.so*"):
if 'Qt' in so_file.name:
libs[subdir.name] = so_file
break
return libs
def analyze_ldd_dependencies(self, lib_path: Path) -> Set[str]:
"""Анализирует зависимости библиотеки с помощью ldd"""
qt_deps = set()
try:
result = subprocess.run(['ldd', str(lib_path)],
capture_output=True, text=True, check=True)
# Парсим вывод ldd и ищем Qt библиотеки
for line in result.stdout.split('\n'):
# Ищем строки вида: libQt6Core.so.6 => /path/to/lib
match = re.search(r'libQt6(\w+)\.so', line)
if match:
qt_module = f"Qt{match.group(1)}"
qt_deps.add(qt_module)
except (subprocess.CalledProcessError, FileNotFoundError) as e:
print(f"Предупреждение: не удалось выполнить ldd для {lib_path}: {e}")
return qt_deps
def build_real_dependency_graph(self, pyside_libs: Dict[str, Path]) -> Dict[str, Set[str]]:
"""Строит граф зависимостей на основе ldd анализа"""
dependencies = {}
print("Анализ реальных зависимостей с помощью ldd...")
for module, lib_path in pyside_libs.items():
print(f" Анализируется {module}...")
deps = self.analyze_ldd_dependencies(lib_path)
dependencies[module] = deps
if deps:
print(f" Зависимости: {', '.join(sorted(deps))}")
return dependencies
def analyze_file_imports(self, file_path: Path) -> Set[str]:
"""Анализирует один Python файл и возвращает используемые PySide6 модули"""
modules = set()
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name.startswith('PySide6.'):
module = alias.name.split('.', 2)[1]
if module.startswith('Qt'):
modules.add(module)
elif isinstance(node, ast.ImportFrom):
if node.module and node.module.startswith('PySide6.'):
module = node.module.split('.', 2)[1]
if module.startswith('Qt'):
modules.add(module)
except Exception as e:
print(f"Ошибка при анализе {file_path}: {e}")
return modules
def get_all_dependencies(self, modules: Set[str], dependency_graph: Dict[str, Set[str]]) -> Set[str]:
"""Получает все зависимости для набора модулей, используя граф зависимостей из ldd"""
all_deps = set(modules)
if not dependency_graph:
return all_deps
# Повторяем до тех пор, пока не найдем все транзитивные зависимости
changed = True
iteration = 0
while changed and iteration < 10: # Защита от бесконечного цикла
changed = False
current_deps = set(all_deps)
for module in current_deps:
if module in dependency_graph:
new_deps = dependency_graph[module] - all_deps
if new_deps:
all_deps.update(new_deps)
changed = True
iteration += 1
return all_deps
def analyze_project(self, project_path: Path, appdir_path: Path = None) -> Dict:
"""Анализирует весь проект"""
python_files = self.find_python_files(project_path)
print(f"Найдено {len(python_files)} Python файлов")
# Анализ статических импортов
used_modules_code = set()
file_modules = {}
for file_path in python_files:
modules = self.analyze_file_imports(file_path)
if modules:
file_modules[str(file_path.relative_to(project_path))] = list(modules)
used_modules_code.update(modules)
print(f"Найдено {len(used_modules_code)} модулей в коде: {', '.join(sorted(used_modules_code))}")
# Поиск PySide6 библиотек
search_base = appdir_path if appdir_path else project_path
pyside_libs = self.find_pyside6_libs(search_base)
if not pyside_libs:
print("ОШИБКА: PySide6 библиотеки не найдены! Анализ невозможен.")
return {
'error': 'PySide6 библиотеки не найдены',
'analysis_method': 'failed',
'found_libraries': 0,
'directly_used_code': sorted(used_modules_code),
'all_required': [],
'removable': [],
'available_modules': [],
'file_usage': file_modules
}
print(f"Найдено {len(pyside_libs)} PySide6 библиотек")
# Анализ реальных зависимостей с ldd
real_dependencies = self.build_real_dependency_graph(pyside_libs)
# Определяем модули, которые реально используются через ldd
used_modules_ldd = set()
for module in used_modules_code:
if module in real_dependencies:
used_modules_ldd.update(real_dependencies[module])
used_modules_ldd.add(module)
print(f"Реальные зависимости через ldd: {', '.join(sorted(used_modules_ldd))}")
# Объединяем результаты анализа кода и ldd
all_used_modules = used_modules_code | used_modules_ldd
# Получаем все необходимые модули включая зависимости
all_required = self.get_all_dependencies(all_used_modules, real_dependencies)
# Все доступные PySide6 модули
available_modules = set(pyside_libs.keys())
# Модули, которые можно удалить
removable = available_modules - all_required
return {
'analysis_method': 'ldd + static analysis',
'found_libraries': len(pyside_libs),
'directly_used_code': sorted(used_modules_code),
'directly_used_ldd': sorted(used_modules_ldd),
'all_required': sorted(all_required),
'removable': sorted(removable),
'available_modules': sorted(available_modules),
'file_usage': file_modules,
'real_dependencies': {k: sorted(v) for k, v in real_dependencies.items()},
'library_paths': {k: str(v) for k, v in pyside_libs.items()},
'analysis_summary': {
'total_modules': len(available_modules),
'required_modules': len(all_required),
'removable_modules': len(removable),
'space_saving_potential': f"{len(removable)/len(available_modules)*100:.1f}%" if available_modules else "0%"
}
}
def generate_appimage_recipe(self, removable_modules: List[str], template_path: Path) -> str:
"""Генерирует обновленный AppImage рецепт с командами очистки"""
# Читаем существующий рецепт
try:
with open(template_path, 'r', encoding='utf-8') as f:
recipe_content = f.read()
except FileNotFoundError:
print(f"Шаблон рецепта не найден: {template_path}")
return ""
# Генерируем новые команды очистки
cleanup_lines = []
# QML удаляем только если не используется
qml_modules = {'QtQml', 'QtQuick', 'QtQuickWidgets'}
if qml_modules.issubset(set(removable_modules)):
cleanup_lines.append(" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/")
# Инструменты разработки (всегда удаляем)
cleanup_lines.append(" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}")
# Модули для удаления
if removable_modules:
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
# Генерируем команду для удаления нативных библиотек с сохранением нужных
required_libs = set()
for module in sorted(set(self.all_required_modules)):
required_libs.add(f"libQt6{module.replace('Qt', '')}*")
# Добавляем системные библиотеки
for lib in self.system_libs:
required_libs.add(f"{lib}*")
keep_pattern = '|'.join(sorted(required_libs))
cleanup_lines.extend([
" - shopt -s extglob",
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
])
# Заменяем блок очистки в рецепте
import re
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines)
updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL)
return updated_recipe
def main():
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
parser.add_argument('project_path', help='Путь к проекту для анализа')
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
args = parser.parse_args()
project_path = Path(args.project_path)
if not project_path.exists():
print(f"Ошибка: путь {project_path} не существует")
sys.exit(1)
appdir_path = Path(args.appdir) if args.appdir else None
if appdir_path and not appdir_path.exists():
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
appdir_path = None
analyzer = PySide6DependencyAnalyzer()
results = analyzer.analyze_project(project_path, appdir_path)
# Сохраняем в анализатор для генерации команд
analyzer.all_required_modules = set(results.get('all_required', []))
# Выводим результаты
print("\n" + "="*60)
print("АНАЛИЗ ЗАВИСИМОСТЕЙ PYSIDE6 (ldd analysis)")
print("="*60)
if 'error' in results:
print(f"\nОШИБКА: {results['error']}")
sys.exit(1)
print(f"\nМетод анализа: {results['analysis_method']}")
print(f"Найдено библиотек: {results['found_libraries']}")
if results['directly_used_code']:
print(f"\nИспользуемые модули в коде ({len(results['directly_used_code'])}):")
for module in results['directly_used_code']:
print(f"{module}")
if results['directly_used_ldd']:
print(f"\nРеальные зависимости через ldd ({len(results['directly_used_ldd'])}):")
for module in results['directly_used_ldd']:
print(f"{module}")
print(f"\nВсе необходимые модули ({len(results['all_required'])}):")
for module in results['all_required']:
print(f"{module}")
print(f"\nМодули, которые можно удалить ({len(results['removable'])}):")
for module in results['removable']:
print(f"{module}")
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
if args.verbose and results['real_dependencies']:
Devlin(f"\nРеальные зависимости (ldd):")
for module, deps in results['real_dependencies'].items():
if deps:
print(f" {module}{', '.join(deps)}")
# Обновляем AppImage рецепт
recipe_path = Path("../build-aux/AppImageBuilder.yml")
if recipe_path.exists():
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
if updated_recipe:
with open(recipe_path, 'w', encoding='utf-8') as f:
f.write(updated_recipe)
print(f"\nAppImage рецепт обновлен: {recipe_path}")
else:
print(f"\nОШИБКА: не удалось обновить рецепт")
else:
print(f"\nПредупреждение: рецепт AppImage не найден в {recipe_path}")
# Сохраняем результаты в JSON
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"Результаты сохранены в: {args.output}")
print("\n" + "="*60)
if __name__ == "__main__":
main()

View File

@@ -3,12 +3,13 @@
---
## 📋 Contents
- [Overview](#overview)
- [Creating the Theme Folder](#creating-the-theme-folder)
- [Style File](#style-file)
- [Metadata](#metadata)
- [Screenshots](#screenshots)
- [Fonts and Icons](#fonts-and-icons)
- [Overview](#-overview)
- [Creating the Theme Folder](#-creating-the-theme-folder)
- [Style File](#-style-file-stylespy)
- [Animation configuration](#-animation-configuration)
- [Metadata](#-metadata-metainfoini)
- [Screenshots](#-screenshots)
- [Fonts and Icons](#-fonts-and-icons-optional)
---
@@ -45,6 +46,53 @@ def custom_button_style(color1, color2):
---
## 🎥 Animation configuration
The `GAME_CARD_ANIMATION` dictionary controls all animation parameters for game cards:
```python
GAME_CARD_ANIMATION = {
# Animation type when transitioning to a detailed page
# Available values: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce", "none"
"detail_page_animation_type": "fade",
# Border width settings (in pixels)
"default_border_width": 2,
"hover_border_width": 8,
"focus_border_width": 12,
"pulse_min_border_width": 8,
"pulse_max_border_width": 10,
# Animation duration (in milliseconds)
"thickness_anim_duration": 300,
"pulse_anim_duration": 800,
"gradient_anim_duration": 3000,
# Gradient animation angles (in degrees)
"gradient_start_angle": 360,
"gradient_end_angle": 0,
# Smoothing curves for smooth animations
"thickness_easing_curve": "OutBack",
"thickness_easing_curve_out": "InBack",
# Gradient colors for animated stroke
"gradient_colors": [
{"position": 0, "color": "#00fff5"},
{"position": 0.33, "color": "#FF5733"},
{"position": 0.66, "color": "#9B59B6"},
{"position": 1, "color": "#00fff5"}
],
# Duration of transitions to the detailed page
"detail_page_fade_duration": 350,
"detail_page_slide_duration": 500,
"detail_page_zoom_duration": 400
}
```
---
## 📝 Metadata (`metainfo.ini`)
```ini

View File

@@ -3,12 +3,13 @@
---
## 📋 Содержание
- [Обзор](#обзор)
- [Создание папки темы](#создание-папки-темы)
- [Файл стилей](#файл-стилей)
- [Метаинформация](#метаинформация)
- [Скриншоты](#скриншоты)
- [Шрифты и иконки](#шрифты-и-иконки)
- [Обзор](#-обзор)
- [Создание папки темы](#-создание-папки-темы)
- [Файл стилей](#-файл-стилей-stylespy)
- [Конфигурация анимации](#-конфигурация-анимации)
- [Метаинформация](#-метаинформация-metainfoini)
- [Скриншоты](#-скриншоты)
- [Шрифты и иконки](#-шрифты-и-иконки-опционально)
---
@@ -45,6 +46,53 @@ def custom_button_style(color1, color2):
---
## 🎥 Конфигурация анимации
Словарь `GAME_CARD_ANIMATION` управляет всеми параметрами анимации для карточек игр:
```python
GAME_CARD_ANIMATION = {
# Тип анимации при переходе на детальную страницу
# Доступные значения: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce", "none"
"detail_page_animation_type": "fade",
# Настройки ширины обводки (в пикселях)
"default_border_width": 2,
"hover_border_width": 8,
"focus_border_width": 12,
"pulse_min_border_width": 8,
"pulse_max_border_width": 10,
# Длительности анимаций (в миллисекундах)
"thickness_anim_duration": 300,
"pulse_anim_duration": 800,
"gradient_anim_duration": 3000,
# Углы анимации градиента (в градусах)
"gradient_start_angle": 360,
"gradient_end_angle": 0,
# Кривые сглаживания для плавных анимаций
"thickness_easing_curve": "OutBack",
"thickness_easing_curve_out": "InBack",
# Цвета градиента для анимированной обводки
"gradient_colors": [
{"position": 0, "color": "#00fff5"},
{"position": 0.33, "color": "#FF5733"},
{"position": 0.66, "color": "#9B59B6"},
{"position": 1, "color": "#00fff5"}
],
# Длительности переходов на детальную страницу
"detail_page_fade_duration": 350,
"detail_page_slide_duration": 500,
"detail_page_zoom_duration": 400
}
```
---
## 📝 Метаинформация (`metainfo.ini`)
```ini

View File

@@ -280,7 +280,12 @@ class ContextMenuManager:
)
)
menu.exec(game_card.mapToGlobal(pos))
# Устанавливаем фокус на первый элемент меню
actions = menu.actions()
if actions:
menu.setActiveAction(actions[0])
menu.exec(game_card.mapToGlobal(pos))
def _launch_game(self, game_card):
"""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

View File

@@ -1,3 +0,0 @@
name=Pulse Online
description_ru=Многопользовательская онлайн-игра в жанре MMORPG, действие которой происходит в научно-фантастическом мире с уникальной боевой системой и глубоким крафтом. Игроки могут исследовать обширные локации, выполнять квесты, сражаться с противниками и взаимодействовать с другими участниками игры.
description_en=A multiplayer online game in the MMORPG genre set in a sci-fi world with a unique combat system and deep crafting mechanics. Players can explore vast locations, complete quests, battle enemies, and interact with other participants in the game.

View File

@@ -747,6 +747,11 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
games: list[tuple] = []
cache_dir.mkdir(parents=True, exist_ok=True)
user_json_path = cache_dir / "user.json"
if not user_json_path.exists():
callback(games)
return
def process_games(installed_games: list | None):
if installed_games is None:
logger.info("No installed Epic Games Store games found")
@@ -855,12 +860,12 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
app_name,
f"legendary:launch:{app_name}",
"",
last_launch, # Время последнего запуска
formatted_playtime, # Форматированное время игры
protondb_tier, # ProtonDB tier
last_launch,
formatted_playtime,
protondb_tier,
status or "",
last_launch_timestamp, # Временная метка последнего запуска
playtime_seconds, # Время игры в секундах
last_launch_timestamp,
playtime_seconds,
"epic"
)
pending_images -= 1
@@ -880,7 +885,7 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
get_protondb_tier_async(steam_appid, on_protondb_tier)
else:
logger.debug(f"No Steam app found for EGS game {title}")
on_protondb_tier("") # Proceed with empty ProtonDB tier
on_protondb_tier("")
get_steam_apps_and_index_async(on_steam_apps)

View File

@@ -21,6 +21,13 @@ image_load_queue = Queue()
image_executor = ThreadPoolExecutor(max_workers=4)
queue_lock = threading.Lock()
def get_device_pixel_ratio() -> float:
"""
Retrieves the device pixel ratio from QApplication, with a fallback of 1.0 if not available.
"""
app = QApplication.instance()
return app.devicePixelRatio() if isinstance(app, QApplication) else 1.0
def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[QPixmap], None], app_name: str = ""):
"""
Асинхронно загружает обложку через очередь задач.
@@ -164,7 +171,6 @@ class FullscreenDialog(QDialog):
:param theme: Объект темы для стилизации (если None, используется default_styles)
"""
super().__init__(parent)
# Удаление диалога после закрытия
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.setFocus()
@@ -173,14 +179,12 @@ class FullscreenDialog(QDialog):
self.current_index = current_index
self.theme = theme if theme else default_styles
# Убираем стандартные элементы управления окна
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.init_ui()
self.update_display()
# Фильтруем события для закрытия диалога по клику
self.imageLabel.installEventFilter(self)
self.captionLabel.installEventFilter(self)
@@ -190,32 +194,28 @@ class FullscreenDialog(QDialog):
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
# Контейнер для изображения и стрелок
self.imageContainer = QWidget()
self.imageContainer.setFixedSize(self.FIXED_WIDTH, self.FIXED_HEIGHT)
self.imageContainerLayout = QHBoxLayout(self.imageContainer)
self.imageContainerLayout.setContentsMargins(0, 0, 0, 0)
self.imageContainerLayout.setSpacing(0)
# Левая стрелка
self.prevButton = QToolButton()
self.prevButton.setArrowType(Qt.ArrowType.LeftArrow)
self.prevButton.setStyleSheet(self.theme.PREV_BUTTON_STYLE)
self.prevButton.setStyleSheet(getattr(self.theme, "PREV_BUTTON_STYLE", ""))
self.prevButton.setCursor(Qt.CursorShape.PointingHandCursor)
self.prevButton.setFixedSize(40, 40)
self.prevButton.clicked.connect(self.show_prev)
self.imageContainerLayout.addWidget(self.prevButton)
# Метка для изображения
self.imageLabel = QLabel()
self.imageLabel.setFixedSize(self.FIXED_WIDTH - 80, self.FIXED_HEIGHT)
self.imageLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.imageContainerLayout.addWidget(self.imageLabel, stretch=1)
# Правая стрелка
self.nextButton = QToolButton()
self.nextButton.setArrowType(Qt.ArrowType.RightArrow)
self.nextButton.setStyleSheet(self.theme.NEXT_BUTTON_STYLE)
self.nextButton.setStyleSheet(getattr(self.theme, "NEXT_BUTTON_STYLE", ""))
self.nextButton.setCursor(Qt.CursorShape.PointingHandCursor)
self.nextButton.setFixedSize(40, 40)
self.nextButton.clicked.connect(self.show_next)
@@ -223,16 +223,14 @@ class FullscreenDialog(QDialog):
self.mainLayout.addWidget(self.imageContainer)
# Небольшой отступ между изображением и подписью
spacer = QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.mainLayout.addItem(spacer)
# Подпись
self.captionLabel = QLabel()
self.captionLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.captionLabel.setFixedHeight(40)
self.captionLabel.setWordWrap(True)
self.captionLabel.setStyleSheet(self.theme.CAPTION_LABEL_STYLE)
self.captionLabel.setStyleSheet(getattr(self.theme, "CAPTION_LABEL_STYLE", ""))
self.captionLabel.setCursor(Qt.CursorShape.PointingHandCursor)
self.mainLayout.addWidget(self.captionLabel)
@@ -241,28 +239,37 @@ class FullscreenDialog(QDialog):
if not self.images:
return
# Очищаем старое содержимое
self.imageLabel.clear()
self.captionLabel.clear()
QApplication.processEvents()
pixmap, caption = self.images[self.current_index]
# Масштабируем изображение так, чтобы оно поместилось в область фиксированного размера
# Учитываем devicePixelRatio для масштабирования высокого качества
device_pixel_ratio = get_device_pixel_ratio()
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
# Масштабируем изображение из оригинального pixmap
scaled_pixmap = pixmap.scaled(
self.FIXED_WIDTH - 80, # учитываем ширину стрелок
self.FIXED_HEIGHT,
target_width,
target_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
self.imageLabel.setPixmap(scaled_pixmap)
self.captionLabel.setText(caption)
self.setWindowTitle(caption)
# Принудительная перерисовка виджетов
self.imageLabel.repaint()
self.captionLabel.repaint()
self.repaint()
def resizeEvent(self, event):
"""Обновляет изображение при изменении размера окна."""
super().resizeEvent(event)
self.update_display() # Перерисовываем изображение с учетом нового размера
def show_prev(self):
"""Показывает предыдущее изображение."""
if self.images:
@@ -292,7 +299,6 @@ class FullscreenDialog(QDialog):
def mousePressEvent(self, event):
"""Закрывает диалог при клике на пустую область."""
pos = event.pos()
# Проверяем, находится ли клик вне imageContainer и captionLabel
if not (self.imageContainer.geometry().contains(pos) or
self.captionLabel.geometry().contains(pos)):
self.close()
@@ -305,15 +311,14 @@ class ClickablePixmapItem(QGraphicsPixmapItem):
"""
def __init__(self, pixmap, caption="Просмотр изображения", images_list=None, index=0, carousel=None):
"""
:param pixmap: QPixmap для отображения в карусели
:param pixmap: QPixmap для отображения в карусели (оригинальное, высокое разрешение)
:param caption: Подпись к изображению
:param images_list: Список всех изображений (кортежей (QPixmap, caption)),
чтобы в диалоге можно было перелистывать.
Если не передан, будет использован только текущее изображение.
:param index: Индекс текущего изображения в images_list.
:param carousel: Ссылка на родительскую карусель (ImageCarousel) для управления стрелками.
:param images_list: Список всех изображений (кортежей (QPixmap, caption))
:param index: Индекс текущего изображения в images_list
:param carousel: Ссылка на родительскую карусель (ImageCarousel)
"""
super().__init__(pixmap)
super().__init__()
self.original_pixmap = pixmap # Store original high-resolution pixmap
self.caption = caption
self.images_list = images_list if images_list is not None else [(pixmap, caption)]
self.index = index
@@ -323,6 +328,20 @@ class ClickablePixmapItem(QGraphicsPixmapItem):
self._click_start_position = None
self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.update_pixmap() # Set initial pixmap
def update_pixmap(self, height=300):
"""Update the displayed pixmap by scaling from the original high-resolution pixmap."""
if self.original_pixmap.isNull():
return
# Scale pixmap to desired height, considering device pixel ratio
device_pixel_ratio = get_device_pixel_ratio()
scaled_pixmap = self.original_pixmap.scaledToHeight(
int(height * device_pixel_ratio),
Qt.TransformationMode.SmoothTransformation
)
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
self.setPixmap(scaled_pixmap)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
@@ -339,17 +358,14 @@ class ClickablePixmapItem(QGraphicsPixmapItem):
event.accept()
def show_fullscreen(self):
# Скрываем стрелки карусели перед открытием FullscreenDialog
if self.carousel:
self.carousel.prevArrow.hide()
self.carousel.nextArrow.hide()
dialog = FullscreenDialog(self.images_list, current_index=self.index)
dialog.exec()
# После закрытия диалога обновляем видимость стрелок
if self.carousel:
self.carousel.update_arrows_visibility()
class ImageCarousel(QGraphicsView):
"""
Карусель изображений с адаптивностью, возможностью увеличения по клику
@@ -357,19 +373,16 @@ class ImageCarousel(QGraphicsView):
"""
def __init__(self, images: list[tuple], parent: QWidget | None = None, theme: object | None = None):
super().__init__(parent)
# Аннотируем тип scene как QGraphicsScene
self.carousel_scene: QGraphicsScene = QGraphicsScene(self)
self.setScene(self.carousel_scene)
self.images = images # Список кортежей: (QPixmap, caption)
self.image_items = []
self._animation = None
self.theme = theme if theme else default_styles
self.max_height = 300 # Default height for images
self.init_ui()
self.create_arrows()
# Переменные для поддержки перетаскивания
self._drag_active = False
self._drag_start_position = None
self._scroll_start_value = None
@@ -380,30 +393,38 @@ class ImageCarousel(QGraphicsView):
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setFrameShape(QFrame.Shape.NoFrame)
x_offset = 10 # Отступ между изображениями
max_height = 300 # Фиксированная высота изображений
self.update_scene()
def update_scene(self):
"""Update the scene with scaled images based on current size and scale."""
self.carousel_scene.clear()
self.image_items.clear()
x_offset = 10
x = 0
device_pixel_ratio = get_device_pixel_ratio()
for i, (pixmap, caption) in enumerate(self.images):
item = ClickablePixmapItem(
pixmap.scaledToHeight(max_height, Qt.TransformationMode.SmoothTransformation),
pixmap, # Pass original pixmap
caption,
images_list=self.images,
index=i,
carousel=self # Передаем ссылку на карусель
carousel=self
)
item.update_pixmap(self.max_height) # Scale to current height
item.setPos(x, 0)
self.carousel_scene.addItem(item)
self.image_items.append(item)
x += item.pixmap().width() + x_offset
x += item.pixmap().width() / device_pixel_ratio + x_offset
self.setSceneRect(0, 0, x, max_height)
self.setSceneRect(0, 0, x, self.max_height)
def create_arrows(self):
"""Создаёт кнопки-стрелки и привязывает их к функциям прокрутки."""
self.prevArrow = QToolButton(self)
self.prevArrow.setArrowType(Qt.ArrowType.LeftArrow)
self.prevArrow.setStyleSheet(self.theme.PREV_BUTTON_STYLE) # type: ignore
self.prevArrow.setStyleSheet(getattr(self.theme, "PREV_BUTTON_STYLE", ""))
self.prevArrow.setFixedSize(40, 40)
self.prevArrow.setCursor(Qt.CursorShape.PointingHandCursor)
self.prevArrow.setAutoRepeat(True)
@@ -414,7 +435,7 @@ class ImageCarousel(QGraphicsView):
self.nextArrow = QToolButton(self)
self.nextArrow.setArrowType(Qt.ArrowType.RightArrow)
self.nextArrow.setStyleSheet(self.theme.NEXT_BUTTON_STYLE) # type: ignore
self.nextArrow.setStyleSheet(getattr(self.theme, "NEXT_BUTTON_STYLE", ""))
self.nextArrow.setFixedSize(40, 40)
self.nextArrow.setCursor(Qt.CursorShape.PointingHandCursor)
self.nextArrow.setAutoRepeat(True)
@@ -423,14 +444,9 @@ class ImageCarousel(QGraphicsView):
self.nextArrow.clicked.connect(self.scroll_right)
self.nextArrow.raise_()
# Проверяем видимость стрелок при создании
self.update_arrows_visibility()
def update_arrows_visibility(self):
"""
Показывает стрелки, если контент шире видимой области.
Иначе скрывает их.
"""
if hasattr(self, "prevArrow") and hasattr(self, "nextArrow"):
if self.horizontalScrollBar().maximum() == 0:
self.prevArrow.hide()
@@ -444,7 +460,8 @@ class ImageCarousel(QGraphicsView):
margin = 10
self.prevArrow.move(margin, (self.height() - self.prevArrow.height()) // 2)
self.nextArrow.move(self.width() - self.nextArrow.width() - margin,
(self.height() - self.nextArrow.height()) // 2)
(self.height() - self.nextArrow.height()) // 2)
self.update_scene() # Re-scale images on resize
self.update_arrows_visibility()
def animate_scroll(self, end_value):
@@ -469,19 +486,15 @@ class ImageCarousel(QGraphicsView):
self.animate_scroll(new_value)
def update_images(self, new_images):
self.carousel_scene.clear()
self.images = new_images
self.image_items.clear()
self.init_ui()
self.update_scene()
self.update_arrows_visibility()
# Обработка событий мыши для перетаскивания
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._drag_active = True
self._drag_start_position = event.pos()
self._scroll_start_value = self.horizontalScrollBar().value()
# Скрываем стрелки при начале перетаскивания
if hasattr(self, "prevArrow"):
self.prevArrow.hide()
if hasattr(self, "nextArrow"):
@@ -497,6 +510,5 @@ class ImageCarousel(QGraphicsView):
def mouseReleaseEvent(self, event):
self._drag_active = False
# Показываем стрелки после завершения перетаскивания (с проверкой видимости)
self.update_arrows_visibility()
super().mouseReleaseEvent(event)

View File

@@ -35,14 +35,13 @@ from portprotonqt.howlongtobeat_api import HowLongToBeat
from portprotonqt.downloader import Downloader
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, QScrollArea, QSlider,
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QGraphicsEffect, QGraphicsOpacityEffect, QApplication, QPushButton, QProgressBar, QCheckBox)
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QGraphicsOpacityEffect, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy)
from PySide6.QtCore import Qt, QAbstractAnimation, QPropertyAnimation, QByteArray, QUrl, Signal, QTimer, Slot, QEasingCurve, QParallelAnimationGroup, QRect
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
from PySide6.QtCore import Qt, QAbstractAnimation, QPropertyAnimation, QByteArray, QUrl, Signal, QTimer, Slot
from typing import cast
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from PySide6.QtWidgets import QSizePolicy
logger = get_logger(__name__)
@@ -1880,17 +1879,126 @@ class MainWindow(QMainWindow):
self.current_play_button = playButton
# Анимация
opacityEffect = QGraphicsOpacityEffect(detailPage)
detailPage.setGraphicsEffect(opacityEffect)
animation = QPropertyAnimation(opacityEffect, QByteArray(b"opacity"))
animation.setDuration(800)
animation.setStartValue(0)
animation.setEndValue(1)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self._animations[detailPage] = animation
animation.finished.connect(
lambda: detailPage.setGraphicsEffect(cast(QGraphicsEffect, None))
)
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350)
if animation_type == "fade":
opacity_effect = QGraphicsOpacityEffect(detailPage)
detailPage.setGraphicsEffect(opacity_effect)
animation = QPropertyAnimation(opacity_effect, QByteArray(b"opacity"))
animation.setDuration(duration)
animation.setStartValue(0)
animation.setEndValue(1)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self._animations[detailPage] = animation
animation.finished.connect(lambda: None)
elif animation_type == "slide_left":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
detailPage.move(self.width(), 0)
animation = QPropertyAnimation(detailPage, QByteArray(b"pos"))
animation.setDuration(duration)
animation.setStartValue(detailPage.pos())
animation.setEndValue(self.stackedWidget.rect().topLeft())
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self._animations[detailPage] = animation
elif animation_type == "slide_right":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
detailPage.move(-self.width(), 0)
animation = QPropertyAnimation(detailPage, QByteArray(b"pos"))
animation.setDuration(duration)
animation.setStartValue(detailPage.pos())
animation.setEndValue(self.stackedWidget.rect().topLeft())
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self._animations[detailPage] = animation
elif animation_type == "slide_up":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
detailPage.move(0, self.height())
animation = QPropertyAnimation(detailPage, QByteArray(b"pos"))
animation.setDuration(duration)
animation.setStartValue(detailPage.pos())
animation.setEndValue(self.stackedWidget.rect().topLeft())
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self._animations[detailPage] = animation
elif animation_type == "slide_down":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
detailPage.move(0, -self.height())
animation = QPropertyAnimation(detailPage, QByteArray(b"pos"))
animation.setDuration(duration)
animation.setStartValue(detailPage.pos())
animation.setEndValue(self.stackedWidget.rect().topLeft())
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self._animations[detailPage] = animation
elif animation_type == "bounce":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_zoom_duration", 400)
detailPage.setWindowOpacity(0.0)
opacity_anim = QPropertyAnimation(detailPage, QByteArray(b"windowOpacity"))
opacity_anim.setDuration(duration)
opacity_anim.setStartValue(0.0)
opacity_anim.setEndValue(1.0)
# Animate geometry
initial_rect = QRect(detailPage.x() + detailPage.width() // 4, detailPage.y() + detailPage.height() // 4,
detailPage.width() // 2, detailPage.height() // 2)
final_rect = detailPage.geometry()
geometry_anim = QPropertyAnimation(detailPage, QByteArray(b"geometry"))
geometry_anim.setDuration(duration)
geometry_anim.setStartValue(initial_rect)
geometry_anim.setEndValue(final_rect)
geometry_anim.setEasingCurve(QEasingCurve.Type.OutBack)
group_anim = QParallelAnimationGroup()
group_anim.addAnimation(opacity_anim)
group_anim.addAnimation(geometry_anim)
def load_image_and_restore_effect():
if not detailPage or detailPage.isHidden():
logger.warning("Detail page is None or hidden, skipping image load")
return
# No need to restore graphics effect, just ensure full opacity
detailPage.setWindowOpacity(1.0)
if cover_path:
def on_pixmap_ready(pixmap):
if not detailPage or detailPage.isHidden():
logger.warning("Detail page is None or hidden, skipping pixmap update")
return
rounded = round_corners(pixmap, 10)
imageLabel.setPixmap(rounded)
logger.debug("Pixmap set for imageLabel")
def on_palette_ready(palette):
if not detailPage or detailPage.isHidden():
logger.warning("Detail page is None or hidden, skipping palette update")
return
dark_palette = [self.darkenColor(color, factor=200) for color in palette]
stops = ",\n".join(
[f"stop:{i/(len(dark_palette)-1):.2f} {dark_palette[i].name()}" for i in range(len(dark_palette))]
)
detailPage.setStyleSheet(self.theme.detail_page_style(stops))
logger.debug("Stylesheet updated with palette")
self.getColorPalette_async(cover_path, num_colors=5, callback=on_palette_ready)
load_pixmap_async(cover_path, 300, 400, on_pixmap_ready)
# Clean up function
def cleanup_animation():
if detailPage in self._animations:
del self._animations[detailPage]
group_anim.finished.connect(load_image_and_restore_effect)
group_anim.finished.connect(cleanup_animation)
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self._animations[detailPage] = group_anim
elif animation_type == "none":
pass
def toggleFavoriteInDetailPage(self, game_name, label):
favorites = read_favorites()

View File

@@ -9,6 +9,10 @@ favoriteLabelSize = 48, 48
pixmapsScaledSize = 60, 60
GAME_CARD_ANIMATION = {
# Тип анимации fade при входе на детальную страницу
# Возможные значения: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce", "none"
"detail_page_animation_type": "fade",
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
# Значение в пикселях.
@@ -75,7 +79,16 @@ GAME_CARD_ANIMATION = {
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
{"position": 0.66, "color": "#9B59B6"}, # Цвет на 66% (пурпурный)
{"position": 1, "color": "#00fff5"} # Конечный цвет (возвращение к циану)
]
],
# Длительность анимации fade при входе на детальную страницу
"detail_page_fade_duration": 350,
# Длительность анимации slide при входе на детальную страницу
"detail_page_slide_duration": 500,
# Длительность анимации zoom при входе на детальную страницу
"detail_page_zoom_duration": 400
}
# СТИЛЬ ШАПКИ ГЛАВНОГО ОКНА

View File

@@ -27,6 +27,10 @@ color_g = "rgba(0, 0, 0, 0)"
color_h = "transparent"
GAME_CARD_ANIMATION = {
# Тип анимации fade при входе на детальную страницу
# Возможные значения: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce", "none"
"detail_page_animation_type": "fade",
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
# Значение в пикселях.
@@ -93,7 +97,16 @@ GAME_CARD_ANIMATION = {
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
{"position": 0.66, "color": "#9B59B6"}, # Цвет на 66% (пурпурный)
{"position": 1, "color": "#00fff5"} # Конечный цвет (возвращение к циану)
]
],
# Длительность анимации fade при входе на детальную страницу
"detail_page_fade_duration": 350,
# Длительность анимации slide при входе на детальную страницу
"detail_page_slide_duration": 500,
# Длительность анимации zoom при входе на детальную страницу
"detail_page_zoom_duration": 400
}
CONTEXT_MENU_STYLE = f"""