51 Commits
v0.1.2 ... main

Author SHA1 Message Date
3aa6b90f1e chore(readme): update todo
All checks were successful
Check Translations / check-translations (push) Successful in 12s
Code and build check / Check code (push) Successful in 1m34s
Code and build check / Build with uv (push) Successful in 49s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
66f19afdaa chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
0a915b4630 fix: QMessageBox on context menu
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
762d071396 feat: added add to desktop and menu to egs context menu
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
ca442f3318 feat: update context menu for egs games
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
9708b8d1a2 feat(egs-api): Implement add_egs_to_steam to add EGS games to Steam via shortcuts.vdf
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
0802c97d9f feat(egs-api): add Steam ID
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
c59a0401b1 feat: added playtime and last launch to EGS
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
3a8a9bf2eb fix: prevent premature game termination detection for EGS games
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
73afe7c554 feat: added import to context menu
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
a8ec615352 feat: replace steam placeholder icon to real egs icon
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
74622356fc feat: added handle egs games to toggleGame
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
be10225e4e Revert "feat: hide the games from EGS until after the workout"
This reverts commit a21705da15.

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-22 13:27:30 +00:00
329b7f1038 standart_theme: reworked styles, adding vars
All checks were successful
Code and build check / Check code (pull_request) Successful in 1m23s
Code and build check / Build with uv (pull_request) Successful in 50s
Code and build check / Check code (push) Successful in 1m18s
Code and build check / Build with uv (push) Successful in 48s
2025-06-22 13:22:07 +07:00
fea07e19fe reworked styles to look the same in normal use and gamescope session
All checks were successful
Code and build check / Check code (pull_request) Successful in 1m44s
Code and build check / Build with uv (pull_request) Successful in 59s
Code and build check / Check code (push) Successful in 1m28s
Code and build check / Build with uv (push) Successful in 1m0s
2025-06-21 22:33:03 +07:00
37b108f689 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-21 17:45:27 +05:00
78f5118709 native package: fix combobox list
All checks were successful
Code and build check / Check code (push) Successful in 1m40s
Code and build check / Build with uv (push) Successful in 51s
2025-06-21 12:39:23 +00:00
1f14dd7fdf native package: fix combobox full width in PORTPROTON SETTINGS thanks to @Boria138 2025-06-21 12:39:23 +00:00
3d3bdd8f98 dark_theme remove duplicate SETTINGS_CHECKBOX_STYLE 2025-06-21 12:39:23 +00:00
9d7c674544 native package: combobox full width in PORTPROTON SETTINGS 2025-06-21 12:39:23 +00:00
e6c90508ab native package: left alignment for portproton & addgame QLabels 2025-06-21 12:39:23 +00:00
d0eea92139 chore(readme): update todo
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-21 11:29:19 +05:00
04726491c0 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-21 11:25:18 +05:00
bd1b7c07ae fix: force Fusion style for consistent QComboBox styling
All checks were successful
Code and build check / Check code (push) Successful in 1m28s
Code and build check / Build with uv (push) Successful in 49s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-21 10:46:51 +05:00
e6161d2e3f feat(ci): disable renovate untill uppstream fixed work with .python-version
All checks were successful
Code and build check / Check code (push) Successful in 1m38s
Code and build check / Build with uv (push) Successful in 56s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-21 09:15:01 +05:00
b82080600f fix(renovate): disable workflow and python version update
All checks were successful
Code and build check / Check code (push) Successful in 1m37s
Code and build check / Build with uv (push) Successful in 57s
renovate / renovate (push) Successful in 24s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 23:01:00 +05:00
05693514aa fix(renovate): uv lock file maintance
All checks were successful
Code and build check / Check code (push) Successful in 1m57s
Code and build check / Build with uv (push) Successful in 1m2s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 22:50:52 +05:00
1c2835a933 chore(deps): update
All checks were successful
Code and build check / Check code (push) Successful in 1m42s
Code and build check / Build with uv (push) Successful in 1m0s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 22:41:01 +05:00
d229914fb6 Revert "chore(deps): pin dependencies"
All checks were successful
Code and build check / Check code (push) Successful in 1m45s
Code and build check / Build with uv (push) Successful in 58s
This reverts commit 4d58830910.
2025-06-20 22:06:21 +05:00
ce69a18249 fix(renovate): workflow ignore
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 22:06:01 +05:00
4d58830910 chore(deps): pin dependencies
All checks were successful
Code and build check / Check code (pull_request) Successful in 1m48s
Code and build check / Build with uv (pull_request) Successful in 56s
Code and build check / Check code (push) Successful in 1m49s
Code and build check / Build with uv (push) Successful in 59s
renovate / renovate (push) Successful in 56s
2025-06-20 16:52:33 +00:00
016ba537be fix(renovate): config syntax again
Some checks failed
Code and build check / Build with uv (push) Has been cancelled
Code and build check / Check code (push) Has been cancelled
renovate / renovate (push) Successful in 39s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:51:37 +05:00
6eeb93f6ba fix(renovate): config syntax
Some checks failed
Code and build check / Build with uv (push) Has been cancelled
Code and build check / Check code (push) Has been cancelled
renovate / renovate (push) Successful in 12s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:48:49 +05:00
3f5d058740 fix(renovate): RENOVATE_CONFIG_FILE
Some checks failed
Code and build check / Build with uv (push) Has been cancelled
Code and build check / Check code (push) Has been cancelled
renovate / renovate (push) Successful in 10s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:44:25 +05:00
1a9228b76d ci: added renovate auto update bot
Some checks failed
Code and build check / Check code (push) Successful in 1m32s
Code and build check / Build with uv (push) Successful in 49s
renovate / renovate (push) Failing after 54s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:37:14 +05:00
e9e0bea854 feat: stay overlay on top
All checks were successful
Code and build check / Check code (push) Successful in 1m30s
Code and build check / Build with uv (push) Successful in 50s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 19:31:33 +05:00
f7d9f5c150 chore(readme): update todo
All checks were successful
Code and build check / Check code (push) Successful in 1m48s
Code and build check / Build with uv (push) Successful in 55s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 18:36:45 +05:00
bcb5987d31 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 18:32:50 +05:00
b1aa987e4e fix: ensure application quits on window close
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 18:30:24 +05:00
f4c8b70bd0 feat: add --session CLI argument for start gamescope
All checks were successful
Code and build check / Check code (push) Successful in 1m39s
Code and build check / Build with uv (push) Successful in 51s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-18 22:48:24 +05:00
ff960df77c feat: transfer focus to hovered GameCard with mutual exclusivity
All checks were successful
Code and build check / Check code (push) Successful in 1m50s
Code and build check / Build with uv (push) Successful in 54s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 23:11:25 +05:00
a57f509295 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 23:01:38 +05:00
32bbe89911 fix: enforce mutual exclusivity of hovered and focused states in GameCard
All checks were successful
Code and build check / Check code (push) Successful in 1m40s
Code and build check / Build with uv (push) Successful in 54s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 22:58:57 +05:00
593db00166 fix(themes): typo in GAME_CARD_ANIMATION
All checks were successful
Code and build check / Check code (push) Successful in 1m44s
Code and build check / Build with uv (push) Successful in 58s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 14:39:21 +05:00
79a78c785b chore(changelog): update
Some checks failed
Code and build check / Check code (push) Failing after 1m43s
Code and build check / Build with uv (push) Successful in 57s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 14:26:19 +05:00
0b92d058a9 feat: move GameCard animation properties to styles
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 14:24:06 +05:00
9df22edfc9 chore(changelog): update
All checks were successful
Code and build check / Check code (push) Successful in 2m0s
Code and build check / Build with uv (push) Successful in 52s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 22:36:56 +05:00
4559231712 fix: prevent multiple GameCard highlight animations on rapid mouse movement
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 22:34:06 +05:00
18dbd42369 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 17:14:22 +05:00
76c0e607c5 fedora 40 is EOL
All checks were successful
Code and build check / Check code (push) Successful in 1m46s
Code and build check / Build with uv (push) Successful in 53s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 17:11:52 +05:00
a91c9dacd8 fix(build): fedora dependency
Some checks failed
Code and build check / Build with uv (push) Has been cancelled
Code and build check / Check code (push) Has been cancelled
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 17:10:18 +05:00
29 changed files with 3307 additions and 1304 deletions

View File

@ -40,7 +40,7 @@ jobs:
strategy:
matrix:
fedora_version: [40, 41, 42, rawhide]
fedora_version: [41, 42, rawhide]
container:
image: fedora:${{ matrix.fedora_version }}

View File

@ -97,7 +97,7 @@ jobs:
strategy:
matrix:
fedora_version: [40, 41, 42, rawhide]
fedora_version: [41, 42, rawhide]
container:
image: fedora:${{ matrix.fedora_version }}

View File

@ -3,6 +3,27 @@
Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
- Аргумент `--session` для запуска приложения в gamescope с GAMESCOPE_CMD
### Changed
- Удалены сборки для Fedora 40
- Перенесены параметры анимации GameCard в `styles.py` с подробной документацией для поддержки кастомизации тем.
- Статус выделения и наведения на карточки теперь взаимоисключают друг друга
### Fixed
- Дублирование обводки выделения карточек при быстром перемешении мыши
- Завершение приложения при закритие окна
- Использование системной палитры в темах
- Ошибки темы в нативном пакете
### Contributors
- @Dervart
---
## [0.1.2] - 2025-06-15
### Added
@ -64,6 +85,10 @@
- Корректная обработка событий геймпада во время игры
- Убийсво всех процессов "зомби" при закрытии программы
### Contributors
- @Vector_null
- @Dervart
---
## [0.1.1] 2025-05-17
@ -84,6 +109,11 @@
- Зависание GUI
- Сбой при повреждённом Steam
### Contributors
- @Vector_null
- @Dervart
- @alex2844
---
> См. подробности по каждому коммиту в истории репозитория.

View File

@ -4,7 +4,6 @@
<p align="center">Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.</p>
</div>
## В планах
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
@ -15,7 +14,8 @@
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
- [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор)
- [ ] Продумать систему вкладок вместо текущей
- [X] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
- [ ] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
- [X] Разобраться почему теряется часть стилей в Gamescope
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
- [X] Получать описания и названия игр из базы данных Steam
@ -39,9 +39,11 @@
- [X] Добавить в карточки данные с AreWeAntiCheatYet
- [X] Продублировать бейджи с карточки на страницу с деталями игры
- [X] Добавить парсинг ярлыков из Steam
- [X] Добавить парсинг ярлыков из EGS (скрыто для переработки)
- [X] Добавить парсинг ярлыков из EGS
- [ ] Избавиться от бинарника legendary
- [ ] Добавить запуск и скачивание игр из EGS
- [X] Добавить запуск игр из EGS
- [ ] Добавить скачивание игр из EGS
- [ ] Добавить поддержку запуска сторонних игр из EGS
- [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода
- [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api)
- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql)
@ -55,6 +57,7 @@
- [X] Добавить систему избранного для карточек
- [X] Заменить все `print` на `logging`
- [ ] Привести все логи к единому языку
- [ ] Уменьшить количество строк для перевода
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
- [X] Исправить частичное применение тем на лету
@ -68,6 +71,7 @@
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
- [ ] Доделать светлую тему
- [ ] Добавить подсказки к управлению с геймпада
- [ ] Добавить загрузку звуков в темы например для добавления звука запуска в тему и тд
### Установка (devel)

View File

@ -28,19 +28,19 @@ BuildRequires: git
%package -n python3-%{pypi_name}-git
Summary: %{summary}
%{?python_provide:%python_provide python3-%{pypi_name}}
Requires: python3dist(babel)
Requires: python3dist(evdev)
Requires: python3dist(icoextract)
Requires: python3dist(numpy)
Requires: python3dist(orjson)
Requires: python3dist(psutil)
Requires: python3dist(pyside6)
Requires: python3dist(pyudev)
Requires: python3dist(requests)
Requires: python3dist(tqdm)
Requires: python3dist(vdf)
Requires: python3dist(pefile)
Requires: python3dist(pillow)
Requires: python3-babel
Requires: python3-evdev
Requires: python3-icoextract
Requires: python3-numpy
Requires: python3-orjson
Requires: python3-psutil
Requires: python3-pyside6
Requires: python3-pyudev
Requires: python3-requests
Requires: python3-tqdm
Requires: python3-vdf
Requires: python3-pefile
Requires: python3-pillow
Requires: perl-Image-ExifTool
Requires: xdg-utils

View File

@ -25,19 +25,19 @@ BuildRequires: git
%package -n python3-%{pypi_name}
Summary: %{summary}
%{?python_provide:%python_provide python3-%{pypi_name}}
Requires: python3dist(babel)
Requires: python3dist(evdev)
Requires: python3dist(icoextract)
Requires: python3dist(numpy)
Requires: python3dist(orjson)
Requires: python3dist(psutil)
Requires: python3dist(pyside6)
Requires: python3dist(pyudev)
Requires: python3dist(requests)
Requires: python3dist(tqdm)
Requires: python3dist(vdf)
Requires: python3dist(pefile)
Requires: python3dist(pillow)
Requires: python3-babel
Requires: python3-evdev
Requires: python3-icoextract
Requires: python3-numpy
Requires: python3-orjson
Requires: python3-psutil
Requires: python3-pyside6
Requires: python3-pyudev
Requires: python3-requests
Requires: python3-tqdm
Requires: python3-vdf
Requires: python3-pefile
Requires: python3-pillow
Requires: perl-Image-ExifTool
Requires: xdg-utils

8
config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
"endpoint": "https://git.linux-gaming.ru/api/v1",
"gitAuthor": "Renovate Bot <noreply@linux-gaming.ru>",
"platform": "gitea",
"onboardingConfigFileName": "renovate.json",
"autodiscover": true,
"optimizeForDisabled": true,
};

View File

@ -14,3 +14,5 @@ MIME-Version:
Content-Type:
Content-Transfer-Encoding:
Generated-By:
start.sh
EGS

View File

@ -20,9 +20,9 @@ Current translation status:
| Locale | Progress | Translated |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 161 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 161 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 161 of 161 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 194 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 194 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 194 of 194 |
---

View File

@ -20,9 +20,9 @@
| Локаль | Прогресс | Переведено |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 161 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 161 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 161 из 161 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 194 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 194 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 194 из 194 |
---

View File

@ -1,4 +1,6 @@
import sys
import os
import subprocess
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon
@ -33,6 +35,13 @@ def main():
window = MainWindow()
if args.session:
gamescope_cmd = os.getenv("GAMESCOPE_CMD", "gamescope -f --xwayland-count 2")
cmd = f"{gamescope_cmd} -- portprotonqt"
logger.info(f"Executing: {cmd}")
subprocess.Popen(cmd, shell=True)
sys.exit(0)
if args.fullscreen:
logger.info("Launching in fullscreen mode due to --fullscreen flag")
save_fullscreen_config(True)

View File

@ -13,4 +13,9 @@ def parse_args():
action="store_true",
help="Запустить приложение в полноэкранном режиме и сохранить эту настройку"
)
parser.add_argument(
"--session",
action="store_true",
help="Запустить приложение с использованием gamescope"
)
return parser.parse_args()

File diff suppressed because it is too large Load Diff

View File

@ -95,6 +95,7 @@ class AddGameDialog(QDialog):
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
layout = QFormLayout(self)
layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
# Game name
self.nameEdit = QLineEdit(self)

View File

@ -12,9 +12,42 @@ from collections.abc import Callable
from portprotonqt.localization import get_egs_language, _
from portprotonqt.logger import get_logger
from portprotonqt.image_utils import load_pixmap_async
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
from portprotonqt.config_utils import get_portproton_location
from portprotonqt.steam_api import (
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail
)
import vdf
import shutil
import zlib
from portprotonqt.downloader import Downloader
from PySide6.QtGui import QPixmap
logger = get_logger(__name__)
downloader = Downloader()
def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None:
"""Получает путь к исполняемому файлу EGS-игры из installed.json с использованием orjson."""
installed_json_path = os.path.join(legendary_config_path, "installed.json")
try:
with open(installed_json_path, "rb") as f:
installed_data = orjson.loads(f.read())
if app_name in installed_data:
install_path = installed_data[app_name].get("install_path", "").decode('utf-8') if isinstance(installed_data[app_name].get("install_path"), bytes) else installed_data[app_name].get("install_path", "")
executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "")
if install_path and executable:
return os.path.join(install_path, executable)
return None
except FileNotFoundError:
logger.error(f"installed.json not found at {installed_json_path}")
return None
except orjson.JSONDecodeError:
logger.error(f"Invalid JSON in {installed_json_path}")
return None
except Exception as e:
logger.error(f"Error reading installed.json: {e}")
return None
def get_cache_dir() -> Path:
"""Returns the path to the cache directory, creating it if necessary."""
@ -26,6 +59,237 @@ def get_cache_dir() -> Path:
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir
def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callback: Callable[[tuple[bool, str]], None]) -> None:
"""
Asynchronously adds an EGS game to Steam via shortcuts.vdf with PortProton tag.
Creates a launch script using legendary CLI with --no-wine and PortProton wrapper.
Wrapper is flatpak run if portproton_location is None or contains .var, otherwise start.sh.
Downloads Steam Grid covers if the game exists in Steam, and generates a thumbnail.
Calls the callback with (success, message).
"""
if not app_name or not app_name.strip() or not game_title or not game_title.strip():
logger.error("Invalid app_name or game_title: empty or whitespace")
callback((False, "Game name or app name is empty or invalid"))
return
if not os.path.exists(legendary_path):
logger.error(f"Legendary executable not found: {legendary_path}")
callback((False, f"Legendary executable not found: {legendary_path}"))
return
portproton_dir = get_portproton_location()
if not portproton_dir:
logger.error("PortProton directory not found")
callback((False, "PortProton directory not found"))
return
# Determine wrapper
wrapper = "flatpak run ru.linux_gaming.PortProton"
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
if portproton_dir is not None and ".var" not in portproton_dir:
wrapper = start_sh_path
if not os.path.exists(start_sh_path):
logger.error(f"start.sh not found at {start_sh_path}")
callback((False, f"start.sh not found at {start_sh_path}"))
return
# Create launch script
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
os.makedirs(steam_scripts_dir, exist_ok=True)
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_title.strip())
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}_egs.sh")
legendary_config_path = os.path.dirname(legendary_path)
script_content = f"""#!/usr/bin/env bash
export LD_PRELOAD=
export LEGENDARY_CONFIG_PATH="{legendary_config_path}"
"{legendary_path}" launch {app_name} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}" "$@"
"""
try:
with open(script_path, "w", encoding="utf-8") as f:
f.write(script_content)
os.chmod(script_path, 0o755)
logger.info(f"Created launch script for EGS game: {script_path}")
except Exception as e:
logger.error(f"Failed to create launch script {script_path}: {e}")
callback((False, f"Failed to create launch script: {e}"))
return
# Generate thumbnail
generated_icon_path = os.path.join(portproton_dir, "data", "img", f"{safe_game_name}_egs.png")
try:
img_dir = os.path.join(portproton_dir, "data", "img")
os.makedirs(img_dir, exist_ok=True)
game_exe = get_egs_executable(app_name, legendary_config_path)
if not game_exe or not os.path.exists(game_exe):
logger.warning(f"Executable not found for {app_name}, skipping thumbnail generation")
icon_path = ""
elif os.path.exists(generated_icon_path):
logger.info(f"Reusing existing thumbnail: {generated_icon_path}")
icon_path = generated_icon_path
else:
success = generate_thumbnail(game_exe, generated_icon_path, size=128, force_resize=True)
if not success or not os.path.exists(generated_icon_path):
logger.warning(f"generate_thumbnail failed for {game_exe}")
icon_path = ""
else:
logger.info(f"Generated thumbnail: {generated_icon_path}")
icon_path = generated_icon_path
except Exception as e:
logger.error(f"Error generating thumbnail for {app_name}: {e}")
icon_path = ""
# Get Steam directories
steam_home = get_steam_home()
if not steam_home:
logger.error("Steam home directory not found")
callback((False, "Steam directory not found"))
return
last_user = get_last_steam_user(steam_home)
if not last_user or 'SteamID' not in last_user:
logger.error("Failed to retrieve Steam user ID")
callback((False, "Failed to get Steam user ID"))
return
userdata_dir = steam_home / "userdata"
user_id = last_user['SteamID']
unsigned_id = convert_steam_id(user_id)
user_dir = userdata_dir / str(unsigned_id)
steam_shortcuts_path = user_dir / "config" / "shortcuts.vdf"
grid_dir = user_dir / "config" / "grid"
os.makedirs(grid_dir, exist_ok=True)
# Backup shortcuts.vdf
backup_path = f"{steam_shortcuts_path}.backup"
if os.path.exists(steam_shortcuts_path):
try:
shutil.copy2(steam_shortcuts_path, backup_path)
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
except Exception as e:
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
callback((False, f"Failed to create backup of shortcuts.vdf: {e}"))
return
# Generate unique appid
unique_string = f"{script_path}{game_title}"
baseid = zlib.crc32(unique_string.encode('utf-8')) & 0xffffffff
appid = baseid | 0x80000000
if appid > 0x7FFFFFFF:
aidvdf = appid - 0x100000000
else:
aidvdf = appid
steam_appid = None
downloaded_count = 0
total_covers = 4
download_lock = threading.Lock()
def on_cover_download(cover_file: str, cover_type: str):
nonlocal downloaded_count
try:
if cover_file and os.path.exists(cover_file):
logger.info(f"Downloaded cover {cover_type} to {cover_file}")
else:
logger.warning(f"Failed to download cover {cover_type} for appid {steam_appid}")
except Exception as e:
logger.error(f"Error processing cover {cover_type} for appid {steam_appid}: {e}")
with download_lock:
downloaded_count += 1
if downloaded_count == total_covers:
finalize_shortcut()
def finalize_shortcut():
tags_dict = {'0': 'PortProton'}
shortcut = {
"appid": aidvdf,
"AppName": game_title,
"Exe": f'"{script_path}"',
"StartDir": f'"{os.path.dirname(script_path)}"',
"icon": icon_path,
"LaunchOptions": "",
"IsHidden": 0,
"AllowDesktopConfig": 1,
"AllowOverlay": 1,
"openvr": 0,
"Devkit": 0,
"DevkitGameID": "",
"LastPlayTime": 0,
"tags": tags_dict
}
logger.info(f"Shortcut entry for EGS game: {shortcut}")
try:
if not os.path.exists(steam_shortcuts_path):
os.makedirs(os.path.dirname(steam_shortcuts_path), exist_ok=True)
open(steam_shortcuts_path, 'wb').close()
try:
if os.path.getsize(steam_shortcuts_path) > 0:
with open(steam_shortcuts_path, 'rb') as f:
shortcuts_data = vdf.binary_load(f)
else:
shortcuts_data = {"shortcuts": {}}
except Exception as load_err:
logger.warning(f"Failed to load shortcuts.vdf, starting fresh: {load_err}")
shortcuts_data = {"shortcuts": {}}
shortcuts = shortcuts_data.get("shortcuts", {})
for _key, entry in shortcuts.items():
if entry.get("AppName") == game_title and entry.get("Exe") == f'"{script_path}"':
logger.info(f"EGS game '{game_title}' already exists in Steam shortcuts")
callback((False, f"Game '{game_title}' already exists in Steam"))
return
new_index = str(len(shortcuts))
shortcuts[new_index] = shortcut
with open(steam_shortcuts_path, 'wb') as f:
vdf.binary_dump({"shortcuts": shortcuts}, f)
except Exception as e:
logger.error(f"Failed to update shortcuts.vdf: {e}")
if os.path.exists(backup_path):
try:
shutil.copy2(backup_path, steam_shortcuts_path)
logger.info("Restored shortcuts.vdf from backup")
except Exception as restore_err:
logger.error(f"Failed to restore shortcuts.vdf: {restore_err}")
callback((False, f"Failed to update shortcuts.vdf: {e}"))
return
logger.info(f"EGS game '{game_title}' added to Steam")
callback((True, f"Game '{game_title}' added to Steam with covers"))
def on_steam_apps(steam_data: tuple[list, dict]):
nonlocal steam_appid
steam_apps, steam_apps_index = steam_data
matching_app = search_app(game_title, steam_apps_index)
steam_appid = matching_app.get("appid") if matching_app else None
if not steam_appid:
logger.info(f"No Steam appid found for EGS game {game_title}, skipping cover download")
finalize_shortcut()
return
cover_types = [
(".jpg", "header.jpg"),
("p.jpg", "library_600x900_2x.jpg"),
("_hero.jpg", "library_hero.jpg"),
("_logo.png", "logo.png")
]
for suffix, cover_type in cover_types:
cover_file = os.path.join(grid_dir, f"{appid}{suffix}")
cover_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{steam_appid}/{cover_type}"
downloader.download_async(
cover_url,
cover_file,
timeout=5,
callback=lambda result, cfile=cover_file, ctype=cover_type: on_cover_download(cfile, ctype)
)
get_steam_apps_and_index_async(on_steam_apps)
def get_egs_game_description_async(
app_name: str,
callback: Callable[[str], None],
@ -281,6 +545,7 @@ def get_egs_game_description_async(
thread = threading.Thread(target=fetch_description, daemon=True)
thread.start()
def run_legendary_list_async(legendary_path: str, callback: Callable[[list | None], None]):
"""
Асинхронно выполняет команду 'legendary list --json' и возвращает результат через callback.
@ -326,6 +591,8 @@ def run_legendary_list_async(legendary_path: str, callback: Callable[[list | Non
def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], None], downloader, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]):
"""
Асинхронно загружает Epic Games Store игры с использованием legendary CLI.
Читает статистику времени игры и последнего запуска из файла statistics.
Проверяет наличие игры в Steam для получения ProtonDB статуса.
"""
logger.debug("Starting to load Epic Games Store games")
games: list[tuple] = []
@ -334,6 +601,14 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
cache_file = cache_dir / "legendary_games.json"
cache_ttl = 3600 # Cache TTL in seconds (1 hour)
# Путь к файлу statistics
portproton_location = get_portproton_location()
if portproton_location is None:
logger.error("PortProton location is not set, cannot locate statistics file")
statistics_file = ""
else:
statistics_file = os.path.join(portproton_location, "data", "tmp", "statistics")
if not os.path.exists(legendary_path):
logger.info("Legendary binary not found, downloading...")
def on_legendary_downloaded(result):
@ -345,7 +620,7 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
logger.error(f"Failed to make legendary binary executable: {e}")
callback(games) # Return empty games list on failure
return
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message)
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file)
else:
logger.error("Failed to download legendary binary")
callback(games) # Return empty games list on failure
@ -356,9 +631,9 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
callback(games)
return
else:
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message)
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file)
def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]):
def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None], statistics_file: str):
"""
Продолжает процесс загрузки EGS игр, либо из кэша, либо через legendary CLI.
"""
@ -410,6 +685,33 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
callback(final_games)
return
# Получаем путь к .exe для извлечения имени
game_exe = get_egs_executable(app_name, os.path.dirname(legendary_path))
exe_name = ""
if game_exe:
exe_name = os.path.splitext(os.path.basename(game_exe))[0]
# Читаем статистику из файла statistics
playtime_seconds = 0
formatted_playtime = ""
last_launch = _("Never")
last_launch_timestamp = 0
if exe_name and os.path.exists(statistics_file):
try:
playtime_data = parse_playtime_file(statistics_file)
matching_key = next(
(key for key in playtime_data if os.path.basename(key).split('.')[0] == exe_name),
None
)
if matching_key:
playtime_seconds = playtime_data[matching_key]
formatted_playtime = format_playtime(playtime_seconds)
except Exception as e:
logger.error(f"Failed to parse playtime data for {app_name}: {e}")
if exe_name:
last_launch = get_last_launch(exe_name) or _("Never")
last_launch_timestamp = get_last_launch_timestamp(exe_name)
metadata_file = metadata_dir / f"{app_name}.json"
cover_url = ""
try:
@ -426,40 +728,54 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
image_folder = os.path.join(os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), "PortProtonQt", "images")
local_path = os.path.join(image_folder, f"{app_name}.jpg") if cover_url else ""
def on_description_fetched(api_description: str):
final_description = api_description or _("No description available")
def on_steam_apps(steam_data: tuple[list, dict]):
steam_apps, steam_apps_index = steam_data
matching_app = search_app(title, steam_apps_index)
steam_appid = matching_app.get("appid") if matching_app else None
def on_cover_loaded(pixmap: QPixmap):
from portprotonqt.steam_api import get_weanticheatyet_status_async
def on_anticheat_status(status: str):
nonlocal pending_images
with results_lock:
game_results[index] = (
title,
final_description,
local_path if os.path.exists(local_path) else "",
app_name,
f"legendary:launch:{app_name}",
"",
_("Never"),
"",
"",
status or "",
0,
0,
"epic"
)
pending_images -= 1
update_progress(total_games - pending_images)
if pending_images == 0:
final_games = [game_results[i] for i in sorted(game_results.keys())]
callback(final_games)
def on_protondb_tier(protondb_tier: str):
def on_description_fetched(api_description: str):
final_description = api_description or _("No description available")
get_weanticheatyet_status_async(title, on_anticheat_status)
def on_cover_loaded(pixmap: QPixmap):
def on_anticheat_status(status: str):
nonlocal pending_images
with results_lock:
game_results[index] = (
title,
final_description,
local_path if os.path.exists(local_path) else "",
app_name,
f"legendary:launch:{app_name}",
"",
last_launch, # Время последнего запуска
formatted_playtime, # Форматированное время игры
protondb_tier, # ProtonDB tier
status or "",
last_launch_timestamp, # Временная метка последнего запуска
playtime_seconds, # Время игры в секундах
"epic"
)
pending_images -= 1
update_progress(total_games - pending_images)
if pending_images == 0:
final_games = [game_results[i] for i in sorted(game_results.keys())]
callback(final_games)
load_pixmap_async(cover_url, 600, 900, on_cover_loaded, app_name=app_name)
get_weanticheatyet_status_async(title, on_anticheat_status)
get_egs_game_description_async(title, on_description_fetched)
load_pixmap_async(cover_url, 600, 900, on_cover_loaded, app_name=app_name)
get_egs_game_description_async(title, on_description_fetched)
if steam_appid:
logger.info(f"Found Steam appid {steam_appid} for EGS game {title}")
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
get_steam_apps_and_index_async(on_steam_apps)
max_workers = min(4, len(valid_games))
with ThreadPoolExecutor(max_workers=max_workers) as executor:

View File

@ -25,6 +25,8 @@ class GameCard(QFrame):
addToSteamRequested = Signal(str, str, str) # name, exec_line, cover_path
removeFromSteamRequested = Signal(str, str) # name, exec_line
openGameFolderRequested = Signal(str, str) # name, exec_line
hoverChanged = Signal(str, bool)
focusChanged = Signal(str, bool)
def __init__(self, name, description, cover_path, appid, controller_support, exec_line,
last_launch, formatted_playtime, protondb_tier, anticheat_status, last_launch_ts, playtime_seconds, game_source,
@ -66,14 +68,14 @@ class GameCard(QFrame):
self.setStyleSheet(self.theme.GAME_CARD_WINDOW_STYLE)
# Параметры анимации обводки
self._borderWidth = 2
self._gradientAngle = 0.0
self._borderWidth = self.theme.GAME_CARD_ANIMATION["default_border_width"]
self._gradientAngle = self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]
self._hovered = False
self._focused = False
# Анимации
self.thickness_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
self.thickness_anim.setDuration(300)
self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"])
self.gradient_anim = None
self.pulse_anim = None
@ -169,7 +171,7 @@ class GameCard(QFrame):
self.steamLabel.setVisible(self.steam_visible)
# Epic Games Store бейдж
egs_icon = self.theme_manager.get_icon("steam")
egs_icon = self.theme_manager.get_icon("epic_games")
self.egsLabel = ClickableLabel(
"Epic Games",
icon=egs_icon,
@ -447,10 +449,8 @@ class GameCard(QFrame):
if self._hovered or self._focused:
center = self.rect().center()
gradient = QConicalGradient(center, self._gradientAngle)
gradient.setColorAt(0, QColor("#00fff5"))
gradient.setColorAt(0.33, QColor("#FF5733"))
gradient.setColorAt(0.66, QColor("#9B59B6"))
gradient.setColorAt(1, QColor("#00fff5"))
for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
gradient.setColorAt(stop["position"], QColor(stop["color"]))
pen.setBrush(QBrush(gradient))
else:
pen.setColor(QColor(0, 0, 0, 0))
@ -467,22 +467,25 @@ class GameCard(QFrame):
if self.pulse_anim:
self.pulse_anim.stop()
self.pulse_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
self.pulse_anim.setDuration(800)
self.pulse_anim.setDuration(self.theme.GAME_CARD_ANIMATION["pulse_anim_duration"])
self.pulse_anim.setLoopCount(0)
self.pulse_anim.setKeyValueAt(0, 8)
self.pulse_anim.setKeyValueAt(0.5, 10)
self.pulse_anim.setKeyValueAt(1, 8)
self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"])
self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
self.pulse_anim.start()
def enterEvent(self, event):
self._hovered = True
self.hoverChanged.emit(self.name, True)
self.setFocus(Qt.FocusReason.MouseFocusReason)
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.OutBack))
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(8)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_border_width"])
self.thickness_anim.finished.connect(self.startPulseAnimation)
self._isPulseAnimationConnected = True
self.thickness_anim.start()
@ -490,9 +493,9 @@ class GameCard(QFrame):
if self.gradient_anim:
self.gradient_anim.stop()
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
self.gradient_anim.setDuration(3000)
self.gradient_anim.setStartValue(360)
self.gradient_anim.setEndValue(0)
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start()
@ -500,66 +503,71 @@ class GameCard(QFrame):
def leaveEvent(self, event):
self._hovered = False
if not self._focused: # Сохраняем анимацию, если есть фокус
self.hoverChanged.emit(self.name, False)
if not self._focused:
if self.gradient_anim:
self.gradient_anim.stop()
self.gradient_anim = None
if self.pulse_anim:
self.pulse_anim.stop()
self.pulse_anim = None
if self.thickness_anim:
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
self.thickness_anim.start()
super().leaveEvent(event)
def focusInEvent(self, event):
if not self._hovered:
self._focused = True
self.focusChanged.emit(self.name, True)
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False
if self.pulse_anim:
self.pulse_anim.stop()
self.pulse_anim = None
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.InBack))
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(2)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"])
self.thickness_anim.finished.connect(self.startPulseAnimation)
self._isPulseAnimationConnected = True
self.thickness_anim.start()
super().leaveEvent(event)
def focusInEvent(self, event):
self._focused = True
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.OutBack))
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(12)
self.thickness_anim.finished.connect(self.startPulseAnimation)
self._isPulseAnimationConnected = True
self.thickness_anim.start()
if self.gradient_anim:
self.gradient_anim.stop()
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
self.gradient_anim.setDuration(3000)
self.gradient_anim.setStartValue(360)
self.gradient_anim.setEndValue(0)
self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start()
if self.gradient_anim:
self.gradient_anim.stop()
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start()
super().focusInEvent(event)
def focusOutEvent(self, event):
self._focused = False
if not self._hovered: # Сохраняем анимацию, если есть наведение
self.focusChanged.emit(self.name, False)
if not self._hovered:
if self.gradient_anim:
self.gradient_anim.stop()
self.gradient_anim = None
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False
if self.pulse_anim:
self.pulse_anim.stop()
self.pulse_anim = None
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.InBack))
if self.thickness_anim:
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(2)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
self.thickness_anim.start()
super().focusOutEvent(event)
def mousePressEvent(self, event):

View File

@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-14 10:37+0500\n"
"POT-Creation-Date: 2025-06-22 18:23+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n"
@ -26,31 +26,70 @@ msgstr ""
msgid "Add to Favorites"
msgstr ""
msgid "Import to Legendary"
msgstr ""
msgid "Remove from Steam"
msgstr ""
msgid "Add to Steam"
msgstr ""
msgid "Open Game Folder"
msgstr ""
msgid "Remove from Desktop"
msgstr ""
msgid "Add to Desktop"
msgstr ""
msgid "Edit Shortcut"
msgstr ""
msgid "Delete from PortProton"
msgstr ""
msgid "Open Game Folder"
msgstr ""
msgid "Remove from Menu"
msgstr ""
msgid "Add to Menu"
msgstr ""
msgid "Remove from Steam"
msgid "Edit Shortcut"
msgstr ""
msgid "Add to Steam"
msgid "Delete from PortProton"
msgstr ""
msgid "Error"
msgstr ""
#, python-brace-format
msgid "Legendary executable not found at {0}"
msgstr ""
msgid "Success"
msgstr ""
#, python-brace-format
msgid "'{0}' was added to Steam. Please restart Steam for changes to take effect."
msgstr ""
#, python-brace-format
msgid "Executable file not found for game: {0}"
msgstr ""
#, python-brace-format
msgid "Opened folder for EGS game '{0}'"
msgstr ""
#, python-brace-format
msgid "Failed to open game folder: {0}"
msgstr ""
msgid "Select Game Installation Folder"
msgstr ""
msgid "No folder selected"
msgstr ""
#, python-brace-format
msgid "Importing '{0}' to Legendary..."
msgstr ""
#, python-brace-format
@ -61,14 +100,51 @@ msgstr ""
msgid "Removed '{0}' from favorites"
msgstr ""
msgid "Error"
msgstr ""
msgid "PortProton is not found."
msgstr ""
#, python-brace-format
msgid "No executable command found in .desktop for game: {0}"
msgid "start.sh not found at {0}"
msgstr ""
#, python-brace-format
msgid "Failed to create .desktop file: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to desktop"
msgstr ""
#, python-brace-format
msgid "Failed to add game to desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from Desktop: {{0}}"
msgstr ""
#, python-brace-format
msgid "Successfully removed game '{0}' from Desktop"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to menu"
msgstr ""
#, python-brace-format
msgid "Failed to add game '{0}' to menu: {1}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from menu: {{0}}"
msgstr ""
#, python-brace-format
msgid "Successfully removed game '{0}' from menu"
msgstr ""
#, python-brace-format
msgid "No executable command found in .desktop file for game: {0}"
msgstr ""
#, python-brace-format
@ -76,7 +152,7 @@ msgid "Failed to parse .desktop file for game: {0}"
msgstr ""
#, python-brace-format
msgid "Error reading .desktop file: {0}"
msgid "Failed to read .desktop file: {0}"
msgstr ""
#, python-brace-format
@ -105,7 +181,7 @@ msgid ""
msgstr ""
#, python-brace-format
msgid "Could not locate .desktop file for '{0}'"
msgid "Could not locate .desktop file for game: {0}"
msgstr ""
#, python-brace-format
@ -121,35 +197,7 @@ msgid "Failed to delete custom data: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to menu"
msgstr ""
#, python-brace-format
msgid "Failed to add game to menu: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game from menu: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' removed from menu"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to desktop"
msgstr ""
#, python-brace-format
msgid "Failed to add game to desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game from Desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' removed from Desktop"
msgid "Failed to add game '{0}' to desktop: {1}"
msgstr ""
msgid "Game name and executable path are required."
@ -174,25 +222,54 @@ msgstr ""
msgid "Failed to copy cover image: {0}"
msgstr ""
msgid "Restart Steam"
msgstr ""
msgid ""
"The game was added successfully.\n"
"Please restart Steam for changes to take effect."
msgstr ""
msgid ""
"The game was removed successfully.\n"
"Please restart Steam for changes to take effect."
#, python-brace-format
msgid "Failed to add game '{0}' to Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Opened folder for '{0}'"
msgid ""
"'{0}' was removed from Steam. Please restart Steam for changes to take "
"effect."
msgstr ""
msgid "PortProton directory not found"
msgstr ""
msgid "Steam directory not found"
msgstr ""
msgid "Failed to get Steam user ID"
msgstr ""
msgid "Steam shortcuts file not found"
msgstr ""
#, python-brace-format
msgid "Failed to open game folder: {0}"
msgid "Failed to create backup of shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to load shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' not found in Steam shortcuts"
msgstr ""
#, python-brace-format
msgid "Failed to update shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove EGS game '{0}' from Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Successfully opened folder for '{0}'"
msgstr ""
msgid "Edit Game"
@ -235,10 +312,10 @@ msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
msgid "No description available"
msgid "Never"
msgstr ""
msgid "Never"
msgid "No description available"
msgstr ""
msgid "Supported"
@ -382,6 +459,21 @@ msgstr ""
msgid "Gamepad haptic feedback:"
msgstr ""
msgid "Open Legendary Login"
msgstr ""
msgid "Legendary Authentication:"
msgstr ""
msgid "Enter Legendary Authorization Code"
msgstr ""
msgid "Authorization Code:"
msgstr ""
msgid "Submit Code"
msgstr ""
msgid "Save Settings"
msgstr ""
@ -397,6 +489,22 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
msgid "Please enter an authorization code"
msgstr ""
msgid "Successfully authenticated with Legendary"
msgstr ""
#, python-brace-format
msgid "Legendary authentication failed: {0}"
msgstr ""
msgid "Legendary executable not found"
msgstr ""
msgid "Unexpected error during authentication"
msgstr ""
msgid "Confirm Reset"
msgstr ""
@ -478,6 +586,20 @@ msgstr ""
msgid "Play"
msgstr ""
#, python-brace-format
msgid "Executable not found for EGS game: {0}"
msgstr ""
msgid "Cannot launch game while another game is running"
msgstr ""
msgid "Launching"
msgstr ""
#, python-brace-format
msgid "Failed to launch game: {0}"
msgstr ""
msgid "Invalid command format (native)"
msgstr ""
@ -488,12 +610,6 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid "Cannot launch game while another game is running"
msgstr ""
msgid "Launching"
msgstr ""
msgid "Reboot"
msgstr ""

View File

@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-14 10:37+0500\n"
"POT-Creation-Date: 2025-06-22 18:23+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n"
@ -26,31 +26,70 @@ msgstr ""
msgid "Add to Favorites"
msgstr ""
msgid "Import to Legendary"
msgstr ""
msgid "Remove from Steam"
msgstr ""
msgid "Add to Steam"
msgstr ""
msgid "Open Game Folder"
msgstr ""
msgid "Remove from Desktop"
msgstr ""
msgid "Add to Desktop"
msgstr ""
msgid "Edit Shortcut"
msgstr ""
msgid "Delete from PortProton"
msgstr ""
msgid "Open Game Folder"
msgstr ""
msgid "Remove from Menu"
msgstr ""
msgid "Add to Menu"
msgstr ""
msgid "Remove from Steam"
msgid "Edit Shortcut"
msgstr ""
msgid "Add to Steam"
msgid "Delete from PortProton"
msgstr ""
msgid "Error"
msgstr ""
#, python-brace-format
msgid "Legendary executable not found at {0}"
msgstr ""
msgid "Success"
msgstr ""
#, python-brace-format
msgid "'{0}' was added to Steam. Please restart Steam for changes to take effect."
msgstr ""
#, python-brace-format
msgid "Executable file not found for game: {0}"
msgstr ""
#, python-brace-format
msgid "Opened folder for EGS game '{0}'"
msgstr ""
#, python-brace-format
msgid "Failed to open game folder: {0}"
msgstr ""
msgid "Select Game Installation Folder"
msgstr ""
msgid "No folder selected"
msgstr ""
#, python-brace-format
msgid "Importing '{0}' to Legendary..."
msgstr ""
#, python-brace-format
@ -61,14 +100,51 @@ msgstr ""
msgid "Removed '{0}' from favorites"
msgstr ""
msgid "Error"
msgstr ""
msgid "PortProton is not found."
msgstr ""
#, python-brace-format
msgid "No executable command found in .desktop for game: {0}"
msgid "start.sh not found at {0}"
msgstr ""
#, python-brace-format
msgid "Failed to create .desktop file: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to desktop"
msgstr ""
#, python-brace-format
msgid "Failed to add game to desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from Desktop: {{0}}"
msgstr ""
#, python-brace-format
msgid "Successfully removed game '{0}' from Desktop"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to menu"
msgstr ""
#, python-brace-format
msgid "Failed to add game '{0}' to menu: {1}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from menu: {{0}}"
msgstr ""
#, python-brace-format
msgid "Successfully removed game '{0}' from menu"
msgstr ""
#, python-brace-format
msgid "No executable command found in .desktop file for game: {0}"
msgstr ""
#, python-brace-format
@ -76,7 +152,7 @@ msgid "Failed to parse .desktop file for game: {0}"
msgstr ""
#, python-brace-format
msgid "Error reading .desktop file: {0}"
msgid "Failed to read .desktop file: {0}"
msgstr ""
#, python-brace-format
@ -105,7 +181,7 @@ msgid ""
msgstr ""
#, python-brace-format
msgid "Could not locate .desktop file for '{0}'"
msgid "Could not locate .desktop file for game: {0}"
msgstr ""
#, python-brace-format
@ -121,35 +197,7 @@ msgid "Failed to delete custom data: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to menu"
msgstr ""
#, python-brace-format
msgid "Failed to add game to menu: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game from menu: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' removed from menu"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to desktop"
msgstr ""
#, python-brace-format
msgid "Failed to add game to desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game from Desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' removed from Desktop"
msgid "Failed to add game '{0}' to desktop: {1}"
msgstr ""
msgid "Game name and executable path are required."
@ -174,25 +222,54 @@ msgstr ""
msgid "Failed to copy cover image: {0}"
msgstr ""
msgid "Restart Steam"
msgstr ""
msgid ""
"The game was added successfully.\n"
"Please restart Steam for changes to take effect."
msgstr ""
msgid ""
"The game was removed successfully.\n"
"Please restart Steam for changes to take effect."
#, python-brace-format
msgid "Failed to add game '{0}' to Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Opened folder for '{0}'"
msgid ""
"'{0}' was removed from Steam. Please restart Steam for changes to take "
"effect."
msgstr ""
msgid "PortProton directory not found"
msgstr ""
msgid "Steam directory not found"
msgstr ""
msgid "Failed to get Steam user ID"
msgstr ""
msgid "Steam shortcuts file not found"
msgstr ""
#, python-brace-format
msgid "Failed to open game folder: {0}"
msgid "Failed to create backup of shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to load shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' not found in Steam shortcuts"
msgstr ""
#, python-brace-format
msgid "Failed to update shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove EGS game '{0}' from Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Successfully opened folder for '{0}'"
msgstr ""
msgid "Edit Game"
@ -235,10 +312,10 @@ msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
msgid "No description available"
msgid "Never"
msgstr ""
msgid "Never"
msgid "No description available"
msgstr ""
msgid "Supported"
@ -382,6 +459,21 @@ msgstr ""
msgid "Gamepad haptic feedback:"
msgstr ""
msgid "Open Legendary Login"
msgstr ""
msgid "Legendary Authentication:"
msgstr ""
msgid "Enter Legendary Authorization Code"
msgstr ""
msgid "Authorization Code:"
msgstr ""
msgid "Submit Code"
msgstr ""
msgid "Save Settings"
msgstr ""
@ -397,6 +489,22 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
msgid "Please enter an authorization code"
msgstr ""
msgid "Successfully authenticated with Legendary"
msgstr ""
#, python-brace-format
msgid "Legendary authentication failed: {0}"
msgstr ""
msgid "Legendary executable not found"
msgstr ""
msgid "Unexpected error during authentication"
msgstr ""
msgid "Confirm Reset"
msgstr ""
@ -478,6 +586,20 @@ msgstr ""
msgid "Play"
msgstr ""
#, python-brace-format
msgid "Executable not found for EGS game: {0}"
msgstr ""
msgid "Cannot launch game while another game is running"
msgstr ""
msgid "Launching"
msgstr ""
#, python-brace-format
msgid "Failed to launch game: {0}"
msgstr ""
msgid "Invalid command format (native)"
msgstr ""
@ -488,12 +610,6 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid "Cannot launch game while another game is running"
msgstr ""
msgid "Launching"
msgstr ""
msgid "Reboot"
msgstr ""

View File

@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-14 10:37+0500\n"
"POT-Creation-Date: 2025-06-22 18:23+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -24,31 +24,70 @@ msgstr ""
msgid "Add to Favorites"
msgstr ""
msgid "Import to Legendary"
msgstr ""
msgid "Remove from Steam"
msgstr ""
msgid "Add to Steam"
msgstr ""
msgid "Open Game Folder"
msgstr ""
msgid "Remove from Desktop"
msgstr ""
msgid "Add to Desktop"
msgstr ""
msgid "Edit Shortcut"
msgstr ""
msgid "Delete from PortProton"
msgstr ""
msgid "Open Game Folder"
msgstr ""
msgid "Remove from Menu"
msgstr ""
msgid "Add to Menu"
msgstr ""
msgid "Remove from Steam"
msgid "Edit Shortcut"
msgstr ""
msgid "Add to Steam"
msgid "Delete from PortProton"
msgstr ""
msgid "Error"
msgstr ""
#, python-brace-format
msgid "Legendary executable not found at {0}"
msgstr ""
msgid "Success"
msgstr ""
#, python-brace-format
msgid "'{0}' was added to Steam. Please restart Steam for changes to take effect."
msgstr ""
#, python-brace-format
msgid "Executable file not found for game: {0}"
msgstr ""
#, python-brace-format
msgid "Opened folder for EGS game '{0}'"
msgstr ""
#, python-brace-format
msgid "Failed to open game folder: {0}"
msgstr ""
msgid "Select Game Installation Folder"
msgstr ""
msgid "No folder selected"
msgstr ""
#, python-brace-format
msgid "Importing '{0}' to Legendary..."
msgstr ""
#, python-brace-format
@ -59,14 +98,51 @@ msgstr ""
msgid "Removed '{0}' from favorites"
msgstr ""
msgid "Error"
msgstr ""
msgid "PortProton is not found."
msgstr ""
#, python-brace-format
msgid "No executable command found in .desktop for game: {0}"
msgid "start.sh not found at {0}"
msgstr ""
#, python-brace-format
msgid "Failed to create .desktop file: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to desktop"
msgstr ""
#, python-brace-format
msgid "Failed to add game to desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from Desktop: {{0}}"
msgstr ""
#, python-brace-format
msgid "Successfully removed game '{0}' from Desktop"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to menu"
msgstr ""
#, python-brace-format
msgid "Failed to add game '{0}' to menu: {1}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from menu: {{0}}"
msgstr ""
#, python-brace-format
msgid "Successfully removed game '{0}' from menu"
msgstr ""
#, python-brace-format
msgid "No executable command found in .desktop file for game: {0}"
msgstr ""
#, python-brace-format
@ -74,7 +150,7 @@ msgid "Failed to parse .desktop file for game: {0}"
msgstr ""
#, python-brace-format
msgid "Error reading .desktop file: {0}"
msgid "Failed to read .desktop file: {0}"
msgstr ""
#, python-brace-format
@ -103,7 +179,7 @@ msgid ""
msgstr ""
#, python-brace-format
msgid "Could not locate .desktop file for '{0}'"
msgid "Could not locate .desktop file for game: {0}"
msgstr ""
#, python-brace-format
@ -119,35 +195,7 @@ msgid "Failed to delete custom data: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to menu"
msgstr ""
#, python-brace-format
msgid "Failed to add game to menu: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game from menu: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' removed from menu"
msgstr ""
#, python-brace-format
msgid "Game '{0}' added to desktop"
msgstr ""
#, python-brace-format
msgid "Failed to add game to desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game from Desktop: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' removed from Desktop"
msgid "Failed to add game '{0}' to desktop: {1}"
msgstr ""
msgid "Game name and executable path are required."
@ -172,25 +220,54 @@ msgstr ""
msgid "Failed to copy cover image: {0}"
msgstr ""
msgid "Restart Steam"
msgstr ""
msgid ""
"The game was added successfully.\n"
"Please restart Steam for changes to take effect."
msgstr ""
msgid ""
"The game was removed successfully.\n"
"Please restart Steam for changes to take effect."
#, python-brace-format
msgid "Failed to add game '{0}' to Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Opened folder for '{0}'"
msgid ""
"'{0}' was removed from Steam. Please restart Steam for changes to take "
"effect."
msgstr ""
msgid "PortProton directory not found"
msgstr ""
msgid "Steam directory not found"
msgstr ""
msgid "Failed to get Steam user ID"
msgstr ""
msgid "Steam shortcuts file not found"
msgstr ""
#, python-brace-format
msgid "Failed to open game folder: {0}"
msgid "Failed to create backup of shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to load shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Game '{0}' not found in Steam shortcuts"
msgstr ""
#, python-brace-format
msgid "Failed to update shortcuts.vdf: {0}"
msgstr ""
#, python-brace-format
msgid "Failed to remove EGS game '{0}' from Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Failed to remove game '{0}' from Steam: {1}"
msgstr ""
#, python-brace-format
msgid "Successfully opened folder for '{0}'"
msgstr ""
msgid "Edit Game"
@ -233,10 +310,10 @@ msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
msgid "No description available"
msgid "Never"
msgstr ""
msgid "Never"
msgid "No description available"
msgstr ""
msgid "Supported"
@ -380,6 +457,21 @@ msgstr ""
msgid "Gamepad haptic feedback:"
msgstr ""
msgid "Open Legendary Login"
msgstr ""
msgid "Legendary Authentication:"
msgstr ""
msgid "Enter Legendary Authorization Code"
msgstr ""
msgid "Authorization Code:"
msgstr ""
msgid "Submit Code"
msgstr ""
msgid "Save Settings"
msgstr ""
@ -395,6 +487,22 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
msgid "Please enter an authorization code"
msgstr ""
msgid "Successfully authenticated with Legendary"
msgstr ""
#, python-brace-format
msgid "Legendary authentication failed: {0}"
msgstr ""
msgid "Legendary executable not found"
msgstr ""
msgid "Unexpected error during authentication"
msgstr ""
msgid "Confirm Reset"
msgstr ""
@ -476,6 +584,20 @@ msgstr ""
msgid "Play"
msgstr ""
#, python-brace-format
msgid "Executable not found for EGS game: {0}"
msgstr ""
msgid "Cannot launch game while another game is running"
msgstr ""
msgid "Launching"
msgstr ""
#, python-brace-format
msgid "Failed to launch game: {0}"
msgstr ""
msgid "Invalid command format (native)"
msgstr ""
@ -486,12 +608,6 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid "Cannot launch game while another game is running"
msgstr ""
msgid "Launching"
msgstr ""
msgid "Reboot"
msgstr ""

View File

@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-14 10:37+0500\n"
"PO-Revision-Date: 2025-06-14 10:37+0500\n"
"POT-Creation-Date: 2025-06-22 18:23+0500\n"
"PO-Revision-Date: 2025-06-22 18:22+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
@ -27,32 +27,73 @@ msgstr "Удалить из Избранного"
msgid "Add to Favorites"
msgstr "Добавить в Избранное"
msgid "Import to Legendary"
msgstr "Импортировать игру"
msgid "Remove from Steam"
msgstr "Удалить из Steam"
msgid "Add to Steam"
msgstr "Добавить в Steam"
msgid "Open Game Folder"
msgstr "Открыть папку с игрой"
msgid "Remove from Desktop"
msgstr "Удалить с рабочего стола"
msgid "Add to Desktop"
msgstr "Добавить на рабочий стол"
msgid "Edit Shortcut"
msgstr "Редактировать"
msgid "Delete from PortProton"
msgstr "Удалить из PortProton"
msgid "Open Game Folder"
msgstr "Открыть папку с игрой"
msgid "Remove from Menu"
msgstr "Удалить из меню"
msgid "Add to Menu"
msgstr "Добавить в меню"
msgid "Remove from Steam"
msgstr "Удалить из Steam"
msgid "Edit Shortcut"
msgstr "Редактировать"
msgid "Add to Steam"
msgstr "Добавить в Steam"
msgid "Delete from PortProton"
msgstr "Удалить из PortProton"
msgid "Error"
msgstr "Ошибка"
#, python-brace-format
msgid "Legendary executable not found at {0}"
msgstr "Legendary не найден по пути {0}"
msgid "Success"
msgstr "Успешно"
#, python-brace-format
msgid "'{0}' was added to Steam. Please restart Steam for changes to take effect."
msgstr ""
"'{0}' был добавлен в Steam. Пожалуйста, перезапустите Steam, чтобы "
"изменения вступили в силу."
#, python-brace-format
msgid "Executable file not found for game: {0}"
msgstr "Не найден исполняемый файл для игры: {0}"
#, python-brace-format
msgid "Opened folder for EGS game '{0}'"
msgstr "Открытие папки для игры EGS '{0}'"
#, python-brace-format
msgid "Failed to open game folder: {0}"
msgstr "Не удалось открыть папку для игры: {0}"
msgid "Select Game Installation Folder"
msgstr "Выберите папку установки игры"
msgid "No folder selected"
msgstr "Не выбрана ни одна папка"
#, python-brace-format
msgid "Importing '{0}' to Legendary..."
msgstr "Игра '{0}' импортирована"
#, python-brace-format
msgid "Added '{0}' to favorites"
@ -62,23 +103,60 @@ msgstr "Добавление '{0}' в избранное"
msgid "Removed '{0}' from favorites"
msgstr "Удаление '{0}' из избранного"
msgid "Error"
msgstr "Ошибка"
msgid "PortProton is not found."
msgstr "PortProton не найден."
#, python-brace-format
msgid "No executable command found in .desktop for game: {0}"
msgstr "Не найдено ни одной исполняемой команды для игры: {0}"
msgid "start.sh not found at {0}"
msgstr "start.sh не найден по адресу {0}"
#, python-brace-format
msgid "Failed to create .desktop file: {0}"
msgstr "Не удалось создать файл .desktop: {0}"
#, python-brace-format
msgid "Game '{0}' added to desktop"
msgstr "Игра '{0}' добавлена на рабочий стол"
#, python-brace-format
msgid "Failed to add game to desktop: {0}"
msgstr "Не удалось добавить игру на рабочий стол: {0}"
#, python-brace-format
msgid "Failed to remove game '{0}' from Desktop: {{0}}"
msgstr "Не удалось удалить игру '{0}' с рабочего стола: {{0}}"
#, python-brace-format
msgid "Successfully removed game '{0}' from Desktop"
msgstr "Успешно удалена игра '{0}' с рабочего стола"
#, python-brace-format
msgid "Game '{0}' added to menu"
msgstr "Игра '{0}' добавлена в меню"
#, python-brace-format
msgid "Failed to add game '{0}' to menu: {1}"
msgstr "Не удалось добавить игру в меню: '{0}' в меню: {1}"
#, python-brace-format
msgid "Failed to remove game '{0}' from menu: {{0}}"
msgstr "Не удалось удалить игру '{0}' из меню: {{0}}"
#, python-brace-format
msgid "Successfully removed game '{0}' from menu"
msgstr "Успешно удалена игра '{0}' из меню"
#, python-brace-format
msgid "No executable command found in .desktop file for game: {0}"
msgstr "В файле .desktop для игры не найдено ни одной исполняемой команды: {0}"
#, python-brace-format
msgid "Failed to parse .desktop file for game: {0}"
msgstr "Не удалось удалить файл .desktop: {0}"
#, python-brace-format
msgid "Error reading .desktop file: {0}"
msgstr "Не удалось удалить файл .desktop: {0}"
msgid "Failed to read .desktop file: {0}"
msgstr "Не удалось прочитать файл .desktop: {0}"
#, python-brace-format
msgid ".desktop file not found for game: {0}"
@ -108,8 +186,8 @@ msgstr ""
".desktop и настраиваемых данных."
#, python-brace-format
msgid "Could not locate .desktop file for '{0}'"
msgstr "Не удалось найти файл .desktop для '{0}'"
msgid "Could not locate .desktop file for game: {0}"
msgstr "Не удалось найти файл .desktop для игры: {0}"
#, python-brace-format
msgid "Failed to delete .desktop file: {0}"
@ -124,36 +202,8 @@ msgid "Failed to delete custom data: {0}"
msgstr "Не удалось удалить настраиваемые данные: {0}"
#, python-brace-format
msgid "Game '{0}' added to menu"
msgstr "Игра '{0}' добавлена в меню"
#, python-brace-format
msgid "Failed to add game to menu: {0}"
msgstr "Не удалось добавить игру в меню: {0}"
#, python-brace-format
msgid "Failed to remove game from menu: {0}"
msgstr "Не удалось удалить игру из меню: {0}"
#, python-brace-format
msgid "Game '{0}' removed from menu"
msgstr "Игра '{0}' удалена из меню"
#, python-brace-format
msgid "Game '{0}' added to desktop"
msgstr "Игра '{0}' добавлена на рабочий стол"
#, python-brace-format
msgid "Failed to add game to desktop: {0}"
msgstr "Не удалось добавить игру на рабочий стол: {0}"
#, python-brace-format
msgid "Failed to remove game from Desktop: {0}"
msgstr "Не удалось удалить игру с рабочего стола: {0}"
#, python-brace-format
msgid "Game '{0}' removed from Desktop"
msgstr "Игра '{0}' удалена с рабочего стола"
msgid "Failed to add game '{0}' to desktop: {1}"
msgstr "Не удалось добавить игру '{0}' на рабочий стол: {1}"
msgid "Game name and executable path are required."
msgstr "Необходимо указать название игры и путь к исполняемому файлу."
@ -177,30 +227,57 @@ msgstr "Не удалось удалить файл .desktop: {0}"
msgid "Failed to copy cover image: {0}"
msgstr "Не удалось удалить игру из меню: {0}"
msgid "Restart Steam"
msgstr "Перезапустите Steam"
msgid ""
"The game was added successfully.\n"
"Please restart Steam for changes to take effect."
msgstr ""
"Игра была успешно добавлена.\n"
"Пожалуйста, перезапустите Steam, чтобы изменения вступили в силу."
msgid ""
"The game was removed successfully.\n"
"Please restart Steam for changes to take effect."
msgstr ""
"Игра была успешно удалена..\n"
"Пожалуйста, перезапустите Steam, чтобы изменения вступили в силу."
#, python-brace-format
msgid "Failed to add game '{0}' to Steam: {1}"
msgstr "Не удалось добавить игру '{0}' в Steam: {1}"
#, python-brace-format
msgid "Opened folder for '{0}'"
msgstr "Открытие папки для '{0}'"
msgid ""
"'{0}' was removed from Steam. Please restart Steam for changes to take "
"effect."
msgstr ""
"'{0}' был удалён из Steam. Пожалуйста, перезапустите Steam, чтобы "
"изменения вступили в силу."
msgid "PortProton directory not found"
msgstr "PortProton не найден."
msgid "Steam directory not found"
msgstr "Каталог Steam не найден"
msgid "Failed to get Steam user ID"
msgstr "Не удалось получить ID пользователя Steam"
msgid "Steam shortcuts file not found"
msgstr "Файл ярлыков Steam не найден"
#, python-brace-format
msgid "Failed to open game folder: {0}"
msgstr "Не удалось открыть папку для игры: {0}"
msgid "Failed to create backup of shortcuts.vdf: {0}"
msgstr "Не удалось создать резервную копию shortcuts.vdf: {0}"
#, python-brace-format
msgid "Failed to load shortcuts.vdf: {0}"
msgstr "Не удалось загрузить shortcuts.vdf: {0}"
#, python-brace-format
msgid "Game '{0}' not found in Steam shortcuts"
msgstr "Игра '{0}' не найдена в ярлыках Steam"
#, python-brace-format
msgid "Failed to update shortcuts.vdf: {0}"
msgstr "Не удалось обновить shortcuts.vdf: {0}"
#, python-brace-format
msgid "Failed to remove EGS game '{0}' from Steam: {1}"
msgstr "Не удалось удалить игру EGS '{0}' из Steam: {1}"
#, python-brace-format
msgid "Failed to remove game '{0}' from Steam: {1}"
msgstr "Не удалось удалить игру '{0}' из Steam: {1}"
#, python-brace-format
msgid "Successfully opened folder for '{0}'"
msgstr "Успешно открыта папка для '{0}'"
msgid "Edit Game"
msgstr "Редактировать игру"
@ -242,12 +319,12 @@ msgstr "Запустить игру \"{name}\" с помощью PortProton"
msgid "Loading Epic Games Store games..."
msgstr "Загрузка игр из Epic Games Store..."
msgid "No description available"
msgstr "Описание не найдено"
msgid "Never"
msgstr "Никогда"
msgid "No description available"
msgstr "Описание не найдено"
msgid "Supported"
msgstr "Поддерживается"
@ -389,6 +466,21 @@ msgstr "Тактильная отдача на геймпаде"
msgid "Gamepad haptic feedback:"
msgstr "Тактильная отдача на геймпаде:"
msgid "Open Legendary Login"
msgstr "Открыть браузер для входа в Legendary"
msgid "Legendary Authentication:"
msgstr "Авторизация в Legendary:"
msgid "Enter Legendary Authorization Code"
msgstr "Введите код авторизации Legendary"
msgid "Authorization Code:"
msgstr "Код авторизации;"
msgid "Submit Code"
msgstr "Отправить код"
msgid "Save Settings"
msgstr "Сохранить настройки"
@ -404,6 +496,22 @@ msgstr "Открытие страницы входа в Legendary в брауз
msgid "Failed to open Legendary login page"
msgstr "Не удалось открыть страницу входа в Legendary"
msgid "Please enter an authorization code"
msgstr "Пожалуйста, введите код авторизации"
msgid "Successfully authenticated with Legendary"
msgstr "Успешная аутентификация в Legendary"
#, python-brace-format
msgid "Legendary authentication failed: {0}"
msgstr "Не удалось выполнить аутентификацию Legendary: {0}"
msgid "Legendary executable not found"
msgstr "Не найден исполняемый файл Legendary"
msgid "Unexpected error during authentication"
msgstr "Неожиданная ошибка при аутентификации"
msgid "Confirm Reset"
msgstr "Подтвердите удаление"
@ -487,6 +595,20 @@ msgstr "Остановить"
msgid "Play"
msgstr "Играть"
#, python-brace-format
msgid "Executable not found for EGS game: {0}"
msgstr "Не найден исполняемый файл для игры EGS: {0}"
msgid "Cannot launch game while another game is running"
msgstr "Невозможно запустить игру пока запущена другая"
msgid "Launching"
msgstr "Идёт запуск"
#, python-brace-format
msgid "Failed to launch game: {0}"
msgstr "Не удалось запустить игру: {0}"
msgid "Invalid command format (native)"
msgstr "Неправильный формат команды (нативная версия)"
@ -497,12 +619,6 @@ msgstr "Неправильный формат команды (flatpak)"
msgid "File not found: {0}"
msgstr "Файл не найден: {0}"
msgid "Cannot launch game while another game is running"
msgstr "Невозможно запустить игру пока запущена другая"
msgid "Launching"
msgstr "Идёт запуск"
msgid "Reboot"
msgstr "Перезагрузить"

View File

@ -17,7 +17,7 @@ from portprotonqt.system_overlay import SystemOverlay
from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel
from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games
from portprotonqt.egs_api import load_egs_games_async
from portprotonqt.egs_api import load_egs_games_async, get_egs_executable
from portprotonqt.theme_manager import ThemeManager, load_theme_screenshots, load_logo
from portprotonqt.time_utils import save_last_launch, get_last_launch, parse_playtime_file, format_playtime, get_last_launch_timestamp, format_last_launch
from portprotonqt.config_utils import (
@ -40,6 +40,7 @@ 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__)
@ -52,38 +53,6 @@ class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.current_exec_line = None
self.currentDetailPage = None
self.current_play_button = None
self.pending_games = []
self.game_card_cache = {}
self.pending_images = {}
self.total_games = 0
self.games_load_timer = QTimer(self)
self.games_load_timer.setSingleShot(True)
self.games_load_timer.timeout.connect(self.finalize_game_loading)
self.games_loaded.connect(self.on_games_loaded)
self.current_add_game_dialog = None
# Добавляем таймер для дебаунсинга сохранения настроек
self.settingsDebounceTimer = QTimer(self)
self.settingsDebounceTimer.setSingleShot(True)
self.settingsDebounceTimer.setInterval(300) # 300 мс задержка
self.settingsDebounceTimer.timeout.connect(self.applySettingsDelayed)
read_time_config()
# Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQt/legendary_cache
self.legendary_config_path = os.path.join(
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
"PortProtonQt", "legendary_cache"
)
os.makedirs(self.legendary_config_path, exist_ok=True)
os.environ["LEGENDARY_CONFIG_PATH"] = self.legendary_config_path
self.legendary_path = os.path.join(self.legendary_config_path, "legendary")
self.downloader = Downloader(max_workers=4)
# Создаём менеджер тем и читаем, какая тема выбрана
self.theme_manager = ThemeManager()
selected_theme = read_theme_from_config()
@ -116,9 +85,47 @@ class MainWindow(QMainWindow):
self.updateGameGrid
)
QApplication.setStyle("Fusion")
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE)
self.setAcceptDrops(True)
self.current_exec_line = None
self.currentDetailPage = None
self.current_play_button = None
self.current_focused_card = None
self.pending_games = []
self.game_card_cache = {}
self.pending_images = {}
self.total_games = 0
self.games_load_timer = QTimer(self)
self.games_load_timer.setSingleShot(True)
self.games_load_timer.timeout.connect(self.finalize_game_loading)
self.games_loaded.connect(self.on_games_loaded)
self.current_add_game_dialog = None
self.current_hovered_card = None
# Добавляем таймер для дебаунсинга сохранения настроек
self.settingsDebounceTimer = QTimer(self)
self.settingsDebounceTimer.setSingleShot(True)
self.settingsDebounceTimer.setInterval(300) # 300 мс задержка
self.settingsDebounceTimer.timeout.connect(self.applySettingsDelayed)
read_time_config()
# Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQt/legendary_cache
self.legendary_config_path = os.path.join(
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
"PortProtonQt", "legendary_cache"
)
os.makedirs(self.legendary_config_path, exist_ok=True)
os.environ["LEGENDARY_CONFIG_PATH"] = self.legendary_config_path
self.legendary_path = os.path.join(self.legendary_config_path, "legendary")
self.downloader = Downloader(max_workers=4)
# Статус-бар
self.setStatusBar(QStatusBar(self))
self.statusBar().setStyleSheet(self.theme.STATUS_BAR_STYLE)
self.progress_bar = QProgressBar()
self.progress_bar.setStyleSheet(self.theme.PROGRESS_BAR_STYLE)
self.progress_bar.setMaximumWidth(200)
self.progress_bar.setTextVisible(True)
self.progress_bar.setVisible(False)
@ -199,8 +206,6 @@ class MainWindow(QMainWindow):
self.restore_state()
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE)
self.setStyleSheet(self.theme.MESSAGE_BOX_STYLE)
self.input_manager = InputManager(self)
QTimer.singleShot(0, self.loadGames)
@ -241,6 +246,65 @@ class MainWindow(QMainWindow):
self.updateGameGrid()
self.progress_bar.setVisible(False)
def _on_card_focused(self, game_name: str, is_focused: bool):
"""Обработчик сигнала focusChanged от GameCard."""
card_key = None
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_focused:
# Если карточка получила фокус
if self.current_hovered_card and self.current_hovered_card != card:
# Сбрасываем текущую hovered карточку
self.current_hovered_card._hovered = False
self.current_hovered_card.leaveEvent(None)
self.current_hovered_card = None
if self.current_focused_card and self.current_focused_card != card:
# Сбрасываем текущую focused карточку
self.current_focused_card._focused = False
self.current_focused_card.clearFocus()
self.current_focused_card = card
else:
# Если карточка потеряла фокус
if self.current_focused_card == card:
self.current_focused_card = None
def _on_card_hovered(self, game_name: str, is_hovered: bool):
"""Обработчик сигнала hoverChanged от GameCard."""
card_key = None
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_hovered:
# Если мышь наведена на карточку
if self.current_focused_card and self.current_focused_card != card:
# Сбрасываем текущую focused карточку
self.current_focused_card._focused = False
self.current_focused_card.clearFocus()
if self.current_hovered_card and self.current_hovered_card != card:
# Сбрасываем предыдущую hovered карточку
self.current_hovered_card._hovered = False
self.current_hovered_card.leaveEvent(None)
self.current_hovered_card = card
else:
# Если мышь покинула карточку
if self.current_hovered_card == card:
self.current_hovered_card = None
def loadGames(self):
display_filter = read_display_filter()
favorites = read_favorites()
@ -261,19 +325,25 @@ class MainWindow(QMainWindow):
self.update_status_message.emit
)
elif display_filter == "favorites":
def on_all_games(portproton_games, steam_games):
games = [game for game in portproton_games + steam_games if game[0] in favorites]
def on_all_games(portproton_games, steam_games, epic_games):
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
self.games_loaded.emit(games)
self._load_portproton_games_async(
lambda pg: self._load_steam_games_async(
lambda sg: on_all_games(pg, sg)
lambda sg: load_egs_games_async(
self.legendary_path,
lambda eg: on_all_games(pg, sg, eg),
self.downloader,
self.update_progress.emit,
self.update_status_message.emit
)
)
)
else:
def on_all_games(portproton_games, steam_games):
def on_all_games(portproton_games, steam_games, epic_games):
seen = set()
games = []
for game in portproton_games + steam_games:
for game in portproton_games + steam_games + epic_games:
# Уникальный ключ: имя + exec_line
key = (game[0], game[4])
if key not in seen:
@ -282,7 +352,13 @@ class MainWindow(QMainWindow):
self.games_loaded.emit(games)
self._load_portproton_games_async(
lambda pg: self._load_steam_games_async(
lambda sg: on_all_games(pg, sg)
lambda sg: load_egs_games_async(
self.legendary_path,
lambda eg: on_all_games(pg, sg, eg),
self.downloader,
self.update_progress.emit,
self.update_status_message.emit
)
)
)
return []
@ -681,6 +757,8 @@ class MainWindow(QMainWindow):
card_width=self.card_width,
context_menu_manager=self.context_menu_manager
)
card.hoverChanged.connect(self._on_card_hovered)
card.focusChanged.connect(self._on_card_focused)
# Подключаем сигналы контекстного меню
card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
@ -890,9 +968,11 @@ class MainWindow(QMainWindow):
formLayout = QFormLayout()
formLayout.setContentsMargins(0, 10, 0, 0)
formLayout.setSpacing(10)
formLayout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
# 1. Time detail_level
self.timeDetailCombo = QComboBox()
self.timeDetailCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.time_keys = ["detailed", "brief"]
self.time_labels = [_("detailed"), _("brief")]
self.timeDetailCombo.addItems(self.time_labels)
@ -911,6 +991,7 @@ class MainWindow(QMainWindow):
# 2. Games sort_method
self.gamesSortCombo = QComboBox()
self.gamesSortCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.sort_keys = ["last_launch", "playtime", "alphabetical", "favorites"]
self.sort_labels = [_("last launch"), _("playtime"), _("alphabetical"), _("favorites")]
self.gamesSortCombo.addItems(self.sort_labels)
@ -929,8 +1010,9 @@ class MainWindow(QMainWindow):
# 3. Games display_filter
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
self.filter_labels = [_("all"), "steam", "portproton", _("favorites")]
self.filter_labels = [_("all"), "steam", "portproton", _("favorites"), "epic games store"]
self.gamesDisplayCombo = QComboBox()
self.gamesDisplayCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.gamesDisplayCombo.addItems(self.filter_labels)
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
self.gamesDisplayCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
@ -1011,6 +1093,37 @@ class MainWindow(QMainWindow):
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
# 8. Legendary Authentication
self.legendaryAuthButton = AutoSizeButton(
_("Open Legendary Login"),
icon=self.theme_manager.get_icon("login")
)
self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.legendaryAuthButton.clicked.connect(self.openLegendaryLogin)
self.legendaryAuthTitle = QLabel(_("Legendary Authentication:"))
self.legendaryAuthTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
self.legendaryAuthTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
formLayout.addRow(self.legendaryAuthTitle, self.legendaryAuthButton)
self.legendaryCodeEdit = QLineEdit()
self.legendaryCodeEdit.setPlaceholderText(_("Enter Legendary Authorization Code"))
self.legendaryCodeEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
self.legendaryCodeEdit.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.legendaryCodeTitle = QLabel(_("Authorization Code:"))
self.legendaryCodeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
self.legendaryCodeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
formLayout.addRow(self.legendaryCodeTitle, self.legendaryCodeEdit)
self.submitCodeButton = AutoSizeButton(
_("Submit Code"),
icon=self.theme_manager.get_icon("save")
)
self.submitCodeButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.submitCodeButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.submitCodeButton.clicked.connect(self.submitLegendaryCode)
formLayout.addRow(QLabel(""), self.submitCodeButton)
layout.addLayout(formLayout)
# Кнопки
@ -1061,6 +1174,37 @@ class MainWindow(QMainWindow):
logger.error(f"Failed to open Legendary login page: {e}")
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
def submitLegendaryCode(self):
"""Submits the Legendary authorization code using the legendary CLI."""
auth_code = self.legendaryCodeEdit.text().strip()
if not auth_code:
QMessageBox.warning(self, _("Error"), _("Please enter an authorization code"))
return
try:
# Execute legendary auth command
result = subprocess.run(
[self.legendary_path, "auth", "--code", auth_code],
capture_output=True,
text=True,
check=True
)
logger.info("Legendary authentication successful: %s", result.stdout)
self.statusBar().showMessage(_("Successfully authenticated with Legendary"), 3000)
self.legendaryCodeEdit.clear()
# Reload Epic Games Store games after successful authentication
self.games = self.loadGames()
self.updateGameGrid()
except subprocess.CalledProcessError as e:
logger.error("Legendary authentication failed: %s", e.stderr)
self.statusBar().showMessage(_("Legendary authentication failed: {0}").format(e.stderr), 5000)
except FileNotFoundError:
logger.error("Legendary executable not found at %s", self.legendary_path)
self.statusBar().showMessage(_("Legendary executable not found"), 5000)
except Exception as e:
logger.error("Unexpected error during Legendary authentication: %s", str(e))
self.statusBar().showMessage(_("Unexpected error during authentication"), 5000)
def resetSettings(self):
"""Сбрасывает настройки и перезапускает приложение."""
reply = QMessageBox.question(
@ -1456,7 +1600,7 @@ class MainWindow(QMainWindow):
steamLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://steamcommunity.com/app/{appid}")))
# Epic Games Store бейдж
egs_icon = self.theme_manager.get_icon("steam")
egs_icon = self.theme_manager.get_icon("epic_games")
egsLabel = ClickableLabel(
"Epic Games",
icon=egs_icon,
@ -1753,11 +1897,110 @@ class MainWindow(QMainWindow):
self.target_exe = None
def toggleGame(self, exec_line, button=None):
# Обработка Steam-игр
if exec_line.startswith("steam://"):
url = QUrl(exec_line)
QDesktopServices.openUrl(url)
return
# Обработка EGS-игр
if exec_line.startswith("legendary:launch:"):
app_name = exec_line.split("legendary:launch:")[1]
# Получаем путь к .exe из installed.json
game_exe = get_egs_executable(app_name, self.legendary_config_path)
if not game_exe or not os.path.exists(game_exe):
QMessageBox.warning(self, _("Error"), _("Executable not found for EGS game: {0}").format(app_name))
return
current_exe = os.path.basename(game_exe)
if self.game_processes and self.target_exe is not None and self.target_exe != current_exe:
QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running"))
return
# Обновляем кнопку
update_button = button if button is not None else self.current_play_button
self.current_running_button = update_button
self.target_exe = current_exe
exe_name = os.path.splitext(current_exe)[0]
# Проверяем, запущена ли игра
if self.game_processes and self.target_exe == current_exe:
# Останавливаем игру
if hasattr(self, 'input_manager'):
self.input_manager.enable_gamepad_handling()
for proc in self.game_processes:
try:
parent = psutil.Process(proc.pid)
children = parent.children(recursive=True)
for child in children:
try:
child.terminate()
except psutil.NoSuchProcess:
pass
psutil.wait_procs(children, timeout=5)
for child in children:
if child.is_running():
child.kill()
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except psutil.NoSuchProcess:
pass
self.game_processes = []
if update_button:
update_button.setText(_("Play"))
icon = self.theme_manager.get_icon("play")
if isinstance(icon, str):
icon = QIcon(icon)
elif icon is None:
icon = QIcon()
update_button.setIcon(icon)
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater()
self.checkProcessTimer = None
self.current_running_button = None
self.target_exe = None
self._gameLaunched = False
else:
# Запускаем игру через PortProton
env_vars = os.environ.copy()
env_vars['START_FROM_STEAM'] = '1'
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
wrapper = "flatpak run ru.linux_gaming.PortProton"
if self.portproton_location is not None and ".var" not in self.portproton_location:
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
wrapper = start_sh
cmd = [wrapper, game_exe]
try:
process = subprocess.Popen(cmd, env=env_vars, shell=False, preexec_fn=os.setsid)
self.game_processes.append(process)
save_last_launch(exe_name, datetime.now())
if update_button:
update_button.setText(_("Launching"))
icon = self.theme_manager.get_icon("stop")
if isinstance(icon, str):
icon = QIcon(icon)
elif icon is None:
icon = QIcon()
update_button.setIcon(icon)
# Delay disabling gamepad handling
if hasattr(self, 'input_manager'):
QTimer.singleShot(200, self.input_manager.disable_gamepad_handling)
self.checkProcessTimer = QTimer(self)
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
self.checkProcessTimer.start(500)
except Exception as e:
logger.error(f"Failed to launch EGS game {app_name}: {e}")
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
return
# Обработка PortProton-игр
entry_exec_split = shlex.split(exec_line)
if entry_exec_split[0] == "env":
if len(entry_exec_split) < 3:
@ -1771,18 +2014,20 @@ class MainWindow(QMainWindow):
file_to_check = entry_exec_split[3]
else:
file_to_check = entry_exec_split[0]
if not os.path.exists(file_to_check):
QMessageBox.warning(self, _("Error"), _("File not found: {0}").format(file_to_check))
return
current_exe = os.path.basename(file_to_check)
current_exe = os.path.basename(file_to_check)
if self.game_processes and self.target_exe is not None and self.target_exe != current_exe:
QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running"))
return
# Обновляем кнопку
update_button = button if button is not None else self.current_play_button
# Если игра уже запущена для этого exe останавливаем её по нажатию кнопки
# Если игра уже запущена для этого exe останавливаем её
if self.game_processes and self.target_exe == current_exe:
if hasattr(self, 'input_manager'):
self.input_manager.enable_gamepad_handling()
@ -1835,6 +2080,15 @@ class MainWindow(QMainWindow):
env_vars['START_FROM_STEAM'] = '1'
elif entry_exec_split[0] == "flatpak":
env_vars['START_FROM_STEAM'] = '1'
return
# Запускаем игру
self.current_running_button = update_button
self.target_exe = current_exe
exe_name = os.path.splitext(current_exe)[0]
env_vars = os.environ.copy()
env_vars['START_FROM_STEAM'] = '1'
try:
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
self.game_processes.append(process)
save_last_launch(exe_name, datetime.now())
@ -1850,10 +2104,12 @@ class MainWindow(QMainWindow):
self.checkProcessTimer = QTimer(self)
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
self.checkProcessTimer.start(500)
except Exception as e:
logger.error(f"Failed to launch game {exe_name}: {e}")
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
def closeEvent(self, event):
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
# Завершаем все игровые процессы
for proc in self.game_processes:
try:
parent = psutil.Process(proc.pid)
@ -1894,4 +2150,5 @@ class MainWindow(QMainWindow):
self.checkProcessTimer.deleteLater()
self.checkProcessTimer = None
QApplication.quit()
event.accept()

View File

@ -20,8 +20,12 @@ class SystemOverlay(QDialog):
self.theme_manager = ThemeManager()
self.setStyleSheet(self.theme.OVERLAY_WINDOW_STYLE)
# Убираем рамку окна
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
# Make window stay on top and frameless
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.Dialog |
Qt.WindowType.WindowStaysOnTopHint
)
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)

View File

@ -8,6 +8,76 @@ current_theme_name = read_theme_from_config()
favoriteLabelSize = 48, 48
pixmapsScaledSize = 60, 60
GAME_CARD_ANIMATION = {
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
# Значение в пикселях.
"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,
# Длительность одного цикла пульсирующей анимации.
# Определяет, как быстро рамка "пульсирует" между min и max значениями.
# Значение в миллисекундах.
"pulse_anim_duration": 800,
# Длительность анимации вращения градиента.
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки.
# Значение в миллисекундах.
"gradient_anim_duration": 3000,
# Начальный угол градиента (в градусах).
# Определяет начальную точку вращения градиента при старте анимации.
"gradient_start_angle": 360,
# Конечный угол градиента (в градусах).
# Определяет конечную точку вращения градиента.
# Значение 0 означает полный поворот на 360 градусов.
"gradient_end_angle": 0,
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе).
# Влияет на "чувство" анимации (например, плавное ускорение или замедление).
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad").
"thickness_easing_curve": "OutBack",
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса).
# Влияет на "чувство" возврата к исходной ширине обводки.
"thickness_easing_curve_out": "InBack",
# Цвета градиента для анимированной обводки.
# Список словарей, где каждый словарь задает позицию (0.01.0) и цвет в формате hex.
# Влияет на внешний вид обводки при наведении или фокусе.
"gradient_colors": [
{"position": 0, "color": "#00fff5"}, # Начальный цвет (циан)
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
{"position": 0.66, "color": "#9B59B6"}, # Цвет на 66% (пурпурный)
{"position": 1, "color": "#00fff5"} # Конечный цвет (возвращение к циану)
]
}
# СТИЛЬ ШАПКИ ГЛАВНОГО ОКНА
MAIN_WINDOW_HEADER_STYLE = """
QFrame {

File diff suppressed because it is too large Load Diff

26
renovate.json Normal file
View File

@ -0,0 +1,26 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:best-practices"],
"rebaseWhen": "never",
"lockFileMaintenance": {
"enabled": true
},
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
},
{
"automerge": true,
"matchUpdateTypes": ["pin", "pinDigest"]
},
{
"enabled": false,
"matchFileNames": [".gitea/workflows/**.yaml", ".gitea/workflows/**.yml"]
},
{
"enabled": false,
"matchFileNames": [".python-version"]
}
]
}

795
uv.lock generated

File diff suppressed because it is too large Load Diff