6 Commits

Author SHA1 Message Date
e6e46d1aee chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-27 14:20:58 +05:00
c64c01165d feat(FileExplorer): add preview icons
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-27 14:19:53 +05:00
4d7caa33b5 feat(FileExplorer): add prev dir action on Y and Square thanks to @Vector_null
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-27 10:37:51 +05:00
7fb05322ad fix: returned game list update on game delete
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-27 10:28:21 +05:00
fea5ff9877 feat(FileExplorer): add quick navigation for mounted drives
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-26 21:36:23 +05:00
dc06f78c43 fix(FileExplorer): normalize path handling for parent directory navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-26 21:10:44 +05:00
4 changed files with 138 additions and 16 deletions

View File

@@ -72,8 +72,8 @@
- [ ] Доделать светлую тему - [ ] Доделать светлую тему
- [ ] Добавить подсказки к управлению с геймпада - [ ] Добавить подсказки к управлению с геймпада
- [ ] Добавить загрузку звуков в темы например для добавления звука запуска в тему и тд - [ ] Добавить загрузку звуков в темы например для добавления звука запуска в тему и тд
- [ ] Добавить миниатюры к выбору файлов в диалоге добавления игры - [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
- [ ] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры - [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
### Установка (devel) ### Установка (devel)

View File

@@ -601,7 +601,6 @@ Icon={icon_path}
return False return False
def delete_game(self, game_name, exec_line): def delete_game(self, game_name, exec_line):
"""Delete the .desktop file and associated custom data for the game."""
reply = QMessageBox.question( reply = QMessageBox.question(
self.parent, self.parent,
_("Confirm Deletion"), _("Confirm Deletion"),
@@ -647,6 +646,10 @@ Icon={icon_path}
_("Failed to delete custom data: {error}").format(error=str(e)) _("Failed to delete custom data: {error}").format(error=str(e))
) )
# Перезагрузка списка игр и обновление сетки
self.load_games()
self.update_game_grid()
def add_to_menu(self, game_name, exec_line): def add_to_menu(self, game_name, exec_line):
"""Copy the .desktop file to ~/.local/share/applications.""" """Copy the .desktop file to ~/.local/share/applications."""
if not self._check_portproton(): if not self._check_portproton():

View File

@@ -1,12 +1,12 @@
import os import os
import tempfile import tempfile
from typing import cast, TYPE_CHECKING from typing import cast, TYPE_CHECKING
from PySide6.QtGui import QPixmap from PySide6.QtGui import QPixmap, QIcon
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QLineEdit, QFormLayout, QPushButton, QDialog, QLineEdit, QFormLayout, QPushButton,
QHBoxLayout, QDialogButtonBox, QLabel, QVBoxLayout, QListWidget QHBoxLayout, QDialogButtonBox, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem
) )
from PySide6.QtCore import Qt, QObject, Signal from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase
from icoextract import IconExtractor, IconExtractorError from icoextract import IconExtractor, IconExtractorError
from PIL import Image from PIL import Image
from portprotonqt.config_utils import get_portproton_location from portprotonqt.config_utils import get_portproton_location
@@ -92,6 +92,7 @@ class FileExplorer(QDialog):
super().__init__(parent) super().__init__(parent)
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.mime_db = QMimeDatabase() # Initialize QMimeDatabase for mimetype detection
self.setup_ui() self.setup_ui()
# Настройки окна # Настройки окна
@@ -111,16 +112,52 @@ class FileExplorer(QDialog):
if self.input_manager: if self.input_manager:
self.input_manager.enable_file_explorer_mode(self) self.input_manager.enable_file_explorer_mode(self)
# Initialize drives list
self.update_drives_list()
def get_mounted_drives(self):
"""Получение списка смонтированных дисков из /proc/mounts, исключая системные пути"""
mounted_drives = []
try:
with open('/proc/mounts') as f:
for line in f:
parts = line.strip().split()
if len(parts) < 2:
continue
mount_point = parts[1]
# Исключаем системные и временные пути
if mount_point.startswith(('/run', '/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')):
continue
# Проверяем, является ли точка монтирования директорией и доступна ли она
if os.path.isdir(mount_point) and os.access(mount_point, os.R_OK):
mounted_drives.append(mount_point)
return sorted(mounted_drives)
except Exception as e:
logger.error(f"Ошибка при получении смонтированных дисков: {e}")
return []
def setup_ui(self): def setup_ui(self):
"""Настройка интерфейса""" """Настройка интерфейса"""
self.setWindowTitle("File Explorer") self.setWindowTitle("File Explorer")
self.setGeometry(100, 100, 800, 600) self.setGeometry(100, 100, 600, 600)
self.main_layout = QVBoxLayout() self.main_layout = QVBoxLayout()
self.main_layout.setContentsMargins(10, 10, 10, 10) self.main_layout.setContentsMargins(10, 10, 10, 10)
self.main_layout.setSpacing(10) self.main_layout.setSpacing(10)
self.setLayout(self.main_layout) self.setLayout(self.main_layout)
# Панель для смонтированных дисков
self.drives_layout = QHBoxLayout()
self.drives_scroll = QScrollArea()
self.drives_scroll.setWidgetResizable(True)
self.drives_container = QWidget()
self.drives_container.setLayout(self.drives_layout)
self.drives_scroll.setWidget(self.drives_container)
self.drives_scroll.setStyleSheet(FileExplorerStyles.BUTTON_STYLE)
self.drives_scroll.setFixedHeight(50)
self.main_layout.addWidget(self.drives_scroll)
# Путь
self.path_label = QLabel() self.path_label = QLabel()
self.path_label.setStyleSheet(FileExplorerStyles.PATH_LABEL_STYLE) self.path_label.setStyleSheet(FileExplorerStyles.PATH_LABEL_STYLE)
self.main_layout.addWidget(self.path_label) self.main_layout.addWidget(self.path_label)
@@ -172,18 +209,65 @@ 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):
self.current_path = full_path # Если выбрана директория, нормализуем путь
self.current_path = os.path.normpath(full_path)
self.update_file_list() self.update_file_list()
else: else:
self.file_signal.file_selected.emit(full_path) # Для файла отправляем нормализованный путь
self.file_signal.file_selected.emit(os.path.normpath(full_path))
self.accept() self.accept()
def previous_dir(self):
"""Возврат к родительской директории"""
try:
if self.current_path == "/":
return # Уже в корне
# Нормализуем путь (убираем конечный слеш, если есть)
normalized_path = os.path.normpath(self.current_path)
# Получаем родительскую директорию
parent_dir = os.path.dirname(normalized_path)
if not parent_dir:
parent_dir = "/"
self.current_path = parent_dir
self.update_file_list()
except Exception as e:
logger.error(f"Error navigating to parent directory: {e}")
def update_drives_list(self):
"""Обновление списка смонтированных дисков"""
for i in reversed(range(self.drives_layout.count())):
widget = self.drives_layout.itemAt(i).widget()
if widget:
widget.deleteLater()
drives = self.get_mounted_drives()
for drive in drives:
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
button = QPushButton(drive_name)
button.setStyleSheet(FileExplorerStyles.BUTTON_STYLE)
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
self.drives_layout.addWidget(button)
self.drives_layout.addStretch()
def change_drive(self, drive_path):
"""Переход к выбранному диску"""
if 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 update_file_list(self): def update_file_list(self):
"""Обновление списка файлов""" """Обновление списка файлов с превью в виде иконок"""
self.file_list.clear() self.file_list.clear()
try: try:
if self.current_path != "/": if self.current_path != "/":
self.file_list.addItem("../") item = QListWidgetItem("../")
item.setIcon(QIcon.fromTheme("folder-symbolic"))
self.file_list.addItem(item)
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))]
@@ -191,13 +275,45 @@ class FileExplorer(QDialog):
# Apply file filter if provided # Apply file filter if provided
files = [f for f in items if os.path.isfile(os.path.join(self.current_path, f))] files = [f for f in items if os.path.isfile(os.path.join(self.current_path, f))]
if self.file_filter: if self.file_filter:
if isinstance(self.file_filter, str):
files = [f for f in files if f.lower().endswith(self.file_filter)] 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):
self.file_list.addItem(f"{d}/") item = QListWidgetItem(f"{d}/")
item.setIcon(QIcon.fromTheme("folder-symbolic"))
self.file_list.addItem(item)
for f in sorted(files): for f in sorted(files):
self.file_list.addItem(f) 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.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio)))
else:
item.setIcon(QIcon.fromTheme("image-x-generic-symbolic"))
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))
else:
item.setIcon(QIcon.fromTheme("application-x-executable-symbolic"))
os.unlink(tmp.name)
else:
item.setIcon(QIcon.fromTheme("application-x-executable-symbolic"))
else:
icon_name = self.mime_db.mimeTypeForFile(file_path).iconName()
symbolic_icon_name = icon_name + "-symbolic" if icon_name else "text-x-generic-symbolic"
item.setIcon(QIcon.fromTheme(symbolic_icon_name, QIcon.fromTheme("text-x-generic-symbolic")))
self.file_list.addItem(item)
self.path_label.setText(f"Path: {self.current_path}") self.path_label.setText(f"Path: {self.current_path}")
self.file_list.setCurrentRow(0) self.file_list.setCurrentRow(0)

View File

@@ -44,6 +44,7 @@ BUTTONS = {
'confirm': {ecodes.BTN_SOUTH}, # A (Xbox) / Cross (PS) 'confirm': {ecodes.BTN_SOUTH}, # A (Xbox) / Cross (PS)
'back': {ecodes.BTN_EAST}, # B (Xbox) / Circle (PS) 'back': {ecodes.BTN_EAST}, # B (Xbox) / Circle (PS)
'add_game': {ecodes.BTN_NORTH}, # X (Xbox) / Triangle (PS) 'add_game': {ecodes.BTN_NORTH}, # X (Xbox) / Triangle (PS)
'prev_dir': {ecodes.BTN_WEST}, # Y (Xbox) / Square (PS)
'prev_tab': {ecodes.BTN_TL}, # LB (Xbox) / L1 (PS) 'prev_tab': {ecodes.BTN_TL}, # LB (Xbox) / L1 (PS)
'next_tab': {ecodes.BTN_TR}, # RB (Xbox) / R1 (PS) 'next_tab': {ecodes.BTN_TR}, # RB (Xbox) / R1 (PS)
'context_menu': {ecodes.BTN_START}, # Start (Xbox) / Options (PS) 'context_menu': {ecodes.BTN_START}, # Start (Xbox) / Options (PS)
@@ -161,10 +162,12 @@ 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']: # Кнопка A if button_code in BUTTONS['confirm']:
self.file_explorer.select_item() self.file_explorer.select_item()
elif button_code in BUTTONS['back']: # Кнопка B elif button_code in BUTTONS['back']:
self.file_explorer.close() self.file_explorer.close()
elif button_code in BUTTONS['prev_dir']:
self.file_explorer.previous_dir()
else: else:
if self.original_button_handler: if self.original_button_handler:
self.original_button_handler(button_code) self.original_button_handler(button_code)