30 Commits

Author SHA1 Message Date
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
16 changed files with 813 additions and 450 deletions

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@
<p align="center">Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.</p> <p align="center">Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.</p>
</div> </div>
## В планах ## В планах
- [X] Адаптировать структуру проекта для поддержки инструментов сборки - [X] Адаптировать структуру проекта для поддержки инструментов сборки
@@ -15,7 +14,8 @@
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено) - [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
- [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)
- [ ] Разобраться почему теряется часть стилей в Gamescope
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800) - [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots) - [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
- [X] Получать описания и названия игр из базы данных Steam - [X] Получать описания и названия игр из базы данных Steam
@@ -41,7 +41,10 @@
- [X] Добавить парсинг ярлыков из Steam - [X] Добавить парсинг ярлыков из Steam
- [X] Добавить парсинг ярлыков из EGS (скрыто для переработки) - [X] Добавить парсинг ярлыков из EGS (скрыто для переработки)
- [ ] Избавиться от бинарника legendary - [ ] Избавиться от бинарника legendary
- [ ] Добавить запуск и скачивание игр из EGS - [X] Добавить запуск игр из EGS
- [ ] Добавить скачивание игр из EGS
- [ ] Добавить поддержку запуска сторонних игр из EGS
- [ ] Добавить поддержку запуска игр с EOS
- [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода - [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода
- [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api) - [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api)
- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql) - [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql)
@@ -68,6 +71,7 @@
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры - [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
- [ ] Доделать светлую тему - [ ] Доделать светлую тему
- [ ] Добавить подсказки к управлению с геймпада - [ ] Добавить подсказки к управлению с геймпада
- [ ] Добавить загрузку звуков в темы например для добавления звука запуска в тему и тд
### Установка (devel) ### Установка (devel)

View File

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

View File

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

@@ -1,4 +1,6 @@
import sys import sys
import os
import subprocess
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
@@ -33,6 +35,13 @@ def main():
window = MainWindow() 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: if args.fullscreen:
logger.info("Launching in fullscreen mode due to --fullscreen flag") logger.info("Launching in fullscreen mode due to --fullscreen flag")
save_fullscreen_config(True) save_fullscreen_config(True)

View File

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

View File

@@ -25,6 +25,8 @@ class GameCard(QFrame):
addToSteamRequested = Signal(str, str, str) # name, exec_line, cover_path addToSteamRequested = Signal(str, str, str) # name, exec_line, cover_path
removeFromSteamRequested = Signal(str, str) # name, exec_line removeFromSteamRequested = Signal(str, str) # name, exec_line
openGameFolderRequested = 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, 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, 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.setStyleSheet(self.theme.GAME_CARD_WINDOW_STYLE)
# Параметры анимации обводки # Параметры анимации обводки
self._borderWidth = 2 self._borderWidth = self.theme.GAME_CARD_ANIMATION["default_border_width"]
self._gradientAngle = 0.0 self._gradientAngle = self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]
self._hovered = False self._hovered = False
self._focused = False self._focused = False
# Анимации # Анимации
self.thickness_anim = QPropertyAnimation(self, QByteArray(b"borderWidth")) 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.gradient_anim = None
self.pulse_anim = None self.pulse_anim = None
@@ -447,10 +449,8 @@ class GameCard(QFrame):
if self._hovered or self._focused: if self._hovered or self._focused:
center = self.rect().center() center = self.rect().center()
gradient = QConicalGradient(center, self._gradientAngle) gradient = QConicalGradient(center, self._gradientAngle)
gradient.setColorAt(0, QColor("#00fff5")) for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
gradient.setColorAt(0.33, QColor("#FF5733")) gradient.setColorAt(stop["position"], QColor(stop["color"]))
gradient.setColorAt(0.66, QColor("#9B59B6"))
gradient.setColorAt(1, QColor("#00fff5"))
pen.setBrush(QBrush(gradient)) pen.setBrush(QBrush(gradient))
else: else:
pen.setColor(QColor(0, 0, 0, 0)) pen.setColor(QColor(0, 0, 0, 0))
@@ -467,22 +467,25 @@ class GameCard(QFrame):
if self.pulse_anim: if self.pulse_anim:
self.pulse_anim.stop() self.pulse_anim.stop()
self.pulse_anim = QPropertyAnimation(self, QByteArray(b"borderWidth")) 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.setLoopCount(0)
self.pulse_anim.setKeyValueAt(0, 8) self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
self.pulse_anim.setKeyValueAt(0.5, 10) self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"])
self.pulse_anim.setKeyValueAt(1, 8) self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
self.pulse_anim.start() self.pulse_anim.start()
def enterEvent(self, event): def enterEvent(self, event):
self._hovered = True self._hovered = True
self.hoverChanged.emit(self.name, True)
self.setFocus(Qt.FocusReason.MouseFocusReason)
self.thickness_anim.stop() self.thickness_anim.stop()
if self._isPulseAnimationConnected: if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False 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.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.thickness_anim.finished.connect(self.startPulseAnimation)
self._isPulseAnimationConnected = True self._isPulseAnimationConnected = True
self.thickness_anim.start() self.thickness_anim.start()
@@ -490,9 +493,9 @@ class GameCard(QFrame):
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle")) self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
self.gradient_anim.setDuration(3000) self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
self.gradient_anim.setStartValue(360) self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
self.gradient_anim.setEndValue(0) self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
self.gradient_anim.setLoopCount(-1) self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start() self.gradient_anim.start()
@@ -500,33 +503,37 @@ class GameCard(QFrame):
def leaveEvent(self, event): def leaveEvent(self, event):
self._hovered = False self._hovered = False
if not self._focused: # Сохраняем анимацию, если есть фокус self.hoverChanged.emit(self.name, False)
if not self._focused:
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = None 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: if self.pulse_anim:
self.pulse_anim.stop() self.pulse_anim.stop()
self.pulse_anim = None self.pulse_anim = None
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.InBack)) if self.thickness_anim:
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(2)
self.thickness_anim.start()
super().leaveEvent(event)
def focusInEvent(self, event):
self._focused = True
self.thickness_anim.stop() self.thickness_anim.stop()
if self._isPulseAnimationConnected: if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False 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_out"]]))
self.thickness_anim.setStartValue(self._borderWidth) self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(12) 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
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(self.theme.GAME_CARD_ANIMATION["focus_border_width"])
self.thickness_anim.finished.connect(self.startPulseAnimation) self.thickness_anim.finished.connect(self.startPulseAnimation)
self._isPulseAnimationConnected = True self._isPulseAnimationConnected = True
self.thickness_anim.start() self.thickness_anim.start()
@@ -534,9 +541,9 @@ class GameCard(QFrame):
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle")) self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
self.gradient_anim.setDuration(3000) self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
self.gradient_anim.setStartValue(360) self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
self.gradient_anim.setEndValue(0) self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
self.gradient_anim.setLoopCount(-1) self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start() self.gradient_anim.start()
@@ -544,22 +551,23 @@ class GameCard(QFrame):
def focusOutEvent(self, event): def focusOutEvent(self, event):
self._focused = False self._focused = False
if not self._hovered: # Сохраняем анимацию, если есть наведение self.focusChanged.emit(self.name, False)
if not self._hovered:
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = None self.gradient_anim = None
if self.pulse_anim:
self.pulse_anim.stop()
self.pulse_anim = None
if self.thickness_anim:
self.thickness_anim.stop() self.thickness_anim.stop()
if self._isPulseAnimationConnected: if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False self._isPulseAnimationConnected = False
if self.pulse_anim: self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
self.pulse_anim.stop()
self.pulse_anim = None
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.InBack))
self.thickness_anim.setStartValue(self._borderWidth) 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() self.thickness_anim.start()
super().focusOutEvent(event) super().focusOutEvent(event)
def mousePressEvent(self, event): def mousePressEvent(self, event):

View File

@@ -52,10 +52,12 @@ class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
QApplication.setStyle("Fusion")
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.current_exec_line = None self.current_exec_line = None
self.currentDetailPage = None self.currentDetailPage = None
self.current_play_button = None self.current_play_button = None
self.current_focused_card = None
self.pending_games = [] self.pending_games = []
self.game_card_cache = {} self.game_card_cache = {}
self.pending_images = {} self.pending_images = {}
@@ -65,6 +67,7 @@ class MainWindow(QMainWindow):
self.games_load_timer.timeout.connect(self.finalize_game_loading) self.games_load_timer.timeout.connect(self.finalize_game_loading)
self.games_loaded.connect(self.on_games_loaded) self.games_loaded.connect(self.on_games_loaded)
self.current_add_game_dialog = None self.current_add_game_dialog = None
self.current_hovered_card = None
# Добавляем таймер для дебаунсинга сохранения настроек # Добавляем таймер для дебаунсинга сохранения настроек
self.settingsDebounceTimer = QTimer(self) self.settingsDebounceTimer = QTimer(self)
@@ -241,6 +244,65 @@ class MainWindow(QMainWindow):
self.updateGameGrid() self.updateGameGrid()
self.progress_bar.setVisible(False) 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): def loadGames(self):
display_filter = read_display_filter() display_filter = read_display_filter()
favorites = read_favorites() favorites = read_favorites()
@@ -681,6 +743,8 @@ class MainWindow(QMainWindow):
card_width=self.card_width, card_width=self.card_width,
context_menu_manager=self.context_menu_manager 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.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
card.deleteGameRequested.connect(self.context_menu_manager.delete_game) card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
@@ -1853,7 +1917,6 @@ class MainWindow(QMainWindow):
def closeEvent(self, event): def closeEvent(self, event):
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна.""" """Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
# Завершаем все игровые процессы
for proc in self.game_processes: for proc in self.game_processes:
try: try:
parent = psutil.Process(proc.pid) parent = psutil.Process(proc.pid)
@@ -1894,4 +1957,5 @@ class MainWindow(QMainWindow):
self.checkProcessTimer.deleteLater() self.checkProcessTimer.deleteLater()
self.checkProcessTimer = None self.checkProcessTimer = None
QApplication.quit()
event.accept() event.accept()

View File

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

View File

@@ -8,6 +8,76 @@ current_theme_name = read_theme_from_config()
favoriteLabelSize = 48, 48 favoriteLabelSize = 48, 48
pixmapsScaledSize = 60, 60 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 = """ MAIN_WINDOW_HEADER_STYLE = """
QFrame { QFrame {

View File

@@ -8,6 +8,76 @@ current_theme_name = read_theme_from_config()
favoriteLabelSize = 48, 48 favoriteLabelSize = 48, 48
pixmapsScaledSize = 60, 60 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"} # Конечный цвет (возвращение к циану)
]
}
CONTEXT_MENU_STYLE = """ CONTEXT_MENU_STYLE = """
QMenu { QMenu {
background: #282a33;; background: #282a33;;

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