3 Commits

Author SHA1 Message Date
48048a3f50 feat: replace QFileDialog with custom FileExplorer for Legendary import
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-30 00:25:28 +05:00
7c617eef78 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-29 23:25:27 +05:00
08ba801f74 fix: prevent empty area when updating game grid thank to @Vector_null
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-29 23:23:22 +05:00
5 changed files with 201 additions and 136 deletions

View File

@@ -26,6 +26,7 @@
- Ошибки темы в нативном пакете - Ошибки темы в нативном пакете
- Ошибки темы в Gamescope - Ошибки темы в Gamescope
- Размер иконок для desktop файлов теперь 128x128 - Размер иконок для desktop файлов теперь 128x128
- Пустая область при обновлении сетки игр
### Contributors ### Contributors
- @Dervart - @Dervart

View File

@@ -6,14 +6,14 @@ import subprocess
import threading import threading
import logging import logging
import orjson import orjson
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog from PySide6.QtWidgets import QMessageBox, QDialog, QMenu
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
from PySide6.QtGui import QDesktopServices from PySide6.QtGui import QDesktopServices
from portprotonqt.localization import _ from portprotonqt.localization import _
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
from portprotonqt.dialogs import AddGameDialog, generate_thumbnail from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -282,18 +282,25 @@ class ContextMenuManager:
""" """
if not self._check_portproton(): if not self._check_portproton():
return return
folder_path = QFileDialog.getExistingDirectory(
self.parent, _("Select Game Installation Folder"), os.path.expanduser("~")
)
if not folder_path:
self._show_status_message(_("No folder selected"))
return
if not os.path.exists(self.legendary_path): if not os.path.exists(self.legendary_path):
self.signals.show_warning_dialog.emit( self.signals.show_warning_dialog.emit(
_("Error"), _("Error"),
_("Legendary executable not found at {path}").format(path=self.legendary_path) _("Legendary executable not found at {path}").format(path=self.legendary_path)
) )
return return
# Используем FileExplorer с directory_only=True
file_explorer = FileExplorer(
parent=self.parent,
theme=self.theme,
initial_path=os.path.expanduser("~"),
directory_only=True
)
def on_folder_selected(folder_path):
if not folder_path:
self._show_status_message(_("No folder selected"))
return
def run_import(): def run_import():
cmd = [self.legendary_path, "import", app_name, folder_path] cmd = [self.legendary_path, "import", app_name, folder_path]
try: try:
@@ -312,6 +319,20 @@ class ContextMenuManager:
self._show_status_message(_("Importing '{game_name}' to Legendary...").format(game_name=game_name)) self._show_status_message(_("Importing '{game_name}' to Legendary...").format(game_name=game_name))
threading.Thread(target=run_import, daemon=True).start() threading.Thread(target=run_import, daemon=True).start()
# Подключаем сигнал выбора файла/папки
file_explorer.file_signal.file_selected.connect(on_folder_selected)
# Центрируем FileExplorer относительно родительского виджета
parent_widget = self.parent
if parent_widget:
parent_geometry = parent_widget.geometry()
center_point = parent_geometry.center()
file_explorer_geometry = file_explorer.geometry()
file_explorer_geometry.moveCenter(center_point)
file_explorer.setGeometry(file_explorer_geometry)
file_explorer.show()
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.

View File

@@ -88,12 +88,13 @@ class FileSelectedSignal(QObject):
file_selected = Signal(str) # Сигнал с путем к выбранному файлу file_selected = Signal(str) # Сигнал с путем к выбранному файлу
class FileExplorer(QDialog): class FileExplorer(QDialog):
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None): def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
super().__init__(parent) super().__init__(parent)
self.theme = theme if theme else default_styles self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager() self.theme_manager = ThemeManager()
self.file_signal = FileSelectedSignal() self.file_signal = FileSelectedSignal()
self.file_filter = file_filter # Store the file filter self.file_filter = file_filter # Store the file filter
self.directory_only = directory_only # Store the directory_only flag
self.mime_db = QMimeDatabase() # Initialize QMimeDatabase for mimetype detection self.mime_db = QMimeDatabase() # Initialize QMimeDatabase for mimetype detection
self.path_history = {} # Dictionary to store last selected item per directory self.path_history = {} # Dictionary to store last selected item per directory
self.initial_path = initial_path # Store initial path if provided self.initial_path = initial_path # Store initial path if provided
@@ -216,13 +217,21 @@ class FileExplorer(QDialog):
full_path = os.path.join(self.current_path, selected) full_path = os.path.join(self.current_path, selected)
if os.path.isdir(full_path): if os.path.isdir(full_path):
# Если выбрана директория, нормализуем путь if self.directory_only:
# Подтверждаем выбор директории
self.file_signal.file_selected.emit(os.path.normpath(full_path))
self.accept()
else:
# Открываем директорию
self.current_path = os.path.normpath(full_path) self.current_path = os.path.normpath(full_path)
self.update_file_list() self.update_file_list()
else: else:
if not self.directory_only:
# Для файла отправляем нормализованный путь # Для файла отправляем нормализованный путь
self.file_signal.file_selected.emit(os.path.normpath(full_path)) self.file_signal.file_selected.emit(os.path.normpath(full_path))
self.accept() self.accept()
else:
logger.debug("Selected item is not a directory, ignoring: %s", full_path)
def previous_dir(self): def previous_dir(self):
"""Возврат к родительской директории""" """Возврат к родительской директории"""
@@ -288,14 +297,7 @@ class FileExplorer(QDialog):
items = os.listdir(self.current_path) items = os.listdir(self.current_path)
dirs = [d for d in items if os.path.isdir(os.path.join(self.current_path, d))] dirs = [d for d in items if os.path.isdir(os.path.join(self.current_path, d))]
# Apply file filter if provided # Добавляем директории
files = [f for f in items if os.path.isfile(os.path.join(self.current_path, f))]
if self.file_filter:
if isinstance(self.file_filter, str):
files = [f for f in files if f.lower().endswith(self.file_filter)]
elif isinstance(self.file_filter, tuple):
files = [f for f in files if any(f.lower().endswith(ext) for ext in self.file_filter)]
for d in sorted(dirs): for d in sorted(dirs):
item = QListWidgetItem(f"{d}/") item = QListWidgetItem(f"{d}/")
folder_icon = self.theme_manager.get_icon("folder") folder_icon = self.theme_manager.get_icon("folder")
@@ -307,6 +309,15 @@ class FileExplorer(QDialog):
item.setIcon(folder_icon) item.setIcon(folder_icon)
self.file_list.addItem(item) self.file_list.addItem(item)
# Добавляем файлы только если directory_only=False
if not self.directory_only:
files = [f for f in items if os.path.isfile(os.path.join(self.current_path, f))]
if self.file_filter:
if isinstance(self.file_filter, str):
files = [f for f in files if f.lower().endswith(self.file_filter)]
elif isinstance(self.file_filter, tuple):
files = [f for f in files if any(f.lower().endswith(ext) for ext in self.file_filter)]
for f in sorted(files): for f in sorted(files):
item = QListWidgetItem(f) item = QListWidgetItem(f)
file_path = os.path.join(self.current_path, f) file_path = os.path.join(self.current_path, f)

View File

@@ -1,5 +1,6 @@
import time import time
import threading import threading
import os
from typing import Protocol, cast from typing import Protocol, cast
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
from pyudev import Context, Monitor, MonitorObserver, Device from pyudev import Context, Monitor, MonitorObserver, Device
@@ -162,8 +163,28 @@ class InputManager(QObject):
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'): if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'):
return return
if button_code in BUTTONS['confirm']: if button_code in BUTTONS['add_game']:
self.file_explorer.select_item() if self.file_explorer.file_list.count() == 0:
return
selected = self.file_explorer.file_list.currentItem().text()
full_path = os.path.join(self.file_explorer.current_path, selected)
if os.path.isdir(full_path):
# Подтверждаем выбор директории
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
self.file_explorer.accept()
else:
logger.debug("Selected item is not a directory: %s", full_path)
elif button_code in BUTTONS['confirm']:
if self.file_explorer.file_list.count() == 0:
return
selected = self.file_explorer.file_list.currentItem().text()
full_path = os.path.join(self.file_explorer.current_path, selected)
if os.path.isdir(full_path):
# Открываем директорию
self.file_explorer.current_path = os.path.normpath(full_path)
self.file_explorer.update_file_list()
else:
logger.debug("Selected item is not a directory, cannot open: %s", full_path)
elif button_code in BUTTONS['back']: elif button_code in BUTTONS['back']:
self.file_explorer.close() self.file_explorer.close()
elif button_code in BUTTONS['prev_dir']: elif button_code in BUTTONS['prev_dir']:

View File

@@ -696,59 +696,50 @@ class MainWindow(QMainWindow):
loaded_count += 1 loaded_count += 1
def updateGameGrid(self, games_list=None): def updateGameGrid(self, games_list=None):
"""Updates the game grid with the provided games list or self.games.""" """Обновляет сетку игровых карточек с сохранением порядка сортировки"""
if games_list is None: # Подготовка данных
games_list = self.games games_list = games_list if games_list is not None else self.games
if not games_list:
# Скрываем все карточки, если список пуст
for card in self.game_card_cache.values():
card.hide()
self.game_card_cache.clear()
self.pending_images.clear()
self.gamesListWidget.updateGeometry()
return
# Создаем словарь текущих игр с уникальным ключом (name + exec_line)
current_games = {(game_data[0], game_data[4]): game_data for game_data in games_list}
# Проверяем, изменился ли список игр или размер карточек
current_game_keys = set(current_games.keys())
cached_game_keys = set(self.game_card_cache.keys())
card_width_changed = self.card_width != getattr(self, '_last_card_width', None)
if current_game_keys == cached_game_keys and not card_width_changed:
# Список игр и размер карточек не изменились, обновляем только видимость
search_text = self.searchEdit.text().strip().lower() search_text = self.searchEdit.text().strip().lower()
for game_key, card in self.game_card_cache.items(): favorites = read_favorites()
game_name = game_key[0] sort_method = read_sort_method()
card.setVisible(search_text in game_name.lower() or not search_text)
self.loadVisibleImages()
return
# Обновляем размер карточек, если он изменился # Сортируем игры согласно текущим настройкам
if card_width_changed: def sort_key(game):
for card in self.game_card_cache.values(): name = game[0]
card.setFixedWidth(self.card_width + 20) # Учитываем extra_margin в GameCard # Избранные всегда первые
if name in favorites:
fav_order = 0
else:
fav_order = 1
# Удаляем карточки, которых больше нет в списке if sort_method == "playtime":
layout_changed = False return (fav_order, -game[11], -game[10]) # playtime_seconds, last_launch_ts
for card_key in list(self.game_card_cache.keys()): elif sort_method == "alphabetical":
if card_key not in current_games: return (fav_order, name.lower())
card = self.game_card_cache.pop(card_key) elif sort_method == "favorites":
self.gamesListLayout.removeWidget(card) return (fav_order,)
card.deleteLater() else: # "last_launch" или по умолчанию
if card_key in self.pending_images: return (fav_order, -game[10], -game[11]) # last_launch_ts, playtime_seconds
del self.pending_images[card_key]
layout_changed = True
# Добавляем новые карточки и обновляем существующие sorted_games = sorted(games_list, key=sort_key)
for game_data in games_list:
# Создаем временный список для новых карточек
new_card_order = []
# Обрабатываем каждую игру в отсортированном порядке
for game_data in sorted_games:
game_name = game_data[0] game_name = game_data[0]
game_key = (game_name, game_data[4]) exec_line = game_data[4]
search_text = self.searchEdit.text().strip().lower() game_key = (game_name, exec_line)
should_be_visible = search_text in game_name.lower() or not search_text should_be_visible = not search_text or search_text in game_name.lower()
# Если карточка уже существует - используем существующую
if game_key in self.game_card_cache:
card = self.game_card_cache[game_key]
card.setVisible(should_be_visible)
new_card_order.append((game_key, card))
continue
if game_key not in self.game_card_cache:
# Создаем новую карточку # Создаем новую карточку
card = GameCard( card = GameCard(
*game_data, *game_data,
@@ -757,8 +748,11 @@ class MainWindow(QMainWindow):
card_width=self.card_width, card_width=self.card_width,
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.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)
@@ -769,25 +763,42 @@ class MainWindow(QMainWindow):
card.addToSteamRequested.connect(self.context_menu_manager.add_to_steam) card.addToSteamRequested.connect(self.context_menu_manager.add_to_steam)
card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam) card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam)
card.openGameFolderRequested.connect(self.context_menu_manager.open_game_folder) card.openGameFolderRequested.connect(self.context_menu_manager.open_game_folder)
# Добавляем в кэш и временный список
self.game_card_cache[game_key] = card self.game_card_cache[game_key] = card
self.gamesListLayout.addWidget(card) new_card_order.append((game_key, card))
layout_changed = True
else:
# Обновляем видимость существующей карточки
card = self.game_card_cache[game_key]
card.setVisible(should_be_visible) card.setVisible(should_be_visible)
# Сохраняем текущий card_width # Полностью перестраиваем макет в правильном порядке, чистим FlowLayout
self._last_card_width = self.card_width while self.gamesListLayout.count():
child = self.gamesListLayout.takeAt(0)
if child.widget():
child.widget().setParent(None)
# Добавляем карточки в макет в отсортированном порядке
for _game_key, card in new_card_order:
self.gamesListLayout.addWidget(card)
# Загружаем обложку, если карточка видима
if card.isVisible():
self.loadVisibleImages()
# Удаляем карточки для игр, которых больше нет в списке
existing_keys = {game_key for game_key, _ in new_card_order}
for card_key in list(self.game_card_cache.keys()):
if card_key not in existing_keys:
card = self.game_card_cache.pop(card_key)
card.deleteLater()
if card_key in self.pending_images:
del self.pending_images[card_key]
# Принудительно обновляем макет # Принудительно обновляем макет
if layout_changed:
self.gamesListLayout.update() self.gamesListLayout.update()
self.gamesListWidget.updateGeometry() self.gamesListWidget.updateGeometry()
self.gamesListWidget.update() self.gamesListWidget.update()
# Загружаем изображения для видимых карточек # Сохраняем текущий размер карточек
self.loadVisibleImages() self._last_card_width = self.card_width
def clearLayout(self, layout): def clearLayout(self, layout):
"""Удаляет все виджеты из layout.""" """Удаляет все виджеты из layout."""