5 Commits

Author SHA1 Message Date
d7ab7dc7ce chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-01 15:56:01 +05:00
82a93abb7d chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-01 15:55:01 +05:00
25439889f7 feat(context_menu_manager): add quick game launch via context menu
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-01 15:52:39 +05:00
4e5ccfc374 feat(file-explorer): add gamepad support for selecting mounted drives
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-01 12:07:43 +05:00
3d87718072 feat(file-explorer): add file selection on double-click
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-01 11:48:28 +05:00
13 changed files with 155 additions and 58 deletions

View File

@@ -10,6 +10,7 @@
- Начальная поддержка EGS (Без EOS, скачивания игр и запуска игр из сторонних магазинов)
- Автодополнение bash для комманды portprotonqt
- Поддержка геймпадов в диалоге выбора игры
- Быстрый запуск игры через контекстное меню
### Changed
- Удалены сборки для Fedora 40

View File

@@ -20,9 +20,9 @@ Current translation status:
| Locale | Progress | Translated |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 182 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 182 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 182 of 182 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 183 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 183 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 183 of 183 |
---

View File

@@ -20,9 +20,9 @@
| Локаль | Прогресс | Переведено |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 182 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 182 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 182 из 182 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 183 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 183 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 183 из 183 |
---

View File

@@ -129,6 +129,11 @@ class ContextMenuManager:
menu = QMenu(self.parent)
menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE)
launch_action = menu.addAction(_("Launch Game"))
launch_action.triggered.connect(
lambda: self._launch_game(game_card)
)
favorites = read_favorites()
is_favorite = game_card.name in favorites
favorite_action = menu.addAction(
@@ -219,6 +224,40 @@ class ContextMenuManager:
menu.exec(game_card.mapToGlobal(pos))
def _launch_game(self, game_card):
"""
Launch a game using a validated exec_line, handling EGS games specifically.
Args:
game_card: The GameCard instance containing game data.
"""
if not self._check_portproton():
return
if game_card.game_source == "epic":
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
# Construct EGS launch command
wrapper = "flatpak run ru.linux_gaming.PortProton"
start_sh_path = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
if self.portproton_location and ".var" not in self.portproton_location:
wrapper = start_sh_path
if not os.path.exists(start_sh_path):
self.signals.show_warning_dialog.emit(
_("Error"),
_("start.sh not found at {path}").format(path=start_sh_path)
)
return
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
else:
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)
if not exec_line:
return
self.parent.toggleGame(exec_line)
def add_egs_to_steam(self, game_name: str, app_name: str):
"""
Adds an EGS game to Steam using the egs_api.

View File

@@ -3,7 +3,7 @@ import tempfile
from typing import cast, TYPE_CHECKING
from PySide6.QtGui import QPixmap, QIcon
from PySide6.QtWidgets import (
QDialog, QLineEdit, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy
QDialog, QLineEdit, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication
)
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
from icoextract import IconExtractor, IconExtractorError
@@ -167,6 +167,7 @@ class FileExplorer(QDialog):
self.drives_scroll.setFixedHeight(70)
self.main_layout.addWidget(self.drives_scroll)
self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Allow focus on scroll area
# Путь
self.path_label = QLabel()
@@ -177,7 +178,7 @@ class FileExplorer(QDialog):
self.file_list = QListWidget()
self.file_list.setStyleSheet(self.theme.FILE_EXPLORER_STYLE)
self.file_list.itemClicked.connect(self.handle_item_click)
self.file_list.itemDoubleClicked.connect(self.handle_item_double_click) # Подключение двойного клика
self.file_list.itemDoubleClicked.connect(self.handle_item_double_click)
self.main_layout.addWidget(self.file_list)
# Кнопки
@@ -227,6 +228,10 @@ class FileExplorer(QDialog):
# Открываем директорию
self.current_path = os.path.normpath(full_path)
self.update_file_list()
elif not self.directory_only:
# Выбираем файл, если directory_only=False
self.file_signal.file_selected.emit(os.path.normpath(full_path))
self.accept()
else:
logger.debug("Double-clicked item is not a directory, ignoring: %s", full_path)
except Exception as e:
@@ -287,14 +292,37 @@ class FileExplorer(QDialog):
widget.deleteLater()
drives = self.get_mounted_drives()
self.drive_buttons = [] # Store buttons for navigation
for drive in drives:
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
button = AutoSizeButton(drive_name, icon=self.theme_manager.get_icon("mount_point"))
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Make button focusable
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
self.drives_layout.addWidget(button)
self.drive_buttons.append(button)
self.drives_layout.addStretch()
# Set focus to first drive button if available
if self.drive_buttons:
self.drive_buttons[0].setFocus()
def select_drive(self):
"""Handle drive selection via gamepad"""
focused_widget = QApplication.focusWidget()
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.drive_buttons:
drive_path = None
for drive in self.get_mounted_drives():
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
if drive_name == focused_widget.text():
drive_path = drive
break
if drive_path and os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):
self.current_path = os.path.normpath(drive_path)
self.update_file_list()
else:
logger.warning(f"Путь диска недоступен: {drive_path}")
def change_drive(self, drive_path):
"""Переход к выбранному диску"""
if os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):

View File

@@ -9,7 +9,7 @@ from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
from PySide6.QtGui import QKeyEvent
from portprotonqt.logger import get_logger
from portprotonqt.image_utils import FullscreenDialog
from portprotonqt.custom_widgets import NavLabel
from portprotonqt.custom_widgets import NavLabel, AutoSizeButton
from portprotonqt.game_card import GameCard
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config
from portprotonqt.dialogs import AddGameDialog
@@ -162,20 +162,13 @@ class InputManager(QObject):
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list'):
return
if button_code in BUTTONS['add_game']:
if self.file_explorer.file_list.count() == 0:
focused_widget = QApplication.focusWidget()
if button_code in BUTTONS['confirm']: # A button (BTN_SOUTH)
if isinstance(focused_widget, AutoSizeButton) and hasattr(self.file_explorer, 'drive_buttons') and focused_widget in self.file_explorer.drive_buttons:
self.file_explorer.select_drive() # Select the focused drive
elif 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):
@@ -188,9 +181,20 @@ class InputManager(QObject):
self.file_explorer.accept()
else:
logger.debug("Selected item is not a directory, cannot select: %s", full_path)
elif button_code in BUTTONS['back']:
elif button_code in BUTTONS['add_game']: # X button
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['back']: # B button
self.file_explorer.close()
elif button_code in BUTTONS['prev_dir']:
elif button_code in BUTTONS['prev_dir']: # Y button
self.file_explorer.previous_dir()
else:
if self.original_button_handler:
@@ -204,15 +208,33 @@ class InputManager(QObject):
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list') or not self.file_explorer.file_list:
return
if not self.file_explorer.file_list.count():
focused_widget = QApplication.focusWidget()
if code in (ecodes.ABS_HAT0X, ecodes.ABS_X) and hasattr(self.file_explorer, 'drive_buttons') and self.file_explorer.drive_buttons:
# Navigate drive buttons horizontally
if not isinstance(focused_widget, AutoSizeButton) or focused_widget not in self.file_explorer.drive_buttons:
# If not focused on a drive button, focus the first one
self.file_explorer.drive_buttons[0].setFocus()
return
current_idx = self.file_explorer.drive_buttons.index(focused_widget)
if value < 0: # Left
next_idx = max(current_idx - 1, 0)
self.file_explorer.drive_buttons[next_idx].setFocus()
elif value > 0: # Right
next_idx = min(current_idx + 1, len(self.file_explorer.drive_buttons) - 1)
self.file_explorer.drive_buttons[next_idx].setFocus()
elif code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.file_explorer.drive_buttons:
# Move focus to file list if navigating down from drive buttons
if value > 0 and self.file_explorer.file_list.count() > 0:
self.file_explorer.file_list.setFocus()
self.file_explorer.file_list.setCurrentRow(0)
self.file_explorer.file_list.scrollToItem(self.file_explorer.file_list.currentItem())
return
if code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
# Для D-pad - реакция с фиксированной скоростью
if code == ecodes.ABS_HAT0Y:
if value != 0:
self.current_direction = value
self.stick_value = 1.0 # Максимальная скорость для D-pad, чтобы скачков не было
self.stick_value = 1.0 # Максимальная скорость для D-pad
if not self.nav_timer.isActive():
self.file_explorer.move_selection(self.current_direction)
self.last_nav_time = current_time
@@ -220,7 +242,6 @@ class InputManager(QObject):
else:
self.current_direction = 0
self.nav_timer.stop()
# Для стика - плавное управление с учетом степени отклонения
elif code == ecodes.ABS_Y:
if abs(value) < self.dead_zone:
@@ -229,19 +250,15 @@ class InputManager(QObject):
self.nav_timer.stop()
self.stick_activated = False
return
# Рассчитываем "силу" отклонения (0.3 - 1.0)
normalized_value = (abs(value) - self.dead_zone) / (32768 - self.dead_zone)
speed_factor = 0.3 + (normalized_value * 0.7) # От 30% до 100% скорости
self.current_direction = -1 if value < 0 else 1
self.stick_value = speed_factor
self.stick_activated = True
if not self.nav_timer.isActive():
self.file_explorer.move_selection(self.current_direction)
self.last_nav_time = current_time
self.nav_timer.start(int(self.initial_nav_delay * 1000))
elif self.original_dpad_handler:
self.original_dpad_handler(code, value, current_time)
except Exception as e:

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-07-01 00:15+0500\n"
"POT-Creation-Date: 2025-07-01 15:54+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n"
@@ -26,6 +26,9 @@ msgstr ""
msgid "PortProton is not found"
msgstr ""
msgid "Launch Game"
msgstr ""
msgid "Remove from Favorites"
msgstr ""
@@ -66,6 +69,10 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -110,10 +117,6 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-07-01 00:15+0500\n"
"POT-Creation-Date: 2025-07-01 15:54+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n"
@@ -26,6 +26,9 @@ msgstr ""
msgid "PortProton is not found"
msgstr ""
msgid "Launch Game"
msgstr ""
msgid "Remove from Favorites"
msgstr ""
@@ -66,6 +69,10 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -110,10 +117,6 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-07-01 00:15+0500\n"
"POT-Creation-Date: 2025-07-01 15:54+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -24,6 +24,9 @@ msgstr ""
msgid "PortProton is not found"
msgstr ""
msgid "Launch Game"
msgstr ""
msgid "Remove from Favorites"
msgstr ""
@@ -64,6 +67,10 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -108,10 +115,6 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""

View File

@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-07-01 00:15+0500\n"
"PO-Revision-Date: 2025-07-01 00:15+0500\n"
"POT-Creation-Date: 2025-07-01 15:54+0500\n"
"PO-Revision-Date: 2025-07-01 15:54+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
@@ -27,6 +27,9 @@ msgstr "Ошибка"
msgid "PortProton is not found"
msgstr "PortProton не найден"
msgid "Launch Game"
msgstr "Запустить игру"
msgid "Remove from Favorites"
msgstr "Удалить из Избранного"
@@ -67,6 +70,10 @@ msgstr "Удалить из PortProton"
msgid "Legendary executable not found at {path}"
msgstr "Legendary не найден по пути {path}"
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr "start.sh не найден по адресу {path}"
msgid "Success"
msgstr "Успешно"
@@ -113,10 +120,6 @@ msgstr "'{game_name}' был(а) добавлен(а) в избранное"
msgid "Removed '{game_name}' from favorites"
msgstr "'{game_name}' был(а) удалён(а) из избранного"
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr "start.sh не найден по адресу {path}"
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr "Запустить игру \"{name}\" с помощью PortProton"