Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
329b7f1038
|
|||
fea07e19fe
|
|||
37b108f689
|
|||
78f5118709 | |||
1f14dd7fdf | |||
3d3bdd8f98 | |||
9d7c674544 | |||
e6c90508ab | |||
d0eea92139
|
|||
04726491c0
|
|||
bd1b7c07ae
|
|||
e6161d2e3f
|
|||
b82080600f
|
|||
05693514aa
|
|||
1c2835a933
|
|||
d229914fb6
|
|||
ce69a18249
|
|||
4d58830910 | |||
016ba537be
|
|||
6eeb93f6ba
|
|||
3f5d058740
|
|||
1a9228b76d
|
|||
e9e0bea854
|
|||
f7d9f5c150
|
|||
bcb5987d31
|
|||
b1aa987e4e
|
|||
f4c8b70bd0
|
|||
ff960df77c
|
|||
a57f509295
|
|||
32bbe89911
|
|||
593db00166
|
|||
79a78c785b
|
|||
0b92d058a9
|
|||
9df22edfc9
|
|||
4559231712
|
|||
18dbd42369
|
|||
76c0e607c5
|
|||
a91c9dacd8
|
.gitea/workflows
CHANGELOG.mdREADME.mdbuild-aux
config.jsportprotonqt
renovate.jsonuv.lock@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
fedora_version: [40, 41, 42, rawhide]
|
||||
fedora_version: [41, 42, rawhide]
|
||||
|
||||
container:
|
||||
image: fedora:${{ matrix.fedora_version }}
|
||||
|
@ -97,7 +97,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
fedora_version: [40, 41, 42, rawhide]
|
||||
fedora_version: [41, 42, rawhide]
|
||||
|
||||
container:
|
||||
image: fedora:${{ matrix.fedora_version }}
|
||||
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -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
|
||||
|
||||
---
|
||||
|
||||
> См. подробности по каждому коммиту в истории репозитория.
|
||||
|
10
README.md
10
README.md
@ -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)
|
||||
- [ ] Разобраться почему теряется часть стилей в Gamescope
|
||||
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
||||
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||
- [X] Получать описания и названия игр из базы данных Steam
|
||||
@ -41,7 +41,10 @@
|
||||
- [X] Добавить парсинг ярлыков из Steam
|
||||
- [X] Добавить парсинг ярлыков из EGS (скрыто для переработки)
|
||||
- [ ] Избавиться от бинарника legendary
|
||||
- [ ] Добавить запуск и скачивание игр из EGS
|
||||
- [X] Добавить запуск игр из EGS
|
||||
- [ ] Добавить скачивание игр из EGS
|
||||
- [ ] Добавить поддержку запуска сторонних игр из EGS
|
||||
- [ ] Добавить поддержку запуска игр с EOS
|
||||
- [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода
|
||||
- [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api)
|
||||
- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql)
|
||||
@ -68,6 +71,7 @@
|
||||
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||
- [ ] Доделать светлую тему
|
||||
- [ ] Добавить подсказки к управлению с геймпада
|
||||
- [ ] Добавить загрузку звуков в темы например для добавления звука запуска в тему и тд
|
||||
|
||||
### Установка (devel)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
8
config.js
Normal 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,
|
||||
};
|
@ -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)
|
||||
|
@ -13,4 +13,9 @@ def parse_args():
|
||||
action="store_true",
|
||||
help="Запустить приложение в полноэкранном режиме и сохранить эту настройку"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--session",
|
||||
action="store_true",
|
||||
help="Запустить приложение с использованием gamescope"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
@ -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):
|
||||
|
@ -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()
|
||||
@ -681,6 +745,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 +956,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 +979,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)
|
||||
@ -931,6 +1000,7 @@ class MainWindow(QMainWindow):
|
||||
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
|
||||
self.filter_labels = [_("all"), "steam", "portproton", _("favorites")]
|
||||
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)
|
||||
@ -1853,7 +1923,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
|
||||
# Завершаем все игровые процессы
|
||||
for proc in self.game_processes:
|
||||
try:
|
||||
parent = psutil.Process(proc.pid)
|
||||
@ -1894,4 +1963,5 @@ class MainWindow(QMainWindow):
|
||||
self.checkProcessTimer.deleteLater()
|
||||
self.checkProcessTimer = None
|
||||
|
||||
QApplication.quit()
|
||||
event.accept()
|
||||
|
@ -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)
|
||||
|
@ -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.0–1.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
26
renovate.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user