feat: use GameCard on autonstall tab

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2025-10-12 13:56:18 +05:00
parent 2d6ef84798
commit 5442100f64
3 changed files with 295 additions and 136 deletions

View File

@@ -1022,6 +1022,130 @@ class InputManager(QObject):
elif current_row_idx == 0: elif current_row_idx == 0:
self._parent.tabButtons[0].setFocus(Qt.FocusReason.OtherFocusReason) self._parent.tabButtons[0].setFocus(Qt.FocusReason.OtherFocusReason)
# Autoinstall tab navigation(index 1)
if self._parent.stackedWidget.currentIndex() == 1 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
focused = QApplication.focusWidget()
game_cards = self._parent.autoInstallContainer.findChildren(GameCard)
if not game_cards:
return
scroll_area = self._parent.autoInstallContainer.parentWidget()
while scroll_area and not isinstance(scroll_area, QScrollArea):
scroll_area = scroll_area.parentWidget()
# If no focused widget or not a GameCard, focus the first card
if not isinstance(focused, GameCard) or focused not in game_cards:
game_cards[0].setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
return
cards = self._parent.autoInstallContainer.findChildren(GameCard, options=Qt.FindChildOption.FindChildrenRecursively)
if not cards:
return
# Group cards by rows with tolerance for y-position
rows = {}
y_tolerance = 10 # Allow slight variations in y-position
for card in cards:
y = card.pos().y()
matched = False
for row_y in rows:
if abs(y - row_y) <= y_tolerance:
rows[row_y].append(card)
matched = True
break
if not matched:
rows[y] = [card]
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
if not sorted_rows:
return
current_row_idx = None
current_col_idx = None
for row_idx, (_y, row_cards) in enumerate(sorted_rows):
for idx, card in enumerate(row_cards):
if card == focused:
current_row_idx = row_idx
current_col_idx = idx
break
if current_row_idx is not None:
break
# Fallback: if focused card not found, select closest row by y-position
if current_row_idx is None:
if not sorted_rows: # Additional safety check
return
focused_y = focused.pos().y()
current_row_idx = min(range(len(sorted_rows)), key=lambda i: abs(sorted_rows[i][0] - focused_y))
if current_row_idx >= len(sorted_rows): # Safety check
return
current_row = sorted_rows[current_row_idx][1]
focused_x = focused.pos().x() + focused.width() / 2
current_col_idx = min(range(len(current_row)), key=lambda i: abs((current_row[i].pos().x() + current_row[i].width() / 2) - focused_x), default=0) # type: ignore
# Add null checks before using current_row_idx and current_col_idx
if current_row_idx is None or current_col_idx is None or current_row_idx >= len(sorted_rows):
return
current_row = sorted_rows[current_row_idx][1]
if code == ecodes.ABS_HAT0X and value != 0:
if value < 0: # Left
if current_col_idx > 0:
next_card = current_row[current_col_idx - 1]
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
else:
if current_row_idx > 0:
prev_row = sorted_rows[current_row_idx - 1][1]
next_card = prev_row[-1] if prev_row else None
if next_card:
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
elif value > 0: # Right
if current_col_idx < len(current_row) - 1:
next_card = current_row[current_col_idx + 1]
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
else:
if current_row_idx < len(sorted_rows) - 1:
next_row = sorted_rows[current_row_idx + 1][1]
next_card = next_row[0] if next_row else None
if next_card:
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
elif code == ecodes.ABS_HAT0Y and value != 0:
if value > 0: # Down
if current_row_idx < len(sorted_rows) - 1:
next_row = sorted_rows[current_row_idx + 1][1]
current_x = focused.pos().x() + focused.width() / 2
next_card = min(
next_row,
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
default=None
)
if next_card:
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
elif value < 0: # Up
if current_row_idx > 0:
prev_row = sorted_rows[current_row_idx - 1][1]
current_x = focused.pos().x() + focused.width() / 2
next_card = min(
prev_row,
key=lambda c: abs((c.pos().x() + c.width() / 2) - current_x),
default=None
)
if next_card:
next_card.setFocus(Qt.FocusReason.OtherFocusReason)
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
elif current_row_idx == 0:
self._parent.tabButtons[0].setFocus(Qt.FocusReason.OtherFocusReason)
# Vertical navigation in other tabs # Vertical navigation in other tabs
elif code == ecodes.ABS_HAT0Y and value != 0: elif code == ecodes.ABS_HAT0Y and value != 0:
focused = QApplication.focusWidget() focused = QApplication.focusWidget()
@@ -1088,6 +1212,21 @@ class InputManager(QObject):
keyboard.on_backspace_pressed() keyboard.on_backspace_pressed()
elif value == 0: elif value == 0:
keyboard.stop_backspace_repeat() keyboard.stop_backspace_repeat()
elif button_code in BUTTONS['search']: # Кнопка Y/Square - поиск (tab-specific)
if value == 1:
current_index = self._parent.stackedWidget.currentIndex()
search_field = None
if current_index == 0: # Library tab
search_field = self._parent.searchLineEdit
elif current_index == 1: # Auto Install tab
search_field = self._parent.autoInstallSearchLineEdit
else:
return
if search_field:
search_field.setFocus()
keyboard.current_input_widget = search_field
keyboard.show()
def eventFilter(self, obj: QObject, event: QEvent) -> bool: def eventFilter(self, obj: QObject, event: QEvent) -> bool:
app = QApplication.instance() app = QApplication.instance()

View File

@@ -11,7 +11,7 @@ from portprotonqt.logger import get_logger
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog
from portprotonqt.game_card import GameCard from portprotonqt.game_card import GameCard
from portprotonqt.animations import DetailPageAnimations from portprotonqt.animations import DetailPageAnimations
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
from portprotonqt.portproton_api import PortProtonAPI from portprotonqt.portproton_api import PortProtonAPI
from portprotonqt.input_manager import InputManager from portprotonqt.input_manager import InputManager
from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit
@@ -518,8 +518,6 @@ class MainWindow(QMainWindow):
self.progress_bar.setValue(100) self.progress_bar.setValue(100)
if exit_code == 0: if exit_code == 0:
self.update_status_message.emit(_("Installation completed successfully."), 5000) self.update_status_message.emit(_("Installation completed successfully."), 5000)
# Reload library after delay
QTimer.singleShot(3000, self.loadGames)
else: else:
self.update_status_message.emit(_("Installation failed."), 5000) self.update_status_message.emit(_("Installation failed."), 5000)
QMessageBox.warning(self, _("Error"), f"Installation failed (code: {exit_code}).") QMessageBox.warning(self, _("Error"), f"Installation failed (code: {exit_code}).")
@@ -1061,113 +1059,150 @@ class MainWindow(QMainWindow):
get_steam_game_info_async(final_name, exec_line, on_steam_info) get_steam_game_info_async(final_name, exec_line, on_steam_info)
def createAutoInstallTab(self): def createAutoInstallTab(self):
"""Create the Auto Install tab with flow layout of simple game cards (cover, name, install button)."""
from portprotonqt.localization import _
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(20)
# Header label autoInstallPage = QWidget()
header = QLabel(_("Auto Install Games")) autoInstallPage.setStyleSheet(self.theme.MAIN_WINDOW_STYLE)
header.setStyleSheet(self.theme.DETAIL_PAGE_TITLE_STYLE) autoInstallLayout = QVBoxLayout(autoInstallPage)
layout.addWidget(header) autoInstallLayout.setContentsMargins(0, 0, 0, 0)
autoInstallLayout.setSpacing(0)
# Scroll area for games # Верхняя панель с заголовком и поиском
scroll = QScrollArea() headerWidget = QWidget()
scroll.setWidgetResizable(True) headerLayout = QHBoxLayout(headerWidget)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) headerLayout.setContentsMargins(20, 10, 20, 10)
scroll_widget = QWidget() headerLayout.setSpacing(10)
from portprotonqt.custom_widgets import FlowLayout
self.auto_install_flow_layout = FlowLayout(scroll_widget) # Store reference for potential updates
self.auto_install_flow_layout.setSpacing(15)
self.auto_install_flow_layout.setContentsMargins(0, 0, 0, 0)
# Load games asynchronously (though now sync inside, but callback for consistency) # Заголовок
titleLabel = QLabel(_("Auto Install Games"))
titleLabel.setStyleSheet(self.theme.TAB_TITLE_STYLE)
titleLabel.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
headerLayout.addWidget(titleLabel)
headerLayout.addStretch()
# Поисковая строка
self.autoInstallSearchLineEdit = CustomLineEdit(self, theme=self.theme)
icon: QIcon = cast(QIcon, self.theme_manager.get_icon("search"))
action_pos = cast(int, CustomLineEdit.ActionPosition.LeadingPosition)
self.search_action = self.autoInstallSearchLineEdit.addAction(icon, action_pos)
self.autoInstallSearchLineEdit.setMaximumWidth(200)
self.autoInstallSearchLineEdit.setPlaceholderText(_("Find Games ..."))
self.autoInstallSearchLineEdit.setClearButtonEnabled(True)
self.autoInstallSearchLineEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE)
self.autoInstallSearchLineEdit.textChanged.connect(self.filterAutoInstallGames)
headerLayout.addWidget(self.autoInstallSearchLineEdit)
autoInstallLayout.addWidget(headerWidget)
# Прогресс-бар
self.autoInstallProgress = QProgressBar()
self.autoInstallProgress.setStyleSheet(self.theme.PROGRESS_BAR_STYLE)
self.autoInstallProgress.setVisible(False)
autoInstallLayout.addWidget(self.autoInstallProgress)
# Скролл
self.autoInstallScrollArea = QScrollArea()
self.autoInstallScrollArea.setWidgetResizable(True)
self.autoInstallScrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.autoInstallScrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.autoInstallScrollArea.setStyleSheet("QScrollArea { border: none; background: transparent; }")
self.autoInstallContainer = QWidget()
self.autoInstallContainerLayout = FlowLayout(self.autoInstallContainer)
self.autoInstallContainer.setLayout(self.autoInstallContainerLayout)
self.autoInstallScrollArea.setWidget(self.autoInstallContainer)
autoInstallLayout.addWidget(self.autoInstallScrollArea)
# Хранение карточек
self.autoInstallGameCards = {}
self.allAutoInstallCards = []
# Обновление обложки
def on_autoinstall_cover_updated(exe_name, local_path):
if exe_name in self.autoInstallGameCards and local_path:
card = self.autoInstallGameCards[exe_name]
card.cover_path = local_path
load_pixmap_async(local_path, self.card_width, int(self.card_width * 1.5), card.on_cover_loaded)
# Загрузка игр
def on_autoinstall_games_loaded(games: list[tuple]): def on_autoinstall_games_loaded(games: list[tuple]):
# Clear existing widgets self.autoInstallProgress.setVisible(False)
while self.auto_install_flow_layout.count():
child = self.auto_install_flow_layout.takeAt(0) # Очистка
if child.widget(): while self.autoInstallContainerLayout.count():
child = self.autoInstallContainerLayout.takeAt(0)
if child:
child.widget().deleteLater() child.widget().deleteLater()
for game in games: self.autoInstallGameCards.clear()
name = game[0] self.allAutoInstallCards.clear()
description = game[1]
cover_path = game[2]
exec_line = game[4]
script_name = exec_line.split("autoinstall:")[1] if exec_line.startswith("autoinstall:") else ""
# Create simple card frame if not games:
card_frame = QFrame() return
card_frame.setFixedWidth(self.card_width)
card_frame.setStyleSheet(self.theme.GAME_CARD_STYLE if hasattr(self.theme, 'GAME_CARD_STYLE') else "")
card_layout = QVBoxLayout(card_frame)
card_layout.setContentsMargins(10, 10, 10, 10)
card_layout.setSpacing(10)
# Cover image # Callback для запуска установки
cover_label = QLabel() def select_callback(name, description, cover_path, appid, exec_line, controller_support, *_):
cover_label.setFixedHeight(120) if not exec_line or not exec_line.startswith("autoinstall:"):
cover_label.setAlignment(Qt.AlignmentFlag.AlignCenter) logger.warning(f"Invalid exec_line for autoinstall: {exec_line}")
pixmap = QPixmap() return
if cover_path and os.path.exists(cover_path) and pixmap.load(cover_path): script_name = exec_line[11:].lstrip(':').strip()
scaled_pix = pixmap.scaled(self.card_width - 40, 120, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) self.launch_autoinstall(script_name)
cover_label.setPixmap(scaled_pix)
else:
# Placeholder
placeholder_icon = self.theme_manager.get_theme_image("placeholder", self.current_theme_name)
if placeholder_icon:
pixmap.load(str(placeholder_icon))
scaled_pix = pixmap.scaled(100, 100, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
cover_label.setPixmap(scaled_pix)
card_layout.addWidget(cover_label)
# Name label # Создаём карточки
name_label = QLabel(name) for game_tuple in games:
name_label.setWordWrap(True) name, description, cover_path, appid, controller_support, exec_line, *_ , game_source, exe_name = game_tuple
name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
name_label.setStyleSheet(self.theme.CARD_TITLE_STYLE if hasattr(self.theme, 'CARD_TITLE_STYLE') else "")
card_layout.addWidget(name_label)
# Optional short description card = GameCard(
if description: name, description, cover_path, appid, controller_support,
desc_label = QLabel(description[:100] + "..." if len(description) > 100 else description) exec_line, None, None, None,
desc_label.setWordWrap(True) None, None, None, game_source,
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) select_callback=select_callback,
desc_label.setStyleSheet(self.theme.CARD_DESC_STYLE if hasattr(self.theme, 'CARD_DESC_STYLE') else "") theme=self.theme,
card_layout.addWidget(desc_label) card_width=self.card_width,
parent=self.autoInstallContainer,
)
# Install button self.autoInstallGameCards[exe_name] = card
install_btn = AutoSizeButton(_("Install")) self.allAutoInstallCards.append(card)
install_btn.setStyleSheet(self.theme.PLAY_BUTTON_STYLE) self.autoInstallContainerLayout.addWidget(card)
install_btn.clicked.connect(lambda checked, s=script_name: self.launch_autoinstall(s))
card_layout.addWidget(install_btn)
card_layout.addStretch() # Загружаем недостающие обложки
for game_tuple in games:
name, _, cover_path, *_ , game_source, exe_name = game_tuple
if not cover_path:
self.portproton_api.download_autoinstall_cover_async(
exe_name, timeout=5,
callback=lambda path, ex=exe_name: on_autoinstall_cover_updated(ex, path)
)
# Add to flow layout self.autoInstallContainer.updateGeometry()
self.auto_install_flow_layout.addWidget(card_frame) self.autoInstallScrollArea.updateGeometry()
self.filterAutoInstallGames()
scroll.setWidget(scroll_widget) # Показываем прогресс
layout.addWidget(scroll) self.autoInstallProgress.setVisible(True)
self.autoInstallProgress.setRange(0, 0)
# Trigger load
self.portproton_api.get_autoinstall_games_async(on_autoinstall_games_loaded) self.portproton_api.get_autoinstall_games_async(on_autoinstall_games_loaded)
self.stackedWidget.addWidget(tab) self.stackedWidget.addWidget(autoInstallPage)
def filterAutoInstallGames(self):
"""Filter auto install game cards based on search text."""
search_text = self.autoInstallSearchLineEdit.text().lower().strip()
visible_count = 0
def on_auto_install_search_changed(self, text: str): for card in self.allAutoInstallCards:
"""Filter auto-install games based on search text.""" if search_text in card.name.lower():
filtered_games = [g for g in self.auto_install_games if text.lower() in g[0].lower() or text.lower() in g[1].lower()] card.setVisible(True)
self.populate_auto_install_grid(filtered_games) visible_count += 1
self.auto_install_clear_search_button.setVisible(bool(text)) else:
card.setVisible(False)
def clear_auto_install_search(self): # Re-layout the container
"""Clear the auto-install search and repopulate grid.""" self.autoInstallContainerLayout.invalidate()
self.auto_install_search_line.clear() self.autoInstallContainer.updateGeometry()
self.populate_auto_install_grid(self.auto_install_games) self.autoInstallScrollArea.updateGeometry()
def createWineTab(self): def createWineTab(self):
"""Вкладка 'Wine Settings'.""" """Вкладка 'Wine Settings'."""
@@ -1787,7 +1822,7 @@ class MainWindow(QMainWindow):
# # 8. Legendary Authentication # # 8. Legendary Authentication
# self.legendaryAuthButton = AutoSizeButton( # self.legendaryAuthButton = AutoSizeButton(
# _("Open Legendary Login"), # _("Open Legendary Login"),
# icon=self.theme_manager.get_icon("login") # icon=self.theme_manager.get_icon("login")self.theme_manager.get_icon("login")
# ) # )
# self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) # self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
# self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
@@ -2713,11 +2748,6 @@ class MainWindow(QMainWindow):
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
return return
if exec_line.startswith("autoinstall:"):
script_name = exec_line.split("autoinstall:")[1]
self.launch_autoinstall(script_name)
return
# Обработка EGS-игр # Обработка EGS-игр
if exec_line.startswith("legendary:launch:"): if exec_line.startswith("legendary:launch:"):
app_name = exec_line.split("legendary:launch:")[1] app_name = exec_line.split("legendary:launch:")[1]

View File

@@ -136,42 +136,38 @@ class PortProtonAPI:
callback(results) callback(results)
def download_autoinstall_cover_async(self, exe_name: str, timeout: int = 5, callback: Callable[[str | None], None] | None = None) -> None: def download_autoinstall_cover_async(self, exe_name: str, timeout: int = 5, callback: Callable[[str | None], None] | None = None) -> None:
"""Download only autoinstall cover image (no metadata).""" """Download only autoinstall cover image (PNG only, no metadata)."""
xdg_data_home = os.getenv("XDG_DATA_HOME", xdg_data_home = os.getenv("XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share")) os.path.join(os.path.expanduser("~"), ".local", "share"))
user_game_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall", exe_name) autoinstall_root = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
os.makedirs(user_game_folder, exist_ok=True) user_game_folder = os.path.join(autoinstall_root, exe_name)
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"] if not os.path.isdir(user_game_folder):
results: str | None = None try:
pending_downloads = 0 os.mkdir(user_game_folder)
except FileExistsError:
pass
def on_cover_downloaded(local_path: str | None, ext: str): cover_url = f"{self.base_url}/{exe_name}/cover.png"
nonlocal pending_downloads, results local_cover_path = os.path.join(user_game_folder, "cover.png")
def on_cover_downloaded(local_path: str | None):
if local_path: if local_path:
logger.info(f"Async autoinstall cover downloaded for {exe_name}: {local_path}") logger.info(f"Async autoinstall cover downloaded for {exe_name}: {local_path}")
results = local_path
else: else:
logger.debug(f"No autoinstall cover downloaded for {exe_name} with extension {ext}") logger.debug(f"No autoinstall cover downloaded for {exe_name}")
pending_downloads -= 1 if callback:
if pending_downloads == 0 and callback: callback(local_path)
callback(results)
for ext in cover_extensions: if self._check_file_exists(cover_url, timeout):
cover_url = f"{self.base_url}/{exe_name}/cover{ext}" self.downloader.download_async(
if self._check_file_exists(cover_url, timeout): cover_url,
local_cover_path = os.path.join(user_game_folder, f"cover{ext}") local_cover_path,
pending_downloads += 1 timeout=timeout,
self.downloader.download_async( callback=on_cover_downloaded
cover_url, )
local_cover_path, else:
timeout=timeout, logger.debug(f"No autoinstall cover found for {exe_name}")
callback=lambda path, ext=ext: on_cover_downloaded(path, ext)
)
break
if pending_downloads == 0:
logger.debug(f"No autoinstall covers found for {exe_name}")
if callback: if callback:
callback(None) callback(None)
@@ -212,10 +208,8 @@ class PortProtonAPI:
portwine_match = None portwine_match = None
for line in content.splitlines(): for line in content.splitlines():
stripped = line.strip() stripped = line.strip()
# Игнорируем закомментированные строки
if stripped.startswith("#"): if stripped.startswith("#"):
continue continue
# Ищем portwine_exe только в активных строках
if "portwine_exe" in stripped and "=" in stripped: if "portwine_exe" in stripped and "=" in stripped:
portwine_match = stripped portwine_match = stripped
break break
@@ -238,7 +232,7 @@ class PortProtonAPI:
return None, None return None, None
def get_autoinstall_games_async(self, callback: Callable[[list[tuple]], None]) -> None: def get_autoinstall_games_async(self, callback: Callable[[list[tuple]], None]) -> None:
"""Load auto-install games with user/builtin covers and async autoinstall cover download.""" """Load auto-install games with user/builtin covers (no async download here)."""
games = [] games = []
auto_dir = os.path.join(self.portproton_location, "data", "scripts", "pw_autoinstall") auto_dir = os.path.join(self.portproton_location, "data", "scripts", "pw_autoinstall")
if not os.path.exists(auto_dir): if not os.path.exists(auto_dir):
@@ -275,15 +269,10 @@ class PortProtonAPI:
cover_path = os.path.join(user_game_folder, candidate) cover_path = os.path.join(user_game_folder, candidate)
break break
# Если обложки нет — пытаемся скачать
if not cover_path: if not cover_path:
logger.debug(f"No local cover found for autoinstall {exe_name}, trying to download...") logger.debug(f"No local cover found for autoinstall {exe_name}")
def on_cover_downloaded(path):
if path:
logger.info(f"Downloaded autoinstall cover for {exe_name}: {path}")
self.download_autoinstall_cover_async(exe_name, timeout=5, callback=on_cover_downloaded)
# Формируем кортеж игры # Формируем кортеж игры (добавлен exe_name в конец)
game_tuple = ( game_tuple = (
display_name, # name display_name, # name
"", # description "", # description
@@ -297,7 +286,8 @@ class PortProtonAPI:
"", # anticheat_status "", # anticheat_status
0, # last_played 0, # last_played
0, # playtime_seconds 0, # playtime_seconds
"autoinstall" # game_source "autoinstall", # game_source
exe_name # exe_name
) )
games.append(game_tuple) games.append(game_tuple)