forked from Boria138/PortProtonQt
feat: use GameCard on autonstall tab
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -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()
|
||||||
|
@@ -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]
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user