Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6ea998535
|
|||
5a1ac2206b
|
|||
3f3e2f8d2c
|
|||
3600499d20
|
|||
5ade36a237
|
|||
96386f2815
|
@ -9,8 +9,6 @@
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Удалены сборки для Fedora 40
|
- Удалены сборки для Fedora 40
|
||||||
- Перенесены параметры анимации GameCard в `styles.py` с подробной документацией для поддержки кастомизации тем.
|
|
||||||
- Статус выделения и наведения на карточки теперь взаимоисключают друг друга
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Дублирование обводки выделения карточек при быстром перемешении мыши
|
- Дублирование обводки выделения карточек при быстром перемешении мыши
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
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
|
||||||
@ -35,13 +33,6 @@ 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)
|
||||||
|
@ -13,9 +13,4 @@ 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()
|
||||||
|
@ -3,7 +3,7 @@ import shlex
|
|||||||
import glob
|
import glob
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu
|
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog
|
||||||
from PySide6.QtCore import QUrl, QPoint
|
from PySide6.QtCore import QUrl, QPoint
|
||||||
from PySide6.QtGui import QDesktopServices
|
from PySide6.QtGui import QDesktopServices
|
||||||
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites
|
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites
|
||||||
@ -53,6 +53,12 @@ class ContextMenuManager:
|
|||||||
favorite_action = menu.addAction(_("Add to Favorites"))
|
favorite_action = menu.addAction(_("Add to Favorites"))
|
||||||
favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True))
|
favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True))
|
||||||
|
|
||||||
|
if game_card.game_source == "epic":
|
||||||
|
import_action = menu.addAction(_("Import to Legendary"))
|
||||||
|
import_action.triggered.connect(
|
||||||
|
lambda: self.import_to_legendary(game_card.name, game_card.appid)
|
||||||
|
)
|
||||||
|
|
||||||
if game_card.game_source not in ("steam", "epic"):
|
if game_card.game_source not in ("steam", "epic"):
|
||||||
desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip()
|
desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip()
|
||||||
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
|
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
|
||||||
@ -92,6 +98,75 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
menu.exec(game_card.mapToGlobal(pos))
|
menu.exec(game_card.mapToGlobal(pos))
|
||||||
|
|
||||||
|
def import_to_legendary(self, game_name, app_name):
|
||||||
|
"""
|
||||||
|
Imports an installed Epic Games Store game to Legendary using the provided app_name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
game_name: The display name of the game.
|
||||||
|
app_name: The Legendary app_name (unique identifier for the game).
|
||||||
|
"""
|
||||||
|
if not self._check_portproton():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Открываем диалог для выбора папки с установленной игрой
|
||||||
|
folder_path = QFileDialog.getExistingDirectory(
|
||||||
|
self.parent,
|
||||||
|
_("Select Game Installation Folder"),
|
||||||
|
os.path.expanduser("~")
|
||||||
|
)
|
||||||
|
if not folder_path:
|
||||||
|
self.parent.statusBar().showMessage(_("No folder selected"), 3000)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Путь к legendary
|
||||||
|
legendary_path = os.path.join(
|
||||||
|
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
|
||||||
|
"PortProtonQt", "legendary_cache", "legendary"
|
||||||
|
)
|
||||||
|
if not os.path.exists(legendary_path):
|
||||||
|
QMessageBox.warning(
|
||||||
|
self.parent,
|
||||||
|
_("Error"),
|
||||||
|
_("Legendary executable not found at {0}").format(legendary_path)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Формируем команду для импорта
|
||||||
|
cmd = [legendary_path, "import", app_name, folder_path]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Выполняем команду legendary import
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Successfully imported '{0}' to Legendary").format(game_name), 3000
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self.parent,
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to import '{0}' to Legendary: {1}").format(game_name, e.stderr)
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self.parent,
|
||||||
|
_("Error"),
|
||||||
|
_("Legendary executable not found")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self.parent,
|
||||||
|
_("Error"),
|
||||||
|
_("Unexpected error during import: {0}").format(str(e))
|
||||||
|
)
|
||||||
|
|
||||||
def toggle_favorite(self, game_card, add: bool):
|
def toggle_favorite(self, game_card, add: bool):
|
||||||
"""
|
"""
|
||||||
Toggle the favorite status of a game and update its icon.
|
Toggle the favorite status of a game and update its icon.
|
||||||
|
@ -12,10 +12,36 @@ from collections.abc import Callable
|
|||||||
from portprotonqt.localization import get_egs_language, _
|
from portprotonqt.localization import get_egs_language, _
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.image_utils import load_pixmap_async
|
from portprotonqt.image_utils import load_pixmap_async
|
||||||
|
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
|
||||||
|
from portprotonqt.config_utils import get_portproton_location
|
||||||
|
from portprotonqt.steam_api import get_weanticheatyet_status_async
|
||||||
|
|
||||||
from PySide6.QtGui import QPixmap
|
from PySide6.QtGui import QPixmap
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None:
|
||||||
|
"""Получает путь к исполняемому файлу EGS-игры из installed.json с использованием orjson."""
|
||||||
|
installed_json_path = os.path.join(legendary_config_path, "installed.json")
|
||||||
|
try:
|
||||||
|
with open(installed_json_path, "rb") as f:
|
||||||
|
installed_data = orjson.loads(f.read())
|
||||||
|
if app_name in installed_data:
|
||||||
|
install_path = installed_data[app_name].get("install_path", "").decode('utf-8') if isinstance(installed_data[app_name].get("install_path"), bytes) else installed_data[app_name].get("install_path", "")
|
||||||
|
executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "")
|
||||||
|
if install_path and executable:
|
||||||
|
return os.path.join(install_path, executable)
|
||||||
|
return None
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error(f"installed.json not found at {installed_json_path}")
|
||||||
|
return None
|
||||||
|
except orjson.JSONDecodeError:
|
||||||
|
logger.error(f"Invalid JSON in {installed_json_path}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading installed.json: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def get_cache_dir() -> Path:
|
def get_cache_dir() -> Path:
|
||||||
"""Returns the path to the cache directory, creating it if necessary."""
|
"""Returns the path to the cache directory, creating it if necessary."""
|
||||||
xdg_cache_home = os.getenv(
|
xdg_cache_home = os.getenv(
|
||||||
@ -281,6 +307,7 @@ def get_egs_game_description_async(
|
|||||||
|
|
||||||
thread = threading.Thread(target=fetch_description, daemon=True)
|
thread = threading.Thread(target=fetch_description, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def run_legendary_list_async(legendary_path: str, callback: Callable[[list | None], None]):
|
def run_legendary_list_async(legendary_path: str, callback: Callable[[list | None], None]):
|
||||||
"""
|
"""
|
||||||
Асинхронно выполняет команду 'legendary list --json' и возвращает результат через callback.
|
Асинхронно выполняет команду 'legendary list --json' и возвращает результат через callback.
|
||||||
@ -326,6 +353,7 @@ def run_legendary_list_async(legendary_path: str, callback: Callable[[list | Non
|
|||||||
def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], None], downloader, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]):
|
def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], None], downloader, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]):
|
||||||
"""
|
"""
|
||||||
Асинхронно загружает Epic Games Store игры с использованием legendary CLI.
|
Асинхронно загружает Epic Games Store игры с использованием legendary CLI.
|
||||||
|
Читает статистику времени игры и последнего запуска из файла statistics.
|
||||||
"""
|
"""
|
||||||
logger.debug("Starting to load Epic Games Store games")
|
logger.debug("Starting to load Epic Games Store games")
|
||||||
games: list[tuple] = []
|
games: list[tuple] = []
|
||||||
@ -334,6 +362,14 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
|
|||||||
cache_file = cache_dir / "legendary_games.json"
|
cache_file = cache_dir / "legendary_games.json"
|
||||||
cache_ttl = 3600 # Cache TTL in seconds (1 hour)
|
cache_ttl = 3600 # Cache TTL in seconds (1 hour)
|
||||||
|
|
||||||
|
# Путь к файлу statistics
|
||||||
|
portproton_location = get_portproton_location()
|
||||||
|
if portproton_location is None:
|
||||||
|
logger.error("PortProton location is not set, cannot locate statistics file")
|
||||||
|
statistics_file = ""
|
||||||
|
else:
|
||||||
|
statistics_file = os.path.join(portproton_location, "data", "tmp", "statistics")
|
||||||
|
|
||||||
if not os.path.exists(legendary_path):
|
if not os.path.exists(legendary_path):
|
||||||
logger.info("Legendary binary not found, downloading...")
|
logger.info("Legendary binary not found, downloading...")
|
||||||
def on_legendary_downloaded(result):
|
def on_legendary_downloaded(result):
|
||||||
@ -345,7 +381,7 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
|
|||||||
logger.error(f"Failed to make legendary binary executable: {e}")
|
logger.error(f"Failed to make legendary binary executable: {e}")
|
||||||
callback(games) # Return empty games list on failure
|
callback(games) # Return empty games list on failure
|
||||||
return
|
return
|
||||||
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message)
|
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file)
|
||||||
else:
|
else:
|
||||||
logger.error("Failed to download legendary binary")
|
logger.error("Failed to download legendary binary")
|
||||||
callback(games) # Return empty games list on failure
|
callback(games) # Return empty games list on failure
|
||||||
@ -356,9 +392,9 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
|
|||||||
callback(games)
|
callback(games)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message)
|
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file)
|
||||||
|
|
||||||
def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]):
|
def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None], statistics_file: str):
|
||||||
"""
|
"""
|
||||||
Продолжает процесс загрузки EGS игр, либо из кэша, либо через legendary CLI.
|
Продолжает процесс загрузки EGS игр, либо из кэша, либо через legendary CLI.
|
||||||
"""
|
"""
|
||||||
@ -410,6 +446,33 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
|
|||||||
callback(final_games)
|
callback(final_games)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Получаем путь к .exe для извлечения имени
|
||||||
|
game_exe = get_egs_executable(app_name, os.path.dirname(legendary_path))
|
||||||
|
exe_name = ""
|
||||||
|
if game_exe:
|
||||||
|
exe_name = os.path.splitext(os.path.basename(game_exe))[0]
|
||||||
|
|
||||||
|
# Читаем статистику из файла statistics
|
||||||
|
playtime_seconds = 0
|
||||||
|
formatted_playtime = ""
|
||||||
|
last_launch = _("Never")
|
||||||
|
last_launch_timestamp = 0
|
||||||
|
if exe_name and os.path.exists(statistics_file):
|
||||||
|
try:
|
||||||
|
playtime_data = parse_playtime_file(statistics_file)
|
||||||
|
matching_key = next(
|
||||||
|
(key for key in playtime_data if os.path.basename(key).split('.')[0] == exe_name),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if matching_key:
|
||||||
|
playtime_seconds = playtime_data[matching_key]
|
||||||
|
formatted_playtime = format_playtime(playtime_seconds)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to parse playtime data for {app_name}: {e}")
|
||||||
|
if exe_name:
|
||||||
|
last_launch = get_last_launch(exe_name) or _("Never")
|
||||||
|
last_launch_timestamp = get_last_launch_timestamp(exe_name)
|
||||||
|
|
||||||
metadata_file = metadata_dir / f"{app_name}.json"
|
metadata_file = metadata_dir / f"{app_name}.json"
|
||||||
cover_url = ""
|
cover_url = ""
|
||||||
try:
|
try:
|
||||||
@ -430,7 +493,6 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
|
|||||||
final_description = api_description or _("No description available")
|
final_description = api_description or _("No description available")
|
||||||
|
|
||||||
def on_cover_loaded(pixmap: QPixmap):
|
def on_cover_loaded(pixmap: QPixmap):
|
||||||
from portprotonqt.steam_api import get_weanticheatyet_status_async
|
|
||||||
def on_anticheat_status(status: str):
|
def on_anticheat_status(status: str):
|
||||||
nonlocal pending_images
|
nonlocal pending_images
|
||||||
with results_lock:
|
with results_lock:
|
||||||
@ -441,12 +503,12 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
|
|||||||
app_name,
|
app_name,
|
||||||
f"legendary:launch:{app_name}",
|
f"legendary:launch:{app_name}",
|
||||||
"",
|
"",
|
||||||
_("Never"),
|
last_launch, # Время последнего запуска
|
||||||
"",
|
formatted_playtime, # Форматированное время игры
|
||||||
"",
|
"",
|
||||||
status or "",
|
status or "",
|
||||||
0,
|
last_launch_timestamp, # Временная метка последнего запуска
|
||||||
0,
|
playtime_seconds, # Время игры в секундах
|
||||||
"epic"
|
"epic"
|
||||||
)
|
)
|
||||||
pending_images -= 1
|
pending_images -= 1
|
||||||
|
@ -26,7 +26,6 @@ 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,
|
||||||
@ -68,14 +67,14 @@ class GameCard(QFrame):
|
|||||||
self.setStyleSheet(self.theme.GAME_CARD_WINDOW_STYLE)
|
self.setStyleSheet(self.theme.GAME_CARD_WINDOW_STYLE)
|
||||||
|
|
||||||
# Параметры анимации обводки
|
# Параметры анимации обводки
|
||||||
self._borderWidth = self.theme.GAME_CARD_ANIMATION["default_border_width"]
|
self._borderWidth = 2
|
||||||
self._gradientAngle = self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]
|
self._gradientAngle = 0.0
|
||||||
self._hovered = False
|
self._hovered = False
|
||||||
self._focused = False
|
self._focused = False
|
||||||
|
|
||||||
# Анимации
|
# Анимации
|
||||||
self.thickness_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
|
self.thickness_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
|
||||||
self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"])
|
self.thickness_anim.setDuration(300)
|
||||||
self.gradient_anim = None
|
self.gradient_anim = None
|
||||||
self.pulse_anim = None
|
self.pulse_anim = None
|
||||||
|
|
||||||
@ -171,7 +170,7 @@ class GameCard(QFrame):
|
|||||||
self.steamLabel.setVisible(self.steam_visible)
|
self.steamLabel.setVisible(self.steam_visible)
|
||||||
|
|
||||||
# Epic Games Store бейдж
|
# Epic Games Store бейдж
|
||||||
egs_icon = self.theme_manager.get_icon("steam")
|
egs_icon = self.theme_manager.get_icon("epic_games")
|
||||||
self.egsLabel = ClickableLabel(
|
self.egsLabel = ClickableLabel(
|
||||||
"Epic Games",
|
"Epic Games",
|
||||||
icon=egs_icon,
|
icon=egs_icon,
|
||||||
@ -449,8 +448,10 @@ class GameCard(QFrame):
|
|||||||
if self._hovered or self._focused:
|
if self._hovered or self._focused:
|
||||||
center = self.rect().center()
|
center = self.rect().center()
|
||||||
gradient = QConicalGradient(center, self._gradientAngle)
|
gradient = QConicalGradient(center, self._gradientAngle)
|
||||||
for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
|
gradient.setColorAt(0, QColor("#00fff5"))
|
||||||
gradient.setColorAt(stop["position"], QColor(stop["color"]))
|
gradient.setColorAt(0.33, QColor("#FF5733"))
|
||||||
|
gradient.setColorAt(0.66, QColor("#9B59B6"))
|
||||||
|
gradient.setColorAt(1, QColor("#00fff5"))
|
||||||
pen.setBrush(QBrush(gradient))
|
pen.setBrush(QBrush(gradient))
|
||||||
else:
|
else:
|
||||||
pen.setColor(QColor(0, 0, 0, 0))
|
pen.setColor(QColor(0, 0, 0, 0))
|
||||||
@ -467,25 +468,23 @@ class GameCard(QFrame):
|
|||||||
if self.pulse_anim:
|
if self.pulse_anim:
|
||||||
self.pulse_anim.stop()
|
self.pulse_anim.stop()
|
||||||
self.pulse_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
|
self.pulse_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
|
||||||
self.pulse_anim.setDuration(self.theme.GAME_CARD_ANIMATION["pulse_anim_duration"])
|
self.pulse_anim.setDuration(800)
|
||||||
self.pulse_anim.setLoopCount(0)
|
self.pulse_anim.setLoopCount(0)
|
||||||
self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
|
self.pulse_anim.setKeyValueAt(0, 8)
|
||||||
self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"])
|
self.pulse_anim.setKeyValueAt(0.5, 10)
|
||||||
self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
|
self.pulse_anim.setKeyValueAt(1, 8)
|
||||||
self.pulse_anim.start()
|
self.pulse_anim.start()
|
||||||
|
|
||||||
def enterEvent(self, event):
|
def enterEvent(self, event):
|
||||||
self._hovered = True
|
self._hovered = True
|
||||||
self.hoverChanged.emit(self.name, True)
|
self.hoverChanged.emit(self.name, True)
|
||||||
self.setFocus(Qt.FocusReason.MouseFocusReason)
|
|
||||||
|
|
||||||
self.thickness_anim.stop()
|
self.thickness_anim.stop()
|
||||||
if self._isPulseAnimationConnected:
|
if self._isPulseAnimationConnected:
|
||||||
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
||||||
self._isPulseAnimationConnected = False
|
self._isPulseAnimationConnected = False
|
||||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
|
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.OutBack))
|
||||||
self.thickness_anim.setStartValue(self._borderWidth)
|
self.thickness_anim.setStartValue(self._borderWidth)
|
||||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_border_width"])
|
self.thickness_anim.setEndValue(8)
|
||||||
self.thickness_anim.finished.connect(self.startPulseAnimation)
|
self.thickness_anim.finished.connect(self.startPulseAnimation)
|
||||||
self._isPulseAnimationConnected = True
|
self._isPulseAnimationConnected = True
|
||||||
self.thickness_anim.start()
|
self.thickness_anim.start()
|
||||||
@ -493,9 +492,9 @@ class GameCard(QFrame):
|
|||||||
if self.gradient_anim:
|
if self.gradient_anim:
|
||||||
self.gradient_anim.stop()
|
self.gradient_anim.stop()
|
||||||
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
|
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
|
||||||
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
self.gradient_anim.setDuration(3000)
|
||||||
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
|
self.gradient_anim.setStartValue(360)
|
||||||
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
self.gradient_anim.setEndValue(0)
|
||||||
self.gradient_anim.setLoopCount(-1)
|
self.gradient_anim.setLoopCount(-1)
|
||||||
self.gradient_anim.start()
|
self.gradient_anim.start()
|
||||||
|
|
||||||
@ -516,24 +515,19 @@ class GameCard(QFrame):
|
|||||||
if self._isPulseAnimationConnected:
|
if self._isPulseAnimationConnected:
|
||||||
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
||||||
self._isPulseAnimationConnected = False
|
self._isPulseAnimationConnected = False
|
||||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
|
self.setBorderWidth(2)
|
||||||
self.thickness_anim.setStartValue(self._borderWidth)
|
self.update()
|
||||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
|
|
||||||
self.thickness_anim.start()
|
|
||||||
super().leaveEvent(event)
|
super().leaveEvent(event)
|
||||||
|
|
||||||
def focusInEvent(self, event):
|
def focusInEvent(self, event):
|
||||||
if not self._hovered:
|
|
||||||
self._focused = True
|
self._focused = True
|
||||||
self.focusChanged.emit(self.name, True)
|
|
||||||
|
|
||||||
self.thickness_anim.stop()
|
self.thickness_anim.stop()
|
||||||
if self._isPulseAnimationConnected:
|
if self._isPulseAnimationConnected:
|
||||||
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
||||||
self._isPulseAnimationConnected = False
|
self._isPulseAnimationConnected = False
|
||||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
|
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.OutBack))
|
||||||
self.thickness_anim.setStartValue(self._borderWidth)
|
self.thickness_anim.setStartValue(self._borderWidth)
|
||||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"])
|
self.thickness_anim.setEndValue(12)
|
||||||
self.thickness_anim.finished.connect(self.startPulseAnimation)
|
self.thickness_anim.finished.connect(self.startPulseAnimation)
|
||||||
self._isPulseAnimationConnected = True
|
self._isPulseAnimationConnected = True
|
||||||
self.thickness_anim.start()
|
self.thickness_anim.start()
|
||||||
@ -541,9 +535,9 @@ class GameCard(QFrame):
|
|||||||
if self.gradient_anim:
|
if self.gradient_anim:
|
||||||
self.gradient_anim.stop()
|
self.gradient_anim.stop()
|
||||||
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
|
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
|
||||||
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
self.gradient_anim.setDuration(3000)
|
||||||
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
|
self.gradient_anim.setStartValue(360)
|
||||||
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
self.gradient_anim.setEndValue(0)
|
||||||
self.gradient_anim.setLoopCount(-1)
|
self.gradient_anim.setLoopCount(-1)
|
||||||
self.gradient_anim.start()
|
self.gradient_anim.start()
|
||||||
|
|
||||||
@ -551,23 +545,22 @@ class GameCard(QFrame):
|
|||||||
|
|
||||||
def focusOutEvent(self, event):
|
def focusOutEvent(self, event):
|
||||||
self._focused = False
|
self._focused = False
|
||||||
self.focusChanged.emit(self.name, False)
|
if not self._hovered: # Сохраняем анимацию, если есть наведение
|
||||||
if not self._hovered:
|
|
||||||
if self.gradient_anim:
|
if self.gradient_anim:
|
||||||
self.gradient_anim.stop()
|
self.gradient_anim.stop()
|
||||||
self.gradient_anim = None
|
self.gradient_anim = None
|
||||||
if self.pulse_anim:
|
|
||||||
self.pulse_anim.stop()
|
|
||||||
self.pulse_anim = None
|
|
||||||
if self.thickness_anim:
|
|
||||||
self.thickness_anim.stop()
|
self.thickness_anim.stop()
|
||||||
if self._isPulseAnimationConnected:
|
if self._isPulseAnimationConnected:
|
||||||
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
||||||
self._isPulseAnimationConnected = False
|
self._isPulseAnimationConnected = False
|
||||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
|
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.setStartValue(self._borderWidth)
|
||||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
|
self.thickness_anim.setEndValue(2)
|
||||||
self.thickness_anim.start()
|
self.thickness_anim.start()
|
||||||
|
|
||||||
super().focusOutEvent(event)
|
super().focusOutEvent(event)
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
|
@ -17,7 +17,7 @@ from portprotonqt.system_overlay import SystemOverlay
|
|||||||
|
|
||||||
from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel
|
from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel
|
||||||
from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games
|
from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games
|
||||||
from portprotonqt.egs_api import load_egs_games_async
|
from portprotonqt.egs_api import load_egs_games_async, get_egs_executable
|
||||||
from portprotonqt.theme_manager import ThemeManager, load_theme_screenshots, load_logo
|
from portprotonqt.theme_manager import ThemeManager, load_theme_screenshots, load_logo
|
||||||
from portprotonqt.time_utils import save_last_launch, get_last_launch, parse_playtime_file, format_playtime, get_last_launch_timestamp, format_last_launch
|
from portprotonqt.time_utils import save_last_launch, get_last_launch, parse_playtime_file, format_playtime, get_last_launch_timestamp, format_last_launch
|
||||||
from portprotonqt.config_utils import (
|
from portprotonqt.config_utils import (
|
||||||
@ -56,7 +56,6 @@ class MainWindow(QMainWindow):
|
|||||||
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 = {}
|
||||||
@ -243,39 +242,10 @@ 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
|
||||||
@ -288,14 +258,10 @@ 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)
|
self.current_hovered_card.leaveEvent(None) # Принудительно вызываем leaveEvent
|
||||||
self.current_hovered_card = card
|
self.current_hovered_card = card
|
||||||
else:
|
else:
|
||||||
# Если мышь покинула карточку
|
# Если мышь покинула карточку
|
||||||
@ -322,19 +288,25 @@ class MainWindow(QMainWindow):
|
|||||||
self.update_status_message.emit
|
self.update_status_message.emit
|
||||||
)
|
)
|
||||||
elif display_filter == "favorites":
|
elif display_filter == "favorites":
|
||||||
def on_all_games(portproton_games, steam_games):
|
def on_all_games(portproton_games, steam_games, epic_games):
|
||||||
games = [game for game in portproton_games + steam_games if game[0] in favorites]
|
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
|
||||||
self.games_loaded.emit(games)
|
self.games_loaded.emit(games)
|
||||||
self._load_portproton_games_async(
|
self._load_portproton_games_async(
|
||||||
lambda pg: self._load_steam_games_async(
|
lambda pg: self._load_steam_games_async(
|
||||||
lambda sg: on_all_games(pg, sg)
|
lambda sg: load_egs_games_async(
|
||||||
|
self.legendary_path,
|
||||||
|
lambda eg: on_all_games(pg, sg, eg),
|
||||||
|
self.downloader,
|
||||||
|
self.update_progress.emit,
|
||||||
|
self.update_status_message.emit
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
def on_all_games(portproton_games, steam_games):
|
def on_all_games(portproton_games, steam_games, epic_games):
|
||||||
seen = set()
|
seen = set()
|
||||||
games = []
|
games = []
|
||||||
for game in portproton_games + steam_games:
|
for game in portproton_games + steam_games + epic_games:
|
||||||
# Уникальный ключ: имя + exec_line
|
# Уникальный ключ: имя + exec_line
|
||||||
key = (game[0], game[4])
|
key = (game[0], game[4])
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
@ -343,7 +315,13 @@ class MainWindow(QMainWindow):
|
|||||||
self.games_loaded.emit(games)
|
self.games_loaded.emit(games)
|
||||||
self._load_portproton_games_async(
|
self._load_portproton_games_async(
|
||||||
lambda pg: self._load_steam_games_async(
|
lambda pg: self._load_steam_games_async(
|
||||||
lambda sg: on_all_games(pg, sg)
|
lambda sg: load_egs_games_async(
|
||||||
|
self.legendary_path,
|
||||||
|
lambda eg: on_all_games(pg, sg, eg),
|
||||||
|
self.downloader,
|
||||||
|
self.update_progress.emit,
|
||||||
|
self.update_status_message.emit
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
@ -743,7 +721,6 @@ 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)
|
||||||
@ -992,7 +969,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# 3. Games display_filter
|
# 3. Games display_filter
|
||||||
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
|
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
|
||||||
self.filter_labels = [_("all"), "steam", "portproton", _("favorites")]
|
self.filter_labels = [_("all"), "steam", "portproton", _("favorites"), "epic games store"]
|
||||||
self.gamesDisplayCombo = QComboBox()
|
self.gamesDisplayCombo = QComboBox()
|
||||||
self.gamesDisplayCombo.addItems(self.filter_labels)
|
self.gamesDisplayCombo.addItems(self.filter_labels)
|
||||||
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||||
@ -1074,6 +1051,37 @@ class MainWindow(QMainWindow):
|
|||||||
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
||||||
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
||||||
|
|
||||||
|
# 8. Legendary Authentication
|
||||||
|
self.legendaryAuthButton = AutoSizeButton(
|
||||||
|
_("Open Legendary Login"),
|
||||||
|
icon=self.theme_manager.get_icon("login")
|
||||||
|
)
|
||||||
|
self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.legendaryAuthButton.clicked.connect(self.openLegendaryLogin)
|
||||||
|
self.legendaryAuthTitle = QLabel(_("Legendary Authentication:"))
|
||||||
|
self.legendaryAuthTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
|
self.legendaryAuthTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
|
formLayout.addRow(self.legendaryAuthTitle, self.legendaryAuthButton)
|
||||||
|
|
||||||
|
self.legendaryCodeEdit = QLineEdit()
|
||||||
|
self.legendaryCodeEdit.setPlaceholderText(_("Enter Legendary Authorization Code"))
|
||||||
|
self.legendaryCodeEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
|
||||||
|
self.legendaryCodeEdit.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.legendaryCodeTitle = QLabel(_("Authorization Code:"))
|
||||||
|
self.legendaryCodeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
|
self.legendaryCodeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
|
formLayout.addRow(self.legendaryCodeTitle, self.legendaryCodeEdit)
|
||||||
|
|
||||||
|
self.submitCodeButton = AutoSizeButton(
|
||||||
|
_("Submit Code"),
|
||||||
|
icon=self.theme_manager.get_icon("save")
|
||||||
|
)
|
||||||
|
self.submitCodeButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
self.submitCodeButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.submitCodeButton.clicked.connect(self.submitLegendaryCode)
|
||||||
|
formLayout.addRow(QLabel(""), self.submitCodeButton)
|
||||||
|
|
||||||
layout.addLayout(formLayout)
|
layout.addLayout(formLayout)
|
||||||
|
|
||||||
# Кнопки
|
# Кнопки
|
||||||
@ -1124,6 +1132,37 @@ class MainWindow(QMainWindow):
|
|||||||
logger.error(f"Failed to open Legendary login page: {e}")
|
logger.error(f"Failed to open Legendary login page: {e}")
|
||||||
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
|
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
|
||||||
|
|
||||||
|
def submitLegendaryCode(self):
|
||||||
|
"""Submits the Legendary authorization code using the legendary CLI."""
|
||||||
|
auth_code = self.legendaryCodeEdit.text().strip()
|
||||||
|
if not auth_code:
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Please enter an authorization code"))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute legendary auth command
|
||||||
|
result = subprocess.run(
|
||||||
|
[self.legendary_path, "auth", "--code", auth_code],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
logger.info("Legendary authentication successful: %s", result.stdout)
|
||||||
|
self.statusBar().showMessage(_("Successfully authenticated with Legendary"), 3000)
|
||||||
|
self.legendaryCodeEdit.clear()
|
||||||
|
# Reload Epic Games Store games after successful authentication
|
||||||
|
self.games = self.loadGames()
|
||||||
|
self.updateGameGrid()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error("Legendary authentication failed: %s", e.stderr)
|
||||||
|
self.statusBar().showMessage(_("Legendary authentication failed: {0}").format(e.stderr), 5000)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("Legendary executable not found at %s", self.legendary_path)
|
||||||
|
self.statusBar().showMessage(_("Legendary executable not found"), 5000)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Unexpected error during Legendary authentication: %s", str(e))
|
||||||
|
self.statusBar().showMessage(_("Unexpected error during authentication"), 5000)
|
||||||
|
|
||||||
def resetSettings(self):
|
def resetSettings(self):
|
||||||
"""Сбрасывает настройки и перезапускает приложение."""
|
"""Сбрасывает настройки и перезапускает приложение."""
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
@ -1519,7 +1558,7 @@ class MainWindow(QMainWindow):
|
|||||||
steamLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://steamcommunity.com/app/{appid}")))
|
steamLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://steamcommunity.com/app/{appid}")))
|
||||||
|
|
||||||
# Epic Games Store бейдж
|
# Epic Games Store бейдж
|
||||||
egs_icon = self.theme_manager.get_icon("steam")
|
egs_icon = self.theme_manager.get_icon("epic_games")
|
||||||
egsLabel = ClickableLabel(
|
egsLabel = ClickableLabel(
|
||||||
"Epic Games",
|
"Epic Games",
|
||||||
icon=egs_icon,
|
icon=egs_icon,
|
||||||
@ -1816,11 +1855,110 @@ class MainWindow(QMainWindow):
|
|||||||
self.target_exe = None
|
self.target_exe = None
|
||||||
|
|
||||||
def toggleGame(self, exec_line, button=None):
|
def toggleGame(self, exec_line, button=None):
|
||||||
|
# Обработка Steam-игр
|
||||||
if exec_line.startswith("steam://"):
|
if exec_line.startswith("steam://"):
|
||||||
url = QUrl(exec_line)
|
url = QUrl(exec_line)
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Обработка EGS-игр
|
||||||
|
if exec_line.startswith("legendary:launch:"):
|
||||||
|
app_name = exec_line.split("legendary:launch:")[1]
|
||||||
|
|
||||||
|
# Получаем путь к .exe из installed.json
|
||||||
|
game_exe = get_egs_executable(app_name, self.legendary_config_path)
|
||||||
|
if not game_exe or not os.path.exists(game_exe):
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Executable not found for EGS game: {0}").format(app_name))
|
||||||
|
return
|
||||||
|
|
||||||
|
current_exe = os.path.basename(game_exe)
|
||||||
|
if self.game_processes and self.target_exe is not None and self.target_exe != current_exe:
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обновляем кнопку
|
||||||
|
update_button = button if button is not None else self.current_play_button
|
||||||
|
self.current_running_button = update_button
|
||||||
|
self.target_exe = current_exe
|
||||||
|
exe_name = os.path.splitext(current_exe)[0]
|
||||||
|
|
||||||
|
# Проверяем, запущена ли игра
|
||||||
|
if self.game_processes and self.target_exe == current_exe:
|
||||||
|
# Останавливаем игру
|
||||||
|
if hasattr(self, 'input_manager'):
|
||||||
|
self.input_manager.enable_gamepad_handling()
|
||||||
|
|
||||||
|
for proc in self.game_processes:
|
||||||
|
try:
|
||||||
|
parent = psutil.Process(proc.pid)
|
||||||
|
children = parent.children(recursive=True)
|
||||||
|
for child in children:
|
||||||
|
try:
|
||||||
|
child.terminate()
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
psutil.wait_procs(children, timeout=5)
|
||||||
|
for child in children:
|
||||||
|
if child.is_running():
|
||||||
|
child.kill()
|
||||||
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
self.game_processes = []
|
||||||
|
if update_button:
|
||||||
|
update_button.setText(_("Play"))
|
||||||
|
icon = self.theme_manager.get_icon("play")
|
||||||
|
if isinstance(icon, str):
|
||||||
|
icon = QIcon(icon)
|
||||||
|
elif icon is None:
|
||||||
|
icon = QIcon()
|
||||||
|
update_button.setIcon(icon)
|
||||||
|
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
|
||||||
|
self.checkProcessTimer.stop()
|
||||||
|
self.checkProcessTimer.deleteLater()
|
||||||
|
self.checkProcessTimer = None
|
||||||
|
self.current_running_button = None
|
||||||
|
self.target_exe = None
|
||||||
|
self._gameLaunched = False
|
||||||
|
else:
|
||||||
|
# Запускаем игру через PortProton
|
||||||
|
env_vars = os.environ.copy()
|
||||||
|
env_vars['START_FROM_STEAM'] = '1'
|
||||||
|
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
|
||||||
|
|
||||||
|
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
||||||
|
if self.portproton_location is not None and ".var" not in self.portproton_location:
|
||||||
|
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||||
|
wrapper = start_sh
|
||||||
|
|
||||||
|
cmd = [wrapper, game_exe]
|
||||||
|
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(cmd, env=env_vars, shell=False, preexec_fn=os.setsid)
|
||||||
|
self.game_processes.append(process)
|
||||||
|
save_last_launch(exe_name, datetime.now())
|
||||||
|
if update_button:
|
||||||
|
update_button.setText(_("Launching"))
|
||||||
|
icon = self.theme_manager.get_icon("stop")
|
||||||
|
if isinstance(icon, str):
|
||||||
|
icon = QIcon(icon)
|
||||||
|
elif icon is None:
|
||||||
|
icon = QIcon()
|
||||||
|
update_button.setIcon(icon)
|
||||||
|
|
||||||
|
# Delay disabling gamepad handling
|
||||||
|
if hasattr(self, 'input_manager'):
|
||||||
|
QTimer.singleShot(200, self.input_manager.disable_gamepad_handling)
|
||||||
|
|
||||||
|
self.checkProcessTimer = QTimer(self)
|
||||||
|
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
|
||||||
|
self.checkProcessTimer.start(500)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to launch EGS game {app_name}: {e}")
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обработка PortProton-игр
|
||||||
entry_exec_split = shlex.split(exec_line)
|
entry_exec_split = shlex.split(exec_line)
|
||||||
if entry_exec_split[0] == "env":
|
if entry_exec_split[0] == "env":
|
||||||
if len(entry_exec_split) < 3:
|
if len(entry_exec_split) < 3:
|
||||||
@ -1834,18 +1972,20 @@ class MainWindow(QMainWindow):
|
|||||||
file_to_check = entry_exec_split[3]
|
file_to_check = entry_exec_split[3]
|
||||||
else:
|
else:
|
||||||
file_to_check = entry_exec_split[0]
|
file_to_check = entry_exec_split[0]
|
||||||
|
|
||||||
if not os.path.exists(file_to_check):
|
if not os.path.exists(file_to_check):
|
||||||
QMessageBox.warning(self, _("Error"), _("File not found: {0}").format(file_to_check))
|
QMessageBox.warning(self, _("Error"), _("File not found: {0}").format(file_to_check))
|
||||||
return
|
return
|
||||||
current_exe = os.path.basename(file_to_check)
|
|
||||||
|
|
||||||
|
current_exe = os.path.basename(file_to_check)
|
||||||
if self.game_processes and self.target_exe is not None and self.target_exe != current_exe:
|
if self.game_processes and self.target_exe is not None and self.target_exe != current_exe:
|
||||||
QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running"))
|
QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running"))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Обновляем кнопку
|
||||||
update_button = button if button is not None else self.current_play_button
|
update_button = button if button is not None else self.current_play_button
|
||||||
|
|
||||||
# Если игра уже запущена для этого exe – останавливаем её по нажатию кнопки
|
# Если игра уже запущена для этого exe – останавливаем её
|
||||||
if self.game_processes and self.target_exe == current_exe:
|
if self.game_processes and self.target_exe == current_exe:
|
||||||
if hasattr(self, 'input_manager'):
|
if hasattr(self, 'input_manager'):
|
||||||
self.input_manager.enable_gamepad_handling()
|
self.input_manager.enable_gamepad_handling()
|
||||||
@ -1898,6 +2038,15 @@ class MainWindow(QMainWindow):
|
|||||||
env_vars['START_FROM_STEAM'] = '1'
|
env_vars['START_FROM_STEAM'] = '1'
|
||||||
elif entry_exec_split[0] == "flatpak":
|
elif entry_exec_split[0] == "flatpak":
|
||||||
env_vars['START_FROM_STEAM'] = '1'
|
env_vars['START_FROM_STEAM'] = '1'
|
||||||
|
return
|
||||||
|
|
||||||
|
# Запускаем игру
|
||||||
|
self.current_running_button = update_button
|
||||||
|
self.target_exe = current_exe
|
||||||
|
exe_name = os.path.splitext(current_exe)[0]
|
||||||
|
env_vars = os.environ.copy()
|
||||||
|
env_vars['START_FROM_STEAM'] = '1'
|
||||||
|
try:
|
||||||
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
||||||
self.game_processes.append(process)
|
self.game_processes.append(process)
|
||||||
save_last_launch(exe_name, datetime.now())
|
save_last_launch(exe_name, datetime.now())
|
||||||
@ -1913,6 +2062,9 @@ class MainWindow(QMainWindow):
|
|||||||
self.checkProcessTimer = QTimer(self)
|
self.checkProcessTimer = QTimer(self)
|
||||||
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
|
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
|
||||||
self.checkProcessTimer.start(500)
|
self.checkProcessTimer.start(500)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to launch game {exe_name}: {e}")
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
|
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
|
||||||
|
@ -8,76 +8,6 @@ current_theme_name = read_theme_from_config()
|
|||||||
favoriteLabelSize = 48, 48
|
favoriteLabelSize = 48, 48
|
||||||
pixmapsScaledSize = 60, 60
|
pixmapsScaledSize = 60, 60
|
||||||
|
|
||||||
GAME_CARD_ANIMATION = {
|
|
||||||
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
|
|
||||||
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
|
|
||||||
# Значение в пикселях.
|
|
||||||
"default_border_width": 2,
|
|
||||||
|
|
||||||
# Ширина обводки при наведении курсора.
|
|
||||||
# Увеличивает толщину рамки, когда курсор находится над карточкой.
|
|
||||||
# Значение в пикселях.
|
|
||||||
"hover_border_width": 8,
|
|
||||||
|
|
||||||
# Ширина обводки при фокусе (например, при выборе с клавиатуры).
|
|
||||||
# Увеличивает толщину рамки, когда карточка в фокусе.
|
|
||||||
# Значение в пикселях.
|
|
||||||
"focus_border_width": 12,
|
|
||||||
|
|
||||||
# Минимальная ширина обводки во время пульсирующей анимации.
|
|
||||||
# Определяет минимальную толщину рамки при пульсации (анимация "дыхания").
|
|
||||||
# Значение в пикселях.
|
|
||||||
"pulse_min_border_width": 8,
|
|
||||||
|
|
||||||
# Максимальная ширина обводки во время пульсирующей анимации.
|
|
||||||
# Определяет максимальную толщину рамки при пульсации.
|
|
||||||
# Значение в пикселях.
|
|
||||||
"pulse_max_border_width": 10,
|
|
||||||
|
|
||||||
# Длительность анимации изменения толщины обводки (например, при наведении или фокусе).
|
|
||||||
# Влияет на скорость перехода от одной ширины обводки к другой.
|
|
||||||
# Значение в миллисекундах.
|
|
||||||
"thickness_anim_duration": 300,
|
|
||||||
|
|
||||||
# Длительность одного цикла пульсирующей анимации.
|
|
||||||
# Определяет, как быстро рамка "пульсирует" между min и max значениями.
|
|
||||||
# Значение в миллисекундах.
|
|
||||||
"pulse_anim_duration": 800,
|
|
||||||
|
|
||||||
# Длительность анимации вращения градиента.
|
|
||||||
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки.
|
|
||||||
# Значение в миллисекундах.
|
|
||||||
"gradient_anim_duration": 3000,
|
|
||||||
|
|
||||||
# Начальный угол градиента (в градусах).
|
|
||||||
# Определяет начальную точку вращения градиента при старте анимации.
|
|
||||||
"gradient_start_angle": 360,
|
|
||||||
|
|
||||||
# Конечный угол градиента (в градусах).
|
|
||||||
# Определяет конечную точку вращения градиента.
|
|
||||||
# Значение 0 означает полный поворот на 360 градусов.
|
|
||||||
"gradient_end_angle": 0,
|
|
||||||
|
|
||||||
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе).
|
|
||||||
# Влияет на "чувство" анимации (например, плавное ускорение или замедление).
|
|
||||||
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad").
|
|
||||||
"thickness_easing_curve": "OutBack",
|
|
||||||
|
|
||||||
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса).
|
|
||||||
# Влияет на "чувство" возврата к исходной ширине обводки.
|
|
||||||
"thickness_easing_curve_out": "InBack",
|
|
||||||
|
|
||||||
# Цвета градиента для анимированной обводки.
|
|
||||||
# Список словарей, где каждый словарь задает позицию (0.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 = """
|
MAIN_WINDOW_HEADER_STYLE = """
|
||||||
QFrame {
|
QFrame {
|
||||||
|
@ -8,76 +8,6 @@ current_theme_name = read_theme_from_config()
|
|||||||
favoriteLabelSize = 48, 48
|
favoriteLabelSize = 48, 48
|
||||||
pixmapsScaledSize = 60, 60
|
pixmapsScaledSize = 60, 60
|
||||||
|
|
||||||
GAME_CARD_ANIMATION = {
|
|
||||||
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
|
|
||||||
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
|
|
||||||
# Значение в пикселях.
|
|
||||||
"default_border_width": 2,
|
|
||||||
|
|
||||||
# Ширина обводки при наведении курсора.
|
|
||||||
# Увеличивает толщину рамки, когда курсор находится над карточкой.
|
|
||||||
# Значение в пикселях.
|
|
||||||
"hover_border_width": 8,
|
|
||||||
|
|
||||||
# Ширина обводки при фокусе (например, при выборе с клавиатуры).
|
|
||||||
# Увеличивает толщину рамки, когда карточка в фокусе.
|
|
||||||
# Значение в пикселях.
|
|
||||||
"focus_border_width": 12,
|
|
||||||
|
|
||||||
# Минимальная ширина обводки во время пульсирующей анимации.
|
|
||||||
# Определяет минимальную толщину рамки при пульсации (анимация "дыхания").
|
|
||||||
# Значение в пикселях.
|
|
||||||
"pulse_min_border_width": 8,
|
|
||||||
|
|
||||||
# Максимальная ширина обводки во время пульсирующей анимации.
|
|
||||||
# Определяет максимальную толщину рамки при пульсации.
|
|
||||||
# Значение в пикселях.
|
|
||||||
"pulse_max_border_width": 10,
|
|
||||||
|
|
||||||
# Длительность анимации изменения толщины обводки (например, при наведении или фокусе).
|
|
||||||
# Влияет на скорость перехода от одной ширины обводки к другой.
|
|
||||||
# Значение в миллисекундах.
|
|
||||||
"thickness_anim_duration": 300,
|
|
||||||
|
|
||||||
# Длительность одного цикла пульсирующей анимации.
|
|
||||||
# Определяет, как быстро рамка "пульсирует" между min и max значениями.
|
|
||||||
# Значение в миллисекундах.
|
|
||||||
"pulse_anim_duration": 800,
|
|
||||||
|
|
||||||
# Длительность анимации вращения градиента.
|
|
||||||
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки.
|
|
||||||
# Значение в миллисекундах.
|
|
||||||
"gradient_anim_duration": 3000,
|
|
||||||
|
|
||||||
# Начальный угол градиента (в градусах).
|
|
||||||
# Определяет начальную точку вращения градиента при старте анимации.
|
|
||||||
"gradient_start_angle": 360,
|
|
||||||
|
|
||||||
# Конечный угол градиента (в градусах).
|
|
||||||
# Определяет конечную точку вращения градиента.
|
|
||||||
# Значение 0 означает полный поворот на 360 градусов.
|
|
||||||
"gradient_end_angle": 0,
|
|
||||||
|
|
||||||
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе).
|
|
||||||
# Влияет на "чувство" анимации (например, плавное ускорение или замедление).
|
|
||||||
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad").
|
|
||||||
"thickness_easing_curve": "OutBack",
|
|
||||||
|
|
||||||
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса).
|
|
||||||
# Влияет на "чувство" возврата к исходной ширине обводки.
|
|
||||||
"thickness_easing_curve_out": "InBack",
|
|
||||||
|
|
||||||
# Цвета градиента для анимированной обводки.
|
|
||||||
# Список словарей, где каждый словарь задает позицию (0.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 = """
|
CONTEXT_MENU_STYLE = """
|
||||||
QMenu {
|
QMenu {
|
||||||
background: #282a33;;
|
background: #282a33;;
|
||||||
|
Reference in New Issue
Block a user