feat(file explorer): added ThumbnailLoader class

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2025-09-23 22:14:13 +05:00
parent 04d8302d6c
commit 3fb8201305

View File

@@ -6,7 +6,7 @@ from PySide6.QtGui import QPixmap, QIcon
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar
) )
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot
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, read_favorite_folders, read_theme_from_config from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
@@ -179,9 +179,11 @@ class FileExplorer(QDialog):
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
self.thumbnail_cache = {} # Cache for loaded thumbnails
self.pending_thumbnails = set() # Track files pending thumbnail loading
self.setup_ui() self.setup_ui()
# Настройки окна # Window settings
self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
@@ -208,8 +210,115 @@ class FileExplorer(QDialog):
self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid
self.update_file_list() self.update_file_list()
class ThumbnailLoader(QRunnable):
"""Class for asynchronous thumbnail loading in a separate thread."""
class Signals(QObject):
thumbnail_ready = Signal(str, QIcon) # Signal for ready thumbnail: file path and icon
def __init__(self, file_path, mime_type, size=64):
super().__init__()
self.file_path = file_path
self.mime_type = mime_type
self.size = size
self.signals = self.Signals()
@Slot()
def run(self):
"""Performs thumbnail loading in a background thread."""
try:
if self.mime_type.startswith("image/"):
pixmap = QPixmap(self.file_path)
if not pixmap.isNull():
scaled_pixmap = pixmap.scaled(self.size, self.size, Qt.AspectRatioMode.KeepAspectRatio)
self.signals.thumbnail_ready.emit(self.file_path, QIcon(scaled_pixmap))
else:
logger.warning("Failed to load image: %s", self.file_path)
elif self.file_path.lower().endswith(".exe"):
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
if generate_thumbnail(self.file_path, tmp.name, size=self.size):
pixmap = QPixmap(tmp.name)
if not pixmap.isNull():
self.signals.thumbnail_ready.emit(self.file_path, QIcon(pixmap))
os.unlink(tmp.name)
else:
logger.warning("Failed to generate thumbnail for .exe: %s", self.file_path)
except Exception as e:
logger.error("Error loading thumbnail for %s: %s", self.file_path, str(e))
def async_load_thumbnails(self, files, mime_db):
"""
Asynchronously loads thumbnails for a list of files.
Args:
files (list): List of file names to process.
mime_db (QMimeDatabase): QMimeDatabase instance for file type detection.
"""
thread_pool = QThreadPool.globalInstance()
thread_pool.setMaxThreadCount(4) # Limit the number of threads
for f in files:
file_path = os.path.join(self.current_path, f)
if file_path in self.thumbnail_cache or file_path in self.pending_thumbnails:
continue # Skip if already cached or pending
mime_type = mime_db.mimeTypeForFile(file_path).name()
if mime_type.startswith("image/") or file_path.lower().endswith(".exe"):
self.pending_thumbnails.add(file_path)
loader = self.ThumbnailLoader(file_path, mime_type, size=64)
loader.signals.thumbnail_ready.connect(self.update_thumbnail)
thread_pool.start(loader)
@Slot(str, QIcon)
def update_thumbnail(self, file_path, icon):
"""
Updates the icon for a file list item after thumbnail loading.
Args:
file_path (str): Path to the file for which the thumbnail was loaded.
icon (QIcon): Loaded icon.
"""
try:
# Cache the thumbnail
self.thumbnail_cache[file_path] = icon
self.pending_thumbnails.discard(file_path)
# Update the item in the file list
file_name = os.path.basename(file_path)
for i in range(self.file_list.count()):
item = self.file_list.item(i)
if item.text() == file_name:
item.setIcon(icon)
break
except Exception as e:
logger.error("Error updating thumbnail for %s: %s", file_path, str(e))
def load_visible_thumbnails(self):
"""Load thumbnails only for visible items in the file list."""
try:
visible_range = self.file_list.count()
first_visible = max(0, self.file_list.indexAt(self.file_list.viewport().rect().topLeft()).row())
last_visible = min(visible_range - 1, self.file_list.indexAt(self.file_list.viewport().rect().bottomRight()).row() + 5)
files_to_load = []
for i in range(first_visible, last_visible + 1):
item = self.file_list.item(i)
if not item:
continue
file_name = item.text()
if file_name.endswith("/"):
continue # Skip directories
file_path = os.path.join(self.current_path, file_name)
if file_path not in self.thumbnail_cache and file_path not in self.pending_thumbnails:
files_to_load.append(file_name)
if files_to_load:
self.async_load_thumbnails(files_to_load, self.mime_db)
except Exception as e:
logger.error("Error loading visible thumbnails: %s", str(e))
def get_mounted_drives(self): def get_mounted_drives(self):
"""Получение списка смонтированных дисков из /proc/mounts, исключая системные пути""" """Retrieve a list of mounted drives from /proc/mounts, excluding system paths."""
mounted_drives = [] mounted_drives = []
try: try:
with open('/proc/mounts') as f: with open('/proc/mounts') as f:
@@ -218,20 +327,20 @@ class FileExplorer(QDialog):
if len(parts) < 2: if len(parts) < 2:
continue continue
mount_point = parts[1] mount_point = parts[1]
# Исключаем системные и временные пути, но сохраняем /run/media # Exclude system and temporary paths, but keep /run/media
if (mount_point.startswith(('/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')) or if (mount_point.startswith(('/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')) or
(mount_point.startswith('/run') and not mount_point.startswith('/run/media'))): (mount_point.startswith('/run') and not mount_point.startswith('/run/media'))):
continue continue
# Проверяем, является ли точка монтирования директорией и доступна ли она # Check if the mount point is a directory and accessible
if os.path.isdir(mount_point) and os.access(mount_point, os.R_OK): if os.path.isdir(mount_point) and os.access(mount_point, os.R_OK):
mounted_drives.append(mount_point) mounted_drives.append(mount_point)
return sorted(mounted_drives) return sorted(mounted_drives)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при получении смонтированных дисков: {e}") logger.error(_("Error retrieving mounted drives: %s"), str(e))
return [] return []
def setup_ui(self): def setup_ui(self):
"""Настройка интерфейса""" """Set up the user interface."""
self.setWindowTitle(_("File Explorer")) self.setWindowTitle(_("File Explorer"))
self.setGeometry(100, 100, 600, 600) self.setGeometry(100, 100, 600, 600)
@@ -240,7 +349,7 @@ class FileExplorer(QDialog):
self.main_layout.setSpacing(10) self.main_layout.setSpacing(10)
self.setLayout(self.main_layout) self.setLayout(self.main_layout)
# Панель для смонтированных дисков и избранных папок # Panel for mounted drives and favorite folders
self.drives_layout = QHBoxLayout() self.drives_layout = QHBoxLayout()
self.drives_scroll = QScrollArea() self.drives_scroll = QScrollArea()
self.drives_scroll.setWidgetResizable(True) self.drives_scroll.setWidgetResizable(True)
@@ -253,12 +362,12 @@ class FileExplorer(QDialog):
self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# Путь # Path label
self.path_label = QLabel() self.path_label = QLabel()
self.path_label.setStyleSheet(self.theme.FILE_EXPLORER_PATH_LABEL_STYLE) self.path_label.setStyleSheet(self.theme.FILE_EXPLORER_PATH_LABEL_STYLE)
self.main_layout.addWidget(self.path_label) self.main_layout.addWidget(self.path_label)
# Список файлов # File list
self.file_list = QListWidget() self.file_list = QListWidget()
self.file_list.setStyleSheet(self.theme.FILE_EXPLORER_STYLE) self.file_list.setStyleSheet(self.theme.FILE_EXPLORER_STYLE)
self.file_list.itemClicked.connect(self.handle_item_click) self.file_list.itemClicked.connect(self.handle_item_click)
@@ -267,7 +376,10 @@ class FileExplorer(QDialog):
self.file_list.customContextMenuRequested.connect(self.show_folder_context_menu) self.file_list.customContextMenuRequested.connect(self.show_folder_context_menu)
self.main_layout.addWidget(self.file_list) self.main_layout.addWidget(self.file_list)
# Кнопки # Connect scroll signal for lazy loading
self.file_list.verticalScrollBar().valueChanged.connect(self.load_visible_thumbnails)
# Buttons
self.button_layout = QHBoxLayout() self.button_layout = QHBoxLayout()
self.button_layout.setSpacing(10) self.button_layout.setSpacing(10)
self.select_button = AutoSizeButton(_("Select"), icon=theme_manager.get_icon("apply")) self.select_button = AutoSizeButton(_("Select"), icon=theme_manager.get_icon("apply"))
@@ -286,43 +398,43 @@ class FileExplorer(QDialog):
if self.context_menu_manager: if self.context_menu_manager:
self.context_menu_manager.show_folder_context_menu(self, pos) self.context_menu_manager.show_folder_context_menu(self, pos)
else: else:
logger.warning("ContextMenuManager not found in parent") logger.warning(_("ContextMenuManager not found in parent"))
def move_selection(self, direction): def move_selection(self, direction):
"""Перемещение выбора по списку""" """Move selection in the list."""
current_row = self.file_list.currentRow() current_row = self.file_list.currentRow()
if direction < 0 and current_row > 0: # Вверх if direction < 0 and current_row > 0: # Up
self.file_list.setCurrentRow(current_row - 1) self.file_list.setCurrentRow(current_row - 1)
elif direction > 0 and current_row < self.file_list.count() - 1: # Вниз elif direction > 0 and current_row < self.file_list.count() - 1: # Down
self.file_list.setCurrentRow(current_row + 1) self.file_list.setCurrentRow(current_row + 1)
self.file_list.scrollToItem(self.file_list.currentItem()) self.file_list.scrollToItem(self.file_list.currentItem())
def handle_item_click(self, item): def handle_item_click(self, item):
"""Обработка одинарного клика мышью""" """Handle single mouse click."""
try: try:
self.file_list.setCurrentItem(item) self.file_list.setCurrentItem(item)
self.path_history[self.current_path] = item.text() # Сохраняем выбранный элемент self.path_history[self.current_path] = item.text() # Save selected item
logger.debug("Selected item: %s", item.text()) logger.debug("Selected item: %s", item.text())
except Exception as e: except Exception as e:
logger.error("Error in handle_item_click: %s", e) logger.error("Error in handle_item_click: %s", e)
def handle_item_double_click(self, item): def handle_item_double_click(self, item):
"""Обработка двойного клика мышью по элементу списка""" """Handle double mouse click on a list item."""
try: try:
self.file_list.setCurrentItem(item) self.file_list.setCurrentItem(item)
self.path_history[self.current_path] = item.text() # Сохраняем выбранный элемент self.path_history[self.current_path] = item.text() # Save selected item
selected = item.text() selected = item.text()
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 selected == "../": if selected == "../":
# Переходим в родительскую директорию # Navigate to parent directory
self.previous_dir() self.previous_dir()
else: else:
# Открываем директорию # Open directory
self.current_path = os.path.normpath(full_path) self.current_path = os.path.normpath(full_path)
self.update_file_list() self.update_file_list()
elif not self.directory_only: elif not self.directory_only:
# Выбираем файл, если directory_only=False # Select file if directory_only=False
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: else:
@@ -331,7 +443,7 @@ class FileExplorer(QDialog):
logger.error("Error in handle_item_double_click: %s", e) logger.error("Error in handle_item_double_click: %s", e)
def select_item(self): def select_item(self):
"""Обработка выбора файла/папки""" """Handle file/folder selection."""
if self.file_list.count() == 0: if self.file_list.count() == 0:
return return
@@ -340,30 +452,30 @@ class FileExplorer(QDialog):
if os.path.isdir(full_path): if os.path.isdir(full_path):
if self.directory_only: if self.directory_only:
# Подтверждаем выбор директории # Confirm directory selection
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: else:
# Открываем директорию # Open directory
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: if not self.directory_only:
# Для файла отправляем нормализованный путь # Emit normalized path for file
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: else:
logger.debug("Selected item is not a directory, ignoring: %s", full_path) logger.debug("Selected item is not a directory, ignoring: %s", full_path)
def previous_dir(self): def previous_dir(self):
"""Возврат к родительской директории""" """Navigate to parent directory."""
try: try:
if self.current_path == "/": if self.current_path == "/":
return # Уже в корне return # Already at root
# Нормализуем путь (убираем конечный слеш, если есть) # Normalize path (remove trailing slash if present)
normalized_path = os.path.normpath(self.current_path) normalized_path = os.path.normpath(self.current_path)
# Получаем родительскую директорию # Get parent directory
parent_dir = os.path.dirname(normalized_path) parent_dir = os.path.dirname(normalized_path)
if not parent_dir: if not parent_dir:
@@ -389,7 +501,7 @@ class FileExplorer(QDialog):
logger.error(f"Error ensuring button visible: {e}") logger.error(f"Error ensuring button visible: {e}")
def update_drives_list(self): def update_drives_list(self):
"""Обновление списка смонтированных дисков и избранных папок.""" """Update the list of mounted drives and favorite folders."""
for i in reversed(range(self.drives_layout.count())): for i in reversed(range(self.drives_layout.count())):
item = self.drives_layout.itemAt(i) item = self.drives_layout.itemAt(i)
if item and item.widget(): if item and item.widget():
@@ -401,7 +513,7 @@ class FileExplorer(QDialog):
drives = self.get_mounted_drives() drives = self.get_mounted_drives()
favorite_folders = read_favorite_folders() favorite_folders = read_favorite_folders()
# Добавляем смонтированные диски # Add mounted drives
for drive in drives: for drive in drives:
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
button = AutoSizeButton(drive_name, icon=theme_manager.get_icon("mount_point")) button = AutoSizeButton(drive_name, icon=theme_manager.get_icon("mount_point"))
@@ -411,7 +523,7 @@ class FileExplorer(QDialog):
self.drives_layout.addWidget(button) self.drives_layout.addWidget(button)
self.drive_buttons.append(button) self.drive_buttons.append(button)
# Добавляем избранные папки # Add favorite folders
for folder in favorite_folders: for folder in favorite_folders:
folder_name = os.path.basename(folder) or folder.split('/')[-1] or folder folder_name = os.path.basename(folder) or folder.split('/')[-1] or folder
button = AutoSizeButton(folder_name, icon=theme_manager.get_icon("folder")) button = AutoSizeButton(folder_name, icon=theme_manager.get_icon("folder"))
@@ -421,92 +533,92 @@ class FileExplorer(QDialog):
self.drives_layout.addWidget(button) self.drives_layout.addWidget(button)
self.drive_buttons.append(button) self.drive_buttons.append(button)
# Добавляем растяжку, чтобы выровнять элементы # Add spacer to align elements
spacer = QWidget() spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
self.drives_layout.addWidget(spacer) self.drives_layout.addWidget(spacer)
def select_drive(self): def select_drive(self):
"""Обрабатывает выбор диска или избранной папки через геймпад.""" """Handle drive or favorite folder selection via gamepad."""
focused_widget = QApplication.focusWidget() focused_widget = QApplication.focusWidget()
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.drive_buttons: if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.drive_buttons:
drive_name = focused_widget.text().strip() # Удаляем пробелы drive_name = focused_widget.text().strip() # Remove whitespace
logger.debug(f"Выбрано имя: {drive_name}") logger.debug(f"Selected name: {drive_name}")
# Специальная обработка корневого каталога # Special handling for root directory
if drive_name == "/": if drive_name == "/":
if os.path.isdir("/") and os.access("/", os.R_OK): if os.path.isdir("/") and os.access("/", os.R_OK):
self.current_path = "/" self.current_path = "/"
self.update_file_list() self.update_file_list()
logger.info("Выбран корневой каталог: /") logger.info("Selected root directory")
return return
else: else:
logger.warning("Корневой каталог недоступен: недостаточно прав или ошибка пути") logger.warning("Root directory is inaccessible: insufficient permissions or path error")
return return
# Проверяем избранные папки # Check favorite folders
favorite_folders = read_favorite_folders() favorite_folders = read_favorite_folders()
logger.debug(f"Избранные папки: {favorite_folders}") logger.debug(f"Favorite folders: {favorite_folders}")
for folder in favorite_folders: for folder in favorite_folders:
folder_name = os.path.basename(os.path.normpath(folder)) or folder # Для корневых путей folder_name = os.path.basename(os.path.normpath(folder)) or folder # For root paths
if folder_name == drive_name and os.path.isdir(folder) and os.access(folder, os.R_OK): if folder_name == drive_name and os.path.isdir(folder) and os.access(folder, os.R_OK):
self.current_path = os.path.normpath(folder) self.current_path = os.path.normpath(folder)
self.update_file_list() self.update_file_list()
logger.info(f"Выбрана избранная папка: {self.current_path}") logger.info(f"Selected favorite folder: {self.current_path}")
return return
# Проверяем смонтированные диски # Check mounted drives
mounted_drives = self.get_mounted_drives() mounted_drives = self.get_mounted_drives()
logger.debug(f"Смонтированные диски: {mounted_drives}") logger.debug(f"Mounted drives: {mounted_drives}")
for drive in mounted_drives: for drive in mounted_drives:
drive_basename = os.path.basename(os.path.normpath(drive)) or drive # Для корневых путей drive_basename = os.path.basename(os.path.normpath(drive)) or drive # For root paths
if drive_basename == drive_name and os.path.isdir(drive) and os.access(drive, os.R_OK): if drive_basename == drive_name and os.path.isdir(drive) and os.access(drive, os.R_OK):
self.current_path = os.path.normpath(drive) self.current_path = os.path.normpath(drive)
self.update_file_list() self.update_file_list()
logger.info(f"Выбран смонтированный диск: {self.current_path}") logger.info(f"Selected mounted drive: {self.current_path}")
return return
logger.warning(f"Путь недоступен: {drive_name}.") logger.warning(f"Path is inaccessible: {drive_name}.")
def change_drive(self, drive_path): def change_drive(self, drive_path):
"""Переход к выбранному диску""" """Navigate to the selected drive."""
if os.path.isdir(drive_path) and os.access(drive_path, os.R_OK): if os.path.isdir(drive_path) and os.access(drive_path, os.R_OK):
self.current_path = os.path.normpath(drive_path) self.current_path = os.path.normpath(drive_path)
self.update_file_list() self.update_file_list()
else: else:
logger.warning(f"Путь диска недоступен: {drive_path}") logger.warning(f"Drive path is inaccessible: {drive_path}")
def update_file_list(self): def update_file_list(self):
"""Обновление списка файлов с превью в виде иконок""" """Update the file list with asynchronous thumbnail loading."""
self.file_list.clear() self.file_list.clear()
self.thumbnail_cache.clear() # Clear cache when changing directories
self.pending_thumbnails.clear() # Clear pending thumbnails
try: try:
if self.current_path != "/": if self.current_path != "/":
item = QListWidgetItem("../") item = QListWidgetItem("../")
folder_icon = theme_manager.get_icon("folder") folder_icon = theme_manager.get_icon("folder")
# Ensure the icon is a QIcon
if isinstance(folder_icon, str) and os.path.isfile(folder_icon): if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
folder_icon = QIcon(folder_icon) folder_icon = QIcon(folder_icon)
elif not isinstance(folder_icon, QIcon): elif not isinstance(folder_icon, QIcon):
folder_icon = QIcon() # Fallback to empty icon folder_icon = QIcon()
item.setIcon(folder_icon) item.setIcon(folder_icon)
self.file_list.addItem(item) 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))]
# Добавляем директории # Add directories
for d in sorted(dirs): for d in sorted(dirs):
item = QListWidgetItem(f"{d}/") item = QListWidgetItem(f"{d}/")
folder_icon = theme_manager.get_icon("folder") folder_icon = theme_manager.get_icon("folder")
# Ensure the icon is a QIcon
if isinstance(folder_icon, str) and os.path.isfile(folder_icon): if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
folder_icon = QIcon(folder_icon) folder_icon = QIcon(folder_icon)
elif not isinstance(folder_icon, QIcon): elif not isinstance(folder_icon, QIcon):
folder_icon = QIcon() # Fallback to empty icon folder_icon = QIcon()
item.setIcon(folder_icon) item.setIcon(folder_icon)
self.file_list.addItem(item) self.file_list.addItem(item)
# Добавляем файлы только если directory_only=False # Add files only if directory_only=False
if not self.directory_only: if not self.directory_only:
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:
@@ -515,26 +627,14 @@ class FileExplorer(QDialog):
elif isinstance(self.file_filter, tuple): elif isinstance(self.file_filter, tuple):
files = [f for f in files if any(f.lower().endswith(ext) for ext in self.file_filter)] files = [f for f in files if any(f.lower().endswith(ext) for ext in self.file_filter)]
# Add files to the list without immediate thumbnail loading
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)
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)))
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)
# Load thumbnails for visible items only
self.load_visible_thumbnails()
self.path_label.setText(_("Path: ") + self.current_path) self.path_label.setText(_("Path: ") + self.current_path)
# Restore last selected item for this directory # Restore last selected item for this directory
@@ -556,10 +656,10 @@ class FileExplorer(QDialog):
self.file_list.setAlternatingRowColors(True) self.file_list.setAlternatingRowColors(True)
except PermissionError: except PermissionError:
self.path_label.setText(f"Access denied: {self.current_path}") self.path_label.setText(_("Access denied: %s") % self.current_path)
def closeEvent(self, event): def closeEvent(self, event):
"""Закрытие окна""" """Handle window closing."""
try: try:
if self.input_manager: if self.input_manager:
self.input_manager.disable_file_explorer_mode() self.input_manager.disable_file_explorer_mode()
@@ -573,13 +673,13 @@ class FileExplorer(QDialog):
super().closeEvent(event) super().closeEvent(event)
def reject(self): def reject(self):
"""Закрытие диалога""" """Close the dialog."""
if self.input_manager: if self.input_manager:
self.input_manager.disable_file_explorer_mode() self.input_manager.disable_file_explorer_mode()
super().reject() super().reject()
def accept(self): def accept(self):
"""Принятие диалога""" """Accept the dialog."""
if self.input_manager: if self.input_manager:
self.input_manager.disable_file_explorer_mode() self.input_manager.disable_file_explorer_mode()
super().accept() super().accept()