Compare commits
21 Commits
v0.1.2
...
4d58830910
Author | SHA1 | Date | |
---|---|---|---|
4d58830910 | |||
016ba537be
|
|||
6eeb93f6ba
|
|||
3f5d058740
|
|||
1a9228b76d
|
|||
e9e0bea854
|
|||
f7d9f5c150
|
|||
bcb5987d31
|
|||
b1aa987e4e
|
|||
f4c8b70bd0
|
|||
ff960df77c
|
|||
a57f509295
|
|||
32bbe89911
|
|||
593db00166
|
|||
79a78c785b
|
|||
0b92d058a9
|
|||
9df22edfc9
|
|||
4559231712
|
|||
18dbd42369
|
|||
76c0e607c5
|
|||
a91c9dacd8
|
@@ -12,7 +12,7 @@ jobs:
|
||||
name: Build AppImage
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
fedora_version: [40, 41, 42, rawhide]
|
||||
fedora_version: [41, 42, rawhide]
|
||||
|
||||
container:
|
||||
image: fedora:${{ matrix.fedora_version }}
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||
|
||||
- name: Checkout repo
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Copy fedora.spec
|
||||
run: |
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
name: Build Arch Package
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: archlinux:base-devel
|
||||
image: archlinux:base-devel@sha256:3f808d41e09261904a04709fc210a154ee56f6a9c369a15a70c2dfac5adab498
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Upload Arch package
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
|
@@ -18,7 +18,7 @@ jobs:
|
||||
name: Build AppImage
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Build Arch Package
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: archlinux:base-devel
|
||||
image: archlinux:base-devel@sha256:3f808d41e09261904a04709fc210a154ee56f6a9c369a15a70c2dfac5adab498
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
su user -c "yes '' | makepkg --noconfirm -s"
|
||||
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Upload Arch package
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
fedora_version: [40, 41, 42, rawhide]
|
||||
fedora_version: [41, 42, rawhide]
|
||||
|
||||
container:
|
||||
image: fedora:${{ matrix.fedora_version }}
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||
|
||||
- name: Checkout repo
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Copy fedora.spec
|
||||
run: |
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
needs: [build-appimage, build-arch, build-fedora]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
sudo apt install -y original-awk unzip
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: https://gitea.com/actions/download-artifact@v3
|
||||
uses: https://gitea.com/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
|
||||
with:
|
||||
path: release/
|
||||
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
awk "/^## \\[$VERSION\\]/ {flag=1; next} /^## \\[/ || /^---/ {flag=0} flag" CHANGELOG.md > changelog.txt
|
||||
|
||||
- name: Release
|
||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||
uses: https://gitea.com/actions/gitea-release-action@f119011bd6c95ab53d2084ace7b539694ce9b252 # v1
|
||||
with:
|
||||
body_path: changelog.txt
|
||||
token: ${{ env.GITEA_TOKEN }}
|
||||
|
@@ -15,10 +15,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: https://gitea.com/actions/setup-python@v5
|
||||
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
|
@@ -20,7 +20,7 @@ jobs:
|
||||
name: Check code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install uv
|
||||
uses: https://github.com/astral-sh/setup-uv@v6
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
name: Build with uv
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install uv
|
||||
uses: https://github.com/astral-sh/setup-uv@v6
|
||||
|
@@ -11,10 +11,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: https://gitea.com/actions/setup-python@v5
|
||||
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
|
21
.gitea/workflows/renovate.yaml
Normal file
21
.gitea/workflows/renovate.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
name: renovate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "@daily"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:41.1.3
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- run: renovate
|
||||
env:
|
||||
RENOVATE_CONFIG_FILE: "/workspace/Boria138/PortProtonQt/config.js"
|
||||
LOG_LEVEL: "debug"
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
16
CHANGELOG.md
16
CHANGELOG.md
@@ -3,6 +3,22 @@
|
||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||
Формат основан на [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
|
||||
|
||||
### Added
|
||||
|
@@ -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)
|
||||
|
@@ -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()
|
||||
|
@@ -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,33 +503,37 @@ 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
|
||||
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.setStartValue(self._borderWidth)
|
||||
self.thickness_anim.setEndValue(2)
|
||||
self.thickness_anim.start()
|
||||
|
||||
super().leaveEvent(event)
|
||||
|
||||
def focusInEvent(self, event):
|
||||
self._focused = True
|
||||
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.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.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._isPulseAnimationConnected = True
|
||||
self.thickness_anim.start()
|
||||
@@ -534,9 +541,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()
|
||||
|
||||
@@ -544,22 +551,23 @@ class GameCard(QFrame):
|
||||
|
||||
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
|
||||
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
|
||||
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_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):
|
||||
|
@@ -56,6 +56,7 @@ class MainWindow(QMainWindow):
|
||||
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 = {}
|
||||
@@ -65,6 +66,7 @@ class MainWindow(QMainWindow):
|
||||
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)
|
||||
@@ -241,6 +243,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 +742,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)
|
||||
@@ -1853,7 +1916,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
|
||||
# Завершаем все игровые процессы
|
||||
for proc in self.game_processes:
|
||||
try:
|
||||
parent = psutil.Process(proc.pid)
|
||||
@@ -1894,4 +1956,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 {
|
||||
|
@@ -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"} # Конечный цвет (возвращение к циану)
|
||||
]
|
||||
}
|
||||
|
||||
CONTEXT_MENU_STYLE = """
|
||||
QMenu {
|
||||
background: #282a33;;
|
||||
|
19
renovate.json
Normal file
19
renovate.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:best-practices"
|
||||
],
|
||||
"rebaseWhen": "never",
|
||||
"packageRules": [
|
||||
{
|
||||
"automerge": true,
|
||||
"matchUpdateTypes": ["pin", "pinDigest"]
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"matchUpdateTypes": ["digest", "pinDigest", "pin"],
|
||||
"matchDepTypes": ["container"],
|
||||
"matchFileNames": [".gitea/workflows/**.yaml", ".gitra/workflows/**.yml"]
|
||||
}
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user