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,35 +282,56 @@ 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 | ||||
|         def run_import(): | ||||
|             cmd = [self.legendary_path, "import", app_name, folder_path] | ||||
|             try: | ||||
|                 subprocess.run(cmd, capture_output=True, text=True, check=True) | ||||
|                 self.signals.show_info_dialog.emit( | ||||
|                     _("Success"), | ||||
|                     _("Imported '{game_name}' to Legendary").format(game_name=game_name) | ||||
|                 ) | ||||
|             except subprocess.CalledProcessError as e: | ||||
|                 self.signals.show_warning_dialog.emit( | ||||
|                     _("Error"), | ||||
|                     _("Failed to import '{game_name}' to Legendary: {error}").format( | ||||
|                         game_name=game_name, error=e.stderr | ||||
|  | ||||
|         # Используем 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: | ||||
|                     subprocess.run(cmd, capture_output=True, text=True, check=True) | ||||
|                     self.signals.show_info_dialog.emit( | ||||
|                         _("Success"), | ||||
|                         _("Imported '{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() | ||||
|                 except subprocess.CalledProcessError as e: | ||||
|                     self.signals.show_warning_dialog.emit( | ||||
|                         _("Error"), | ||||
|                         _("Failed to import '{game_name}' to Legendary: {error}").format( | ||||
|                             game_name=game_name, error=e.stderr | ||||
|                         ) | ||||
|                     ) | ||||
|             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): | ||||
|         """ | ||||
|   | ||||
| @@ -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): | ||||
|             # Если выбрана директория, нормализуем путь | ||||
|             self.current_path = os.path.normpath(full_path) | ||||
|             self.update_file_list() | ||||
|             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: | ||||
|             # Для файла отправляем нормализованный путь | ||||
|             self.file_signal.file_selected.emit(os.path.normpath(full_path)) | ||||
|             self.accept() | ||||
|             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,25 +309,34 @@ class FileExplorer(QDialog): | ||||
|                 item.setIcon(folder_icon) | ||||
|                 self.file_list.addItem(item) | ||||
|  | ||||
|             for f in sorted(files): | ||||
|                 item = QListWidgetItem(f) | ||||
|                 file_path = os.path.join(self.current_path, f) | ||||
|                 mime_type = self.mime_db.mimeTypeForFile(file_path).name() | ||||
|             # Добавляем файлы только если 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)] | ||||
|  | ||||
|                 if mime_type.startswith("image/"): | ||||
|                     pixmap = QPixmap(file_path) | ||||
|                     if not pixmap.isNull(): | ||||
|                         item.setIcon(QIcon(pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio))) | ||||
|                 elif file_path.lower().endswith(".exe"): | ||||
|                     tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False) | ||||
|                     tmp.close() | ||||
|                     if generate_thumbnail(file_path, tmp.name, size=64): | ||||
|                         pixmap = QPixmap(tmp.name) | ||||
|                 for f in sorted(files): | ||||
|                     item = QListWidgetItem(f) | ||||
|                     file_path = os.path.join(self.current_path, f) | ||||
|                     mime_type = self.mime_db.mimeTypeForFile(file_path).name() | ||||
|  | ||||
|                     if mime_type.startswith("image/"): | ||||
|                         pixmap = QPixmap(file_path) | ||||
|                         if not pixmap.isNull(): | ||||
|                             item.setIcon(QIcon(pixmap)) | ||||
|                         os.unlink(tmp.name) | ||||
|                             item.setIcon(QIcon(pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio))) | ||||
|                     elif file_path.lower().endswith(".exe"): | ||||
|                         tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False) | ||||
|                         tmp.close() | ||||
|                         if generate_thumbnail(file_path, tmp.name, size=64): | ||||
|                             pixmap = QPixmap(tmp.name) | ||||
|                             if not pixmap.isNull(): | ||||
|                                 item.setIcon(QIcon(pixmap)) | ||||
|                             os.unlink(tmp.name) | ||||
|  | ||||
|                 self.file_list.addItem(item) | ||||
|                     self.file_list.addItem(item) | ||||
|  | ||||
|             self.path_label.setText(_("Path: ") + self.current_path) | ||||
|  | ||||
|   | ||||
| @@ -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,98 +696,109 @@ 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 | ||||
|         """Обновляет сетку игровых карточек с сохранением порядка сортировки""" | ||||
|         # Подготовка данных | ||||
|         games_list = games_list if games_list is not None else self.games | ||||
|         search_text = self.searchEdit.text().strip().lower() | ||||
|         favorites = read_favorites() | ||||
|         sort_method = read_sort_method() | ||||
|  | ||||
|         # Создаем словарь текущих игр с уникальным ключом (name + exec_line) | ||||
|         current_games = {(game_data[0], game_data[4]): game_data for game_data in games_list} | ||||
|         # Сортируем игры согласно текущим настройкам | ||||
|         def sort_key(game): | ||||
|             name = game[0] | ||||
|             # Избранные всегда первые | ||||
|             if name in favorites: | ||||
|                 fav_order = 0 | ||||
|             else: | ||||
|                 fav_order = 1 | ||||
|  | ||||
|         # Проверяем, изменился ли список игр или размер карточек | ||||
|         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 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 | ||||
|  | ||||
|         if current_game_keys == cached_game_keys and not card_width_changed: | ||||
|             # Список игр и размер карточек не изменились, обновляем только видимость | ||||
|             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 | ||||
|         sorted_games = sorted(games_list, key=sort_key) | ||||
|  | ||||
|         # Обновляем размер карточек, если он изменился | ||||
|         if card_width_changed: | ||||
|             for card in self.game_card_cache.values(): | ||||
|                 card.setFixedWidth(self.card_width + 20)  # Учитываем extra_margin в GameCard | ||||
|         # Создаем временный список для новых карточек | ||||
|         new_card_order = [] | ||||
|  | ||||
|         # Удаляем карточки, которых больше нет в списке | ||||
|         layout_changed = False | ||||
|         # Обрабатываем каждую игру в отсортированном порядке | ||||
|         for game_data in sorted_games: | ||||
|             game_name = game_data[0] | ||||
|             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 | ||||
|  | ||||
|             # Создаем новую карточку | ||||
|             card = GameCard( | ||||
|                 *game_data, | ||||
|                 select_callback=self.openGameDetailPage, | ||||
|                 theme=self.theme, | ||||
|                 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) | ||||
|             card.addToMenuRequested.connect(self.context_menu_manager.add_to_menu) | ||||
|             card.removeFromMenuRequested.connect(self.context_menu_manager.remove_from_menu) | ||||
|             card.addToDesktopRequested.connect(self.context_menu_manager.add_to_desktop) | ||||
|             card.removeFromDesktopRequested.connect(self.context_menu_manager.remove_from_desktop) | ||||
|             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 | ||||
|             new_card_order.append((game_key, card)) | ||||
|             card.setVisible(should_be_visible) | ||||
|  | ||||
|         # Полностью перестраиваем макет в правильном порядке, чистим 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 current_games: | ||||
|             if card_key not in existing_keys: | ||||
|                 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 | ||||
|  | ||||
|         # Добавляем новые карточки и обновляем существующие | ||||
|         for game_data in games_list: | ||||
|             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 | ||||
|  | ||||
|             if game_key not in self.game_card_cache: | ||||
|                 # Создаем новую карточку | ||||
|                 card = GameCard( | ||||
|                     *game_data, | ||||
|                     select_callback=self.openGameDetailPage, | ||||
|                     theme=self.theme, | ||||
|                     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) | ||||
|                 card.addToMenuRequested.connect(self.context_menu_manager.add_to_menu) | ||||
|                 card.removeFromMenuRequested.connect(self.context_menu_manager.remove_from_menu) | ||||
|                 card.addToDesktopRequested.connect(self.context_menu_manager.add_to_desktop) | ||||
|                 card.removeFromDesktopRequested.connect(self.context_menu_manager.remove_from_desktop) | ||||
|                 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] | ||||
|                 card.setVisible(should_be_visible) | ||||
|  | ||||
|         # Сохраняем текущий card_width | ||||
|         self._last_card_width = self.card_width | ||||
|  | ||||
|         # Принудительно обновляем макет | ||||
|         if layout_changed: | ||||
|             self.gamesListLayout.update() | ||||
|             self.gamesListWidget.updateGeometry() | ||||
|             self.gamesListWidget.update() | ||||
|         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