22 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
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
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
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 23:01:00 +05:00
05693514aa fix(renovate): uv lock file maintance
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 22:50:52 +05:00
1c2835a933 chore(deps): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 22:41:01 +05:00
d229914fb6 Revert "chore(deps): pin dependencies"
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 2025-06-20 16:52:33 +00:00
016ba537be fix(renovate): config syntax again
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:51:37 +05:00
6eeb93f6ba fix(renovate): config syntax
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:48:49 +05:00
3f5d058740 fix(renovate): RENOVATE_CONFIG_FILE
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:44:25 +05:00
1a9228b76d ci: added renovate auto update bot
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:37:14 +05:00
e9e0bea854 feat: stay overlay on top
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 19:31:33 +05:00
f7d9f5c150 chore(readme): update todo
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
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
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
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 22:58:57 +05:00
10 changed files with 578 additions and 396 deletions

View File

@@ -6,13 +6,17 @@
## [Unreleased] ## [Unreleased]
### Added ### Added
- Аргумент `--session` для запуска приложения в gamescope с GAMESCOPE_CMD
### Changed ### Changed
- Удалены сборки для Fedora 40 - Удалены сборки для Fedora 40
- Перенесены параметры анимации GameCard в `styles.py` с подробной документацией для поддержки кастомизации тем. - Перенесены параметры анимации GameCard в `styles.py` с подробной документацией для поддержки кастомизации тем.
- Статус выделения и наведения на карточки теперь взаимоисключают друг друга
### Fixed ### Fixed
- Дублирование обводки выделения карточек при быстром перемешении мыши - Дублирование обводки выделения карточек при быстром перемешении мыши
- Завершение приложения при закритие окна
- Использование системной палитры в темах
--- ---
@@ -77,6 +81,10 @@
- Корректная обработка событий геймпада во время игры - Корректная обработка событий геймпада во время игры
- Убийсво всех процессов "зомби" при закрытии программы - Убийсво всех процессов "зомби" при закрытии программы
### Contributors
- @Vector_null
- @Dervart
--- ---
## [0.1.1] 2025-05-17 ## [0.1.1] 2025-05-17
@@ -97,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)

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

@@ -26,6 +26,7 @@ class GameCard(QFrame):
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) 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,
@@ -476,6 +477,8 @@ class GameCard(QFrame):
def enterEvent(self, event): def enterEvent(self, event):
self._hovered = True self._hovered = True
self.hoverChanged.emit(self.name, 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)
@@ -520,42 +523,47 @@ class GameCard(QFrame):
super().leaveEvent(event) super().leaveEvent(event)
def focusInEvent(self, event): def focusInEvent(self, event):
self._focused = True if not self._hovered:
self.thickness_anim.stop() self._focused = True
if self._isPulseAnimationConnected: self.focusChanged.emit(self.name, True)
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()
if self.gradient_anim: self.thickness_anim.stop()
self.gradient_anim.stop() if self._isPulseAnimationConnected:
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle")) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"]) self._isPulseAnimationConnected = False
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]) self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"]) self.thickness_anim.setStartValue(self._borderWidth)
self.gradient_anim.setLoopCount(-1) self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"])
self.gradient_anim.start() 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(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) super().focusInEvent(event)
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
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
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.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(self.theme.GAME_CARD_ANIMATION["default_border_width"]) self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])

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 = {}
@@ -242,10 +244,39 @@ 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): def _on_card_hovered(self, game_name: str, is_hovered: bool):
"""Обработчик сигнала hoverChanged от GameCard.""" """Обработчик сигнала hoverChanged от GameCard."""
card_key = None card_key = None
# Находим ключ карточки по имени игры
for key, card in self.game_card_cache.items(): for key, card in self.game_card_cache.items():
if card.name == game_name: if card.name == game_name:
card_key = key card_key = key
@@ -258,10 +289,14 @@ class MainWindow(QMainWindow):
if is_hovered: 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: if self.current_hovered_card and self.current_hovered_card != card:
# Сбрасываем предыдущую выделенную карточку # Сбрасываем предыдущую hovered карточку
self.current_hovered_card._hovered = False self.current_hovered_card._hovered = False
self.current_hovered_card.leaveEvent(None) # Принудительно вызываем leaveEvent self.current_hovered_card.leaveEvent(None)
self.current_hovered_card = card self.current_hovered_card = card
else: else:
# Если мышь покинула карточку # Если мышь покинула карточку
@@ -709,6 +744,7 @@ class MainWindow(QMainWindow):
context_menu_manager=self.context_menu_manager context_menu_manager=self.context_menu_manager
) )
card.hoverChanged.connect(self._on_card_hovered) 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)
@@ -1881,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)
@@ -1922,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)

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