forked from Boria138/PortProtonQt
Compare commits
3 Commits
a6562ca488
...
48048a3f50
Author | SHA1 | Date | |
---|---|---|---|
48048a3f50
|
|||
7c617eef78
|
|||
08ba801f74
|
@@ -26,6 +26,7 @@
|
|||||||
- Ошибки темы в нативном пакете
|
- Ошибки темы в нативном пакете
|
||||||
- Ошибки темы в Gamescope
|
- Ошибки темы в Gamescope
|
||||||
- Размер иконок для desktop файлов теперь 128x128
|
- Размер иконок для desktop файлов теперь 128x128
|
||||||
|
- Пустая область при обновлении сетки игр
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
- @Dervart
|
- @Dervart
|
||||||
|
@@ -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.
|
||||||
|
@@ -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)
|
||||||
|
@@ -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']:
|
||||||
|
@@ -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."""
|
||||||
|
Reference in New Issue
Block a user