Compare commits
	
		
			38 Commits
		
	
	
		
			v0.1.2
			...
			329b7f1038
		
	
	| 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
	
				 | 
					
					
						
@@ -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