forked from Boria138/PortProtonQt
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			a6562ca488
			...
			48048a3f50
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						48048a3f50
	
				 | 
					
					
						|||
| 
						
						
							
						
						7c617eef78
	
				 | 
					
					
						|||
| 
						
						
							
						
						08ba801f74
	
				 | 
					
					
						
@@ -26,6 +26,7 @@
 | 
			
		||||
- Ошибки темы в нативном пакете
 | 
			
		||||
- Ошибки темы в Gamescope
 | 
			
		||||
- Размер иконок для desktop файлов теперь 128x128
 | 
			
		||||
- Пустая область при обновлении сетки игр
 | 
			
		||||
 | 
			
		||||
### Contributors
 | 
			
		||||
- @Dervart
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,14 @@ import subprocess
 | 
			
		||||
import threading
 | 
			
		||||
import logging
 | 
			
		||||
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.QtGui import QDesktopServices
 | 
			
		||||
from portprotonqt.localization import _
 | 
			
		||||
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.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__)
 | 
			
		||||
 | 
			
		||||
@@ -282,18 +282,25 @@ class ContextMenuManager:
 | 
			
		||||
        """
 | 
			
		||||
        if not self._check_portproton():
 | 
			
		||||
            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):
 | 
			
		||||
            self.signals.show_warning_dialog.emit(
 | 
			
		||||
                _("Error"),
 | 
			
		||||
                _("Legendary executable not found at {path}").format(path=self.legendary_path)
 | 
			
		||||
            )
 | 
			
		||||
            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():
 | 
			
		||||
                cmd = [self.legendary_path, "import", app_name, folder_path]
 | 
			
		||||
                try:
 | 
			
		||||
@@ -312,6 +319,20 @@ class ContextMenuManager:
 | 
			
		||||
            self._show_status_message(_("Importing '{game_name}' to Legendary...").format(game_name=game_name))
 | 
			
		||||
            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):
 | 
			
		||||
        """
 | 
			
		||||
        Toggle the favorite status of a game and update its icon.
 | 
			
		||||
 
 | 
			
		||||
@@ -88,12 +88,13 @@ class FileSelectedSignal(QObject):
 | 
			
		||||
    file_selected = Signal(str)  # Сигнал с путем к выбранному файлу
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
        self.theme = theme if theme else default_styles
 | 
			
		||||
        self.theme_manager = ThemeManager()
 | 
			
		||||
        self.file_signal = FileSelectedSignal()
 | 
			
		||||
        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.path_history = {}  # Dictionary to store last selected item per directory
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
        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.update_file_list()
 | 
			
		||||
        else:
 | 
			
		||||
            if not self.directory_only:
 | 
			
		||||
                # Для файла отправляем нормализованный путь
 | 
			
		||||
                self.file_signal.file_selected.emit(os.path.normpath(full_path))
 | 
			
		||||
                self.accept()
 | 
			
		||||
            else:
 | 
			
		||||
                logger.debug("Selected item is not a directory, ignoring: %s", full_path)
 | 
			
		||||
 | 
			
		||||
    def previous_dir(self):
 | 
			
		||||
        """Возврат к родительской директории"""
 | 
			
		||||
@@ -288,14 +297,7 @@ class FileExplorer(QDialog):
 | 
			
		||||
            items = os.listdir(self.current_path)
 | 
			
		||||
            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):
 | 
			
		||||
                item = QListWidgetItem(f"{d}/")
 | 
			
		||||
                folder_icon = self.theme_manager.get_icon("folder")
 | 
			
		||||
@@ -307,6 +309,15 @@ class FileExplorer(QDialog):
 | 
			
		||||
                item.setIcon(folder_icon)
 | 
			
		||||
                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):
 | 
			
		||||
                    item = QListWidgetItem(f)
 | 
			
		||||
                    file_path = os.path.join(self.current_path, f)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import time
 | 
			
		||||
import threading
 | 
			
		||||
import os
 | 
			
		||||
from typing import Protocol, cast
 | 
			
		||||
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
 | 
			
		||||
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'):
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            if button_code in BUTTONS['confirm']:
 | 
			
		||||
                self.file_explorer.select_item()
 | 
			
		||||
            if button_code in BUTTONS['add_game']:
 | 
			
		||||
                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']:
 | 
			
		||||
                self.file_explorer.close()
 | 
			
		||||
            elif button_code in BUTTONS['prev_dir']:
 | 
			
		||||
 
 | 
			
		||||
@@ -696,59 +696,50 @@ class MainWindow(QMainWindow):
 | 
			
		||||
                loaded_count += 1
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        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:
 | 
			
		||||
            # Список игр и размер карточек не изменились, обновляем только видимость
 | 
			
		||||
        """Обновляет сетку игровых карточек с сохранением порядка сортировки"""
 | 
			
		||||
        # Подготовка данных
 | 
			
		||||
        games_list = games_list if games_list is not None else self.games
 | 
			
		||||
        search_text = self.searchEdit.text().strip().lower()
 | 
			
		||||
            for game_key, card in self.game_card_cache.items():
 | 
			
		||||
                game_name = game_key[0]
 | 
			
		||||
                card.setVisible(search_text in game_name.lower() or not search_text)
 | 
			
		||||
            self.loadVisibleImages()
 | 
			
		||||
            return
 | 
			
		||||
        favorites = read_favorites()
 | 
			
		||||
        sort_method = read_sort_method()
 | 
			
		||||
 | 
			
		||||
        # Обновляем размер карточек, если он изменился
 | 
			
		||||
        if card_width_changed:
 | 
			
		||||
            for card in self.game_card_cache.values():
 | 
			
		||||
                card.setFixedWidth(self.card_width + 20)  # Учитываем extra_margin в GameCard
 | 
			
		||||
        # Сортируем игры согласно текущим настройкам
 | 
			
		||||
        def sort_key(game):
 | 
			
		||||
            name = game[0]
 | 
			
		||||
            # Избранные всегда первые
 | 
			
		||||
            if name in favorites:
 | 
			
		||||
                fav_order = 0
 | 
			
		||||
            else:
 | 
			
		||||
                fav_order = 1
 | 
			
		||||
 | 
			
		||||
        # Удаляем карточки, которых больше нет в списке
 | 
			
		||||
        layout_changed = False
 | 
			
		||||
        for card_key in list(self.game_card_cache.keys()):
 | 
			
		||||
            if card_key not in current_games:
 | 
			
		||||
                card = self.game_card_cache.pop(card_key)
 | 
			
		||||
                self.gamesListLayout.removeWidget(card)
 | 
			
		||||
                card.deleteLater()
 | 
			
		||||
                if card_key in self.pending_images:
 | 
			
		||||
                    del self.pending_images[card_key]
 | 
			
		||||
                layout_changed = True
 | 
			
		||||
            if sort_method == "playtime":
 | 
			
		||||
                return (fav_order, -game[11], -game[10])  # playtime_seconds, last_launch_ts
 | 
			
		||||
            elif sort_method == "alphabetical":
 | 
			
		||||
                return (fav_order, name.lower())
 | 
			
		||||
            elif sort_method == "favorites":
 | 
			
		||||
                return (fav_order,)
 | 
			
		||||
            else:  # "last_launch" или по умолчанию
 | 
			
		||||
                return (fav_order, -game[10], -game[11])  # last_launch_ts, playtime_seconds
 | 
			
		||||
 | 
			
		||||
        # Добавляем новые карточки и обновляем существующие
 | 
			
		||||
        for game_data in games_list:
 | 
			
		||||
        sorted_games = sorted(games_list, key=sort_key)
 | 
			
		||||
 | 
			
		||||
        # Создаем временный список для новых карточек
 | 
			
		||||
        new_card_order = []
 | 
			
		||||
 | 
			
		||||
        # Обрабатываем каждую игру в отсортированном порядке
 | 
			
		||||
        for game_data in sorted_games:
 | 
			
		||||
            game_name = game_data[0]
 | 
			
		||||
            game_key = (game_name, game_data[4])
 | 
			
		||||
            search_text = self.searchEdit.text().strip().lower()
 | 
			
		||||
            should_be_visible = search_text in game_name.lower() or not search_text
 | 
			
		||||
            exec_line = game_data[4]
 | 
			
		||||
            game_key = (game_name, exec_line)
 | 
			
		||||
            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(
 | 
			
		||||
                *game_data,
 | 
			
		||||
@@ -757,8 +748,11 @@ class MainWindow(QMainWindow):
 | 
			
		||||
                card_width=self.card_width,
 | 
			
		||||
                context_menu_manager=self.context_menu_manager
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Подключаем сигналы
 | 
			
		||||
            card.hoverChanged.connect(self._on_card_hovered)
 | 
			
		||||
            card.focusChanged.connect(self._on_card_focused)
 | 
			
		||||
 | 
			
		||||
            # Подключаем сигналы контекстного меню
 | 
			
		||||
            card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
 | 
			
		||||
            card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
 | 
			
		||||
@@ -769,25 +763,42 @@ class MainWindow(QMainWindow):
 | 
			
		||||
            card.addToSteamRequested.connect(self.context_menu_manager.add_to_steam)
 | 
			
		||||
            card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam)
 | 
			
		||||
            card.openGameFolderRequested.connect(self.context_menu_manager.open_game_folder)
 | 
			
		||||
 | 
			
		||||
            # Добавляем в кэш и временный список
 | 
			
		||||
            self.game_card_cache[game_key] = card
 | 
			
		||||
                self.gamesListLayout.addWidget(card)
 | 
			
		||||
                layout_changed = True
 | 
			
		||||
            else:
 | 
			
		||||
                # Обновляем видимость существующей карточки
 | 
			
		||||
                card = self.game_card_cache[game_key]
 | 
			
		||||
            new_card_order.append((game_key, card))
 | 
			
		||||
            card.setVisible(should_be_visible)
 | 
			
		||||
 | 
			
		||||
        # Сохраняем текущий card_width
 | 
			
		||||
        self._last_card_width = self.card_width
 | 
			
		||||
        # Полностью перестраиваем макет в правильном порядке, чистим FlowLayout
 | 
			
		||||
        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.gamesListWidget.updateGeometry()
 | 
			
		||||
        self.gamesListWidget.update()
 | 
			
		||||
 | 
			
		||||
        # Загружаем изображения для видимых карточек
 | 
			
		||||
        self.loadVisibleImages()
 | 
			
		||||
        # Сохраняем текущий размер карточек
 | 
			
		||||
        self._last_card_width = self.card_width
 | 
			
		||||
 | 
			
		||||
    def clearLayout(self, layout):
 | 
			
		||||
        """Удаляет все виджеты из layout."""
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user