12 Commits

Author SHA1 Message Date
Renovate Bot
cb07904c1b fix(deps): lock file maintenance python dependencies
Some checks failed
renovate/artifacts Artifact file update failure
Code check / Check code (pull_request) Successful in 1m18s
2025-08-24 17:39:06 +00:00
05e0d9d846 fix(renovate): disable poetry (bug in upstream)
All checks were successful
Code check / Check code (push) Successful in 1m25s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 22:35:30 +05:00
81433d3c56 chore(changelog): update
All checks were successful
Code check / Check code (push) Successful in 1m38s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 22:13:50 +05:00
0ff66e282b fix(input_manager): enable Escape key to close dialogs
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 22:10:39 +05:00
831b7739ba fix(input-manager): enable drive list navigation with arrow keys in FileExplorer
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 22:05:33 +05:00
50e1dfda57 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 20:56:41 +05:00
fcf04e521d feat(file-explorer): add automatic scrolling for drives layout
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 20:54:50 +05:00
74d0700d7c chore(renovate): use . for source uv
All checks were successful
Code check / Check code (push) Successful in 1m18s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 17:40:36 +05:00
0435c77630 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 17:39:52 +05:00
1cf93a60c8 feat: added favorites to file explorer
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-24 17:37:59 +05:00
31247d21c3 chore(changelog): update
Some checks failed
renovate / renovate (push) Failing after 1m1s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-23 21:35:23 +05:00
c6017a7dce fix(file explorer): don't skip /run/media
All checks were successful
Code check / Check code (push) Successful in 1m22s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-23 21:23:39 +05:00
8 changed files with 349 additions and 72 deletions

View File

@@ -20,7 +20,7 @@ jobs:
- name: Install uv manually - name: Install uv manually
run: | run: |
curl -LsSf https://astral.sh/uv/install.sh | sh curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env . $HOME/.local/bin/env
uv --version uv --version
- name: Download external renovate config - name: Download external renovate config

View File

@@ -9,6 +9,10 @@
- Больше типов анимаций при открытии карточки игры (подробности см. в документации). - Больше типов анимаций при открытии карточки игры (подробности см. в документации).
- Анимация при закрытии карточки игры (подробности см. в документации). - Анимация при закрытии карточки игры (подробности см. в документации).
- Добавлен обработчик нажатий стрелок на клавиатуре в поле ввода (позволяет перемещаться между символами с помощью стрелок). - Добавлен обработчик нажатий стрелок на клавиатуре в поле ввода (позволяет перемещаться между символами с помощью стрелок).
- Система быстрого доступа (избранного) в диалоге выбора файлов.
- Автоматическая прокрутка для панели дисков в диалоге выбора файлов.
- Возможность выбора папок и / или дисков в диалоге выбора файлов через клавиатуру.
- Поднятия в родительскую директорию в диалоге выбора файлов на BackSpace.
### Changed ### Changed
- Уменьшена длительность анимации открытия карточки с 800 до 350 мс. - Уменьшена длительность анимации открытия карточки с 800 до 350 мс.
@@ -28,6 +32,8 @@
- Вкладки больше не переключаются стрелками, если фокус в поле ввода. - Вкладки больше не переключаются стрелками, если фокус в поле ввода.
- Исправлено переключение слайдера: RT (Xbox) / R2 (PS), LT (Xbox) / L2 (PS). - Исправлено переключение слайдера: RT (Xbox) / R2 (PS), LT (Xbox) / L2 (PS).
- Переведен заголовок окна диалога выбора файлов. - Переведен заголовок окна диалога выбора файлов.
- Отображение устройств смонтированных в /run/media в диалоге выбора файлов.
- Закрытие диалога добавления / редактирования игры и диалога выбора файлов через escape.
### Contributors ### Contributors
- @Alex Smith - @Alex Smith

View File

@@ -549,3 +549,41 @@ def save_auto_fullscreen_gamepad(auto_fullscreen):
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen) cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
def read_favorite_folders():
"""
Читает список избранных папок из секции [FavoritesFolders] конфигурационного файла.
Список хранится как строка, заключённая в кавычки, с путями, разделёнными запятыми.
Если секция или параметр отсутствуют, возвращает пустой список.
"""
cp = configparser.ConfigParser()
if os.path.exists(CONFIG_FILE):
try:
cp.read(CONFIG_FILE, encoding="utf-8")
except Exception as e:
logger.error("Ошибка чтения конфига: %s", e)
return []
if cp.has_section("FavoritesFolders") and cp.has_option("FavoritesFolders", "folders"):
favs = cp.get("FavoritesFolders", "folders", fallback="").strip()
if favs.startswith('"') and favs.endswith('"'):
favs = favs[1:-1]
return [os.path.normpath(s.strip()) for s in favs.split(",") if s.strip() and os.path.isdir(os.path.normpath(s.strip()))]
return []
def save_favorite_folders(folders):
"""
Сохраняет список избранных папок в секцию [FavoritesFolders] конфигурационного файла.
Список сохраняется как строка, заключённая в двойные кавычки, где пути разделены запятыми.
"""
cp = configparser.ConfigParser()
if os.path.exists(CONFIG_FILE):
try:
cp.read(CONFIG_FILE, encoding="utf-8")
except Exception as e:
logger.error("Ошибка чтения конфига: %s", e)
if "FavoritesFolders" not in cp:
cp["FavoritesFolders"] = {}
fav_str = ", ".join([os.path.normpath(folder) for folder in folders])
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)

View File

@@ -12,7 +12,7 @@ from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QLineEdit, QApplicati
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
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, read_favorite_folders, save_favorite_folders
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, FileExplorer, generate_thumbnail from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
@@ -150,6 +150,84 @@ class ContextMenuManager:
return hasattr(self.parent, 'target_exe') and self.parent.target_exe == current_exe return hasattr(self.parent, 'target_exe') and self.parent.target_exe == current_exe
def show_folder_context_menu(self, file_explorer, pos):
"""Shows the context menu for a folder in FileExplorer."""
try:
item = file_explorer.file_list.itemAt(pos)
if not item:
logger.debug("No item selected at position %s", pos)
return
selected = item.text()
if not selected.endswith("/"):
logger.debug("Selected item is not a folder: %s", selected)
return # Only for folders
full_path = os.path.normpath(os.path.join(file_explorer.current_path, selected.rstrip("/")))
if not os.path.isdir(full_path):
logger.debug("Path is not a directory: %s", full_path)
return
menu = QMenu(file_explorer)
menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE)
menu.setParent(file_explorer, Qt.WindowType.Popup) # Set transientParent for Wayland
favorite_folders = read_favorite_folders()
is_favorite = full_path in favorite_folders
action_text = _("Remove from Favorites") if is_favorite else _("Add to Favorites")
favorite_action = menu.addAction(self._get_safe_icon("star" if is_favorite else "star_full"), action_text)
favorite_action.triggered.connect(lambda: self.toggle_favorite_folder(file_explorer, full_path, not is_favorite))
# Disconnect file_list signals to prevent navigation during menu interaction
try:
file_explorer.file_list.itemClicked.disconnect(file_explorer.handle_item_click)
file_explorer.file_list.itemDoubleClicked.disconnect(file_explorer.handle_item_double_click)
except TypeError:
pass # Signals may not be connected
# Reconnect signals after menu closes
def reconnect_signals():
try:
file_explorer.file_list.itemClicked.connect(file_explorer.handle_item_click)
file_explorer.file_list.itemDoubleClicked.connect(file_explorer.handle_item_double_click)
except Exception as e:
logger.error("Error reconnecting file list signals: %s", e)
menu.aboutToHide.connect(reconnect_signals)
# Set focus to the first menu item
actions = menu.actions()
if actions:
menu.setActiveAction(actions[0])
# Map local position to global for menu display
global_pos = file_explorer.file_list.mapToGlobal(pos)
menu.exec(global_pos)
except Exception as e:
logger.error("Error showing folder context menu: %s", e)
def toggle_favorite_folder(self, file_explorer, folder_path, add):
"""Adds or removes a folder from favorites."""
favorite_folders = read_favorite_folders()
if add:
if folder_path not in favorite_folders:
favorite_folders.append(folder_path)
save_favorite_folders(favorite_folders)
logger.info(f"Folder added to favorites: {folder_path}")
else:
if folder_path in favorite_folders:
favorite_folders.remove(folder_path)
save_favorite_folders(favorite_folders)
logger.info(f"Folder removed from favorites: {folder_path}")
file_explorer.update_drives_list()
def _get_safe_icon(self, icon_name: str) -> QIcon:
"""Returns a QIcon, ensuring it is valid."""
icon = self.theme_manager.get_icon(icon_name)
if isinstance(icon, QIcon):
return icon
elif isinstance(icon, str) and os.path.exists(icon):
return QIcon(icon)
return QIcon()
def show_context_menu(self, game_card, pos: QPoint): def show_context_menu(self, game_card, pos: QPoint):
""" """
Show the context menu for a game card at the specified position. Show the context menu for a game card at the specified position.
@@ -158,14 +236,6 @@ class ContextMenuManager:
game_card: The GameCard instance requesting the context menu. game_card: The GameCard instance requesting the context menu.
pos: The position (in widget coordinates) where the menu should appear. pos: The position (in widget coordinates) where the menu should appear.
""" """
def get_safe_icon(icon_name: str) -> QIcon:
icon = self.theme_manager.get_icon(icon_name)
if isinstance(icon, QIcon):
return icon
elif isinstance(icon, str) and os.path.exists(icon):
return QIcon(icon)
return QIcon()
menu = QMenu(self.parent) menu = QMenu(self.parent)
menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE) menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE)
@@ -175,7 +245,7 @@ class ContextMenuManager:
exe_path = self._parse_exe_path(exec_line, game_card.name) if exec_line else None exe_path = self._parse_exe_path(exec_line, game_card.name) if exec_line else None
if not exe_path: if not exe_path:
# Show only "Delete from PortProton" if no valid exe # Show only "Delete from PortProton" if no valid exe
delete_action = menu.addAction(get_safe_icon("delete"), _("Delete from PortProton")) delete_action = menu.addAction(self._get_safe_icon("delete"), _("Delete from PortProton"))
delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line)) delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line))
menu.exec(game_card.mapToGlobal(pos)) menu.exec(game_card.mapToGlobal(pos))
return return
@@ -184,7 +254,7 @@ class ContextMenuManager:
is_running = self._is_game_running(game_card) is_running = self._is_game_running(game_card)
action_text = _("Stop Game") if is_running else _("Launch Game") action_text = _("Stop Game") if is_running else _("Launch Game")
action_icon = "stop" if is_running else "play" action_icon = "stop" if is_running else "play"
launch_action = menu.addAction(get_safe_icon(action_icon), action_text) launch_action = menu.addAction(self._get_safe_icon(action_icon), action_text)
launch_action.triggered.connect( launch_action.triggered.connect(
lambda: self._launch_game(game_card) lambda: self._launch_game(game_card)
) )
@@ -193,11 +263,11 @@ class ContextMenuManager:
is_favorite = game_card.name in favorites is_favorite = game_card.name in favorites
icon_name = "star_full" if is_favorite else "star" icon_name = "star_full" if is_favorite else "star"
text = _("Remove from Favorites") if is_favorite else _("Add to Favorites") text = _("Remove from Favorites") if is_favorite else _("Add to Favorites")
favorite_action = menu.addAction(get_safe_icon(icon_name), text) favorite_action = menu.addAction(self._get_safe_icon(icon_name), text)
favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, not is_favorite)) favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, not is_favorite))
if game_card.game_source == "epic": if game_card.game_source == "epic":
import_action = menu.addAction(get_safe_icon("epic_games"), _("Import to Legendary")) import_action = menu.addAction(self._get_safe_icon("epic_games"), _("Import to Legendary"))
import_action.triggered.connect( import_action.triggered.connect(
lambda: self.import_to_legendary(game_card.name, game_card.appid) lambda: self.import_to_legendary(game_card.name, game_card.appid)
) )
@@ -205,13 +275,13 @@ class ContextMenuManager:
is_in_steam = is_game_in_steam(game_card.name) is_in_steam = is_game_in_steam(game_card.name)
icon_name = "delete" if is_in_steam else "steam" icon_name = "delete" if is_in_steam else "steam"
text = _("Remove from Steam") if is_in_steam else _("Add to Steam") text = _("Remove from Steam") if is_in_steam else _("Add to Steam")
steam_action = menu.addAction(get_safe_icon(icon_name), text) steam_action = menu.addAction(self._get_safe_icon(icon_name), text)
steam_action.triggered.connect( steam_action.triggered.connect(
lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source) lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source)
if is_in_steam if is_in_steam
else self.add_egs_to_steam(game_card.name, game_card.appid) else self.add_egs_to_steam(game_card.name, game_card.appid)
) )
open_folder_action = menu.addAction(get_safe_icon("search"), _("Open Game Folder")) open_folder_action = menu.addAction(self._get_safe_icon("search"), _("Open Game Folder"))
open_folder_action.triggered.connect( open_folder_action.triggered.connect(
lambda: self.open_egs_game_folder(game_card.appid) lambda: self.open_egs_game_folder(game_card.appid)
) )
@@ -219,7 +289,7 @@ class ContextMenuManager:
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop") desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
icon_name = "delete" if os.path.exists(desktop_path) else "desktop" icon_name = "delete" if os.path.exists(desktop_path) else "desktop"
text = _("Remove from Desktop") if os.path.exists(desktop_path) else _("Add to Desktop") text = _("Remove from Desktop") if os.path.exists(desktop_path) else _("Add to Desktop")
desktop_action = menu.addAction(get_safe_icon(icon_name), text) desktop_action = menu.addAction(self._get_safe_icon(icon_name), text)
desktop_action.triggered.connect( desktop_action.triggered.connect(
lambda: self.remove_egs_from_desktop(game_card.name) lambda: self.remove_egs_from_desktop(game_card.name)
if os.path.exists(desktop_path) if os.path.exists(desktop_path)
@@ -228,7 +298,7 @@ class ContextMenuManager:
applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications") applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications")
menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop") menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop")
menu_action = menu.addAction( menu_action = menu.addAction(
get_safe_icon("delete" if os.path.exists(menu_path) else "menu"), self._get_safe_icon("delete" if os.path.exists(menu_path) else "menu"),
_("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu") _("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu")
) )
menu_action.triggered.connect( menu_action.triggered.connect(
@@ -242,19 +312,19 @@ class ContextMenuManager:
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop") desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
icon_name = "delete" if os.path.exists(desktop_path) else "desktop" icon_name = "delete" if os.path.exists(desktop_path) else "desktop"
text = _("Remove from Desktop") if os.path.exists(desktop_path) else _("Add to Desktop") text = _("Remove from Desktop") if os.path.exists(desktop_path) else _("Add to Desktop")
desktop_action = menu.addAction(get_safe_icon(icon_name), text) desktop_action = menu.addAction(self._get_safe_icon(icon_name), text)
desktop_action.triggered.connect( desktop_action.triggered.connect(
lambda: self.remove_from_desktop(game_card.name) lambda: self.remove_from_desktop(game_card.name)
if os.path.exists(desktop_path) if os.path.exists(desktop_path)
else self.add_to_desktop(game_card.name, game_card.exec_line) else self.add_to_desktop(game_card.name, game_card.exec_line)
) )
edit_action = menu.addAction(get_safe_icon("edit"), _("Edit Shortcut")) edit_action = menu.addAction(self._get_safe_icon("edit"), _("Edit Shortcut"))
edit_action.triggered.connect( edit_action.triggered.connect(
lambda: self.edit_game_shortcut(game_card.name, game_card.exec_line, game_card.cover_path) lambda: self.edit_game_shortcut(game_card.name, game_card.exec_line, game_card.cover_path)
) )
delete_action = menu.addAction(get_safe_icon("delete"), _("Delete from PortProton")) delete_action = menu.addAction(self._get_safe_icon("delete"), _("Delete from PortProton"))
delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line)) delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line))
open_folder_action = menu.addAction(get_safe_icon("search"), _("Open Game Folder")) open_folder_action = menu.addAction(self._get_safe_icon("search"), _("Open Game Folder"))
open_folder_action.triggered.connect( open_folder_action.triggered.connect(
lambda: self.open_game_folder(game_card.name, game_card.exec_line) lambda: self.open_game_folder(game_card.name, game_card.exec_line)
) )
@@ -262,7 +332,7 @@ class ContextMenuManager:
menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop") menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop")
icon_name = "delete" if os.path.exists(menu_path) else "menu" icon_name = "delete" if os.path.exists(menu_path) else "menu"
text = _("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu") text = _("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu")
menu_action = menu.addAction(get_safe_icon(icon_name), text) menu_action = menu.addAction(self._get_safe_icon(icon_name), text)
menu_action.triggered.connect( menu_action.triggered.connect(
lambda: self.remove_from_menu(game_card.name) lambda: self.remove_from_menu(game_card.name)
if os.path.exists(menu_path) if os.path.exists(menu_path)
@@ -271,7 +341,7 @@ class ContextMenuManager:
is_in_steam = is_game_in_steam(game_card.name) is_in_steam = is_game_in_steam(game_card.name)
icon_name = "delete" if is_in_steam else "steam" icon_name = "delete" if is_in_steam else "steam"
text = _("Remove from Steam") if is_in_steam else _("Add to Steam") text = _("Remove from Steam") if is_in_steam else _("Add to Steam")
steam_action = menu.addAction(get_safe_icon(icon_name), text) steam_action = menu.addAction(self._get_safe_icon(icon_name), text)
steam_action.triggered.connect( steam_action.triggered.connect(
lambda: ( lambda: (
self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source) self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source)
@@ -280,7 +350,7 @@ class ContextMenuManager:
) )
) )
# Устанавливаем фокус на первый элемент меню # Set focus to the first menu item
actions = menu.actions() actions = menu.actions()
if actions: if actions:
menu.setActiveAction(actions[0]) menu.setActiveAction(actions[0])
@@ -422,7 +492,7 @@ class ContextMenuManager:
) )
return return
# Используем FileExplorer с directory_only=True # Use FileExplorer with directory_only=True
file_explorer = FileExplorer( file_explorer = FileExplorer(
parent=self.parent, parent=self.parent,
theme=self.theme, theme=self.theme,
@@ -452,10 +522,10 @@ 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()
# Подключаем сигнал выбора файла/папки # Connect the file selection signal
file_explorer.file_signal.file_selected.connect(on_folder_selected) file_explorer.file_signal.file_selected.connect(on_folder_selected)
# Центрируем FileExplorer относительно родительского виджета # Center FileExplorer relative to the parent widget
parent_widget = self.parent parent_widget = self.parent
if parent_widget: if parent_widget:
parent_geometry = parent_widget.geometry() parent_geometry = parent_widget.geometry()
@@ -789,7 +859,7 @@ Icon={icon_path}
_("Failed to delete custom data: {error}").format(error=str(e)) _("Failed to delete custom data: {error}").format(error=str(e))
) )
# Перезагрузка списка игр и обновление сетки # Reload games list and update grid
self.load_games() self.load_games()
self.update_game_grid() self.update_game_grid()

View File

@@ -9,7 +9,7 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
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, read_favorite_folders
from portprotonqt.localization import _ from portprotonqt.localization import _
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
import portprotonqt.themes.standart.styles as default_styles import portprotonqt.themes.standart.styles as default_styles
@@ -106,13 +106,15 @@ class FileExplorer(QDialog):
self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
# Find InputManager from parent # Find InputManager and ContextMenuManager from parent
self.input_manager = None self.input_manager = None
self.context_menu_manager = None
parent = self.parent() parent = self.parent()
while parent: while parent:
if hasattr(parent, 'input_manager'): if hasattr(parent, 'input_manager'):
self.input_manager = cast("MainWindow", parent).input_manager self.input_manager = cast("MainWindow", parent).input_manager
break if hasattr(parent, 'context_menu_manager'):
self.context_menu_manager = cast("MainWindow", parent).context_menu_manager
parent = parent.parent() parent = parent.parent()
if self.input_manager: if self.input_manager:
@@ -137,8 +139,9 @@ class FileExplorer(QDialog):
if len(parts) < 2: if len(parts) < 2:
continue continue
mount_point = parts[1] mount_point = parts[1]
# Исключаем системные и временные пути # Исключаем системные и временные пути, но сохраняем /run/media
if mount_point.startswith(('/run', '/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')): if (mount_point.startswith(('/dev', '/sys', '/proc', '/tmp', '/snap', '/var/lib')) or
(mount_point.startswith('/run') and not mount_point.startswith('/run/media'))):
continue continue
# Проверяем, является ли точка монтирования директорией и доступна ли она # Проверяем, является ли точка монтирования директорией и доступна ли она
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):
@@ -158,7 +161,7 @@ class FileExplorer(QDialog):
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_layout = QHBoxLayout()
self.drives_scroll = QScrollArea() self.drives_scroll = QScrollArea()
self.drives_scroll.setWidgetResizable(True) self.drives_scroll.setWidgetResizable(True)
@@ -169,7 +172,7 @@ class FileExplorer(QDialog):
self.drives_scroll.setFixedHeight(70) self.drives_scroll.setFixedHeight(70)
self.main_layout.addWidget(self.drives_scroll) self.main_layout.addWidget(self.drives_scroll)
self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.drives_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Allow focus on scroll area self.drives_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# Путь # Путь
self.path_label = QLabel() self.path_label = QLabel()
@@ -181,6 +184,8 @@ class FileExplorer(QDialog):
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)
self.file_list.itemDoubleClicked.connect(self.handle_item_double_click) self.file_list.itemDoubleClicked.connect(self.handle_item_double_click)
self.file_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.file_list.customContextMenuRequested.connect(self.show_folder_context_menu)
self.main_layout.addWidget(self.file_list) self.main_layout.addWidget(self.file_list)
# Кнопки # Кнопки
@@ -197,6 +202,13 @@ class FileExplorer(QDialog):
self.select_button.clicked.connect(self.select_item) self.select_button.clicked.connect(self.select_item)
self.cancel_button.clicked.connect(self.reject) self.cancel_button.clicked.connect(self.reject)
def show_folder_context_menu(self, pos):
"""Shows the context menu for a folder using ContextMenuManager."""
if self.context_menu_manager:
self.context_menu_manager.show_folder_context_menu(self, pos)
else:
logger.warning("ContextMenuManager not found in parent")
def move_selection(self, direction): def move_selection(self, direction):
"""Перемещение выбора по списку""" """Перемещение выбора по списку"""
current_row = self.file_list.currentRow() current_row = self.file_list.currentRow()
@@ -286,44 +298,96 @@ class FileExplorer(QDialog):
except Exception as e: except Exception as e:
logger.error(f"Error navigating to parent directory: {e}") logger.error(f"Error navigating to parent directory: {e}")
def ensure_button_visible(self, button):
"""Ensure the specified button is visible in the drives_scroll area."""
try:
if not button or not self.drives_scroll:
return
# Ensure the button is visible in the scroll area
self.drives_scroll.ensureWidgetVisible(button, 50, 50)
logger.debug(f"Ensured button {button.text()} is visible in drives_scroll")
except Exception as e:
logger.error(f"Error ensuring button visible: {e}")
def update_drives_list(self): def update_drives_list(self):
"""Обновление списка смонтированных дисков""" """Обновление списка смонтированных дисков и избранных папок."""
for i in reversed(range(self.drives_layout.count())): for i in reversed(range(self.drives_layout.count())):
widget = self.drives_layout.itemAt(i).widget() item = self.drives_layout.itemAt(i)
if widget: if item and item.widget():
widget = item.widget()
self.drives_layout.removeWidget(widget)
widget.deleteLater() widget.deleteLater()
self.drive_buttons = []
drives = self.get_mounted_drives() drives = self.get_mounted_drives()
self.drive_buttons = [] # Store buttons for navigation favorite_folders = read_favorite_folders()
# Добавляем смонтированные диски
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=self.theme_manager.get_icon("mount_point")) button = AutoSizeButton(drive_name, icon=self.theme_manager.get_icon("mount_point"))
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Make button focusable button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
button.clicked.connect(lambda checked, path=drive: self.change_drive(path)) button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
self.drives_layout.addWidget(button) self.drives_layout.addWidget(button)
self.drive_buttons.append(button) self.drive_buttons.append(button)
self.drives_layout.addStretch()
# Set focus to first drive button if available # Добавляем избранные папки
if self.drive_buttons: for folder in favorite_folders:
self.drive_buttons[0].setFocus() folder_name = os.path.basename(folder) or folder.split('/')[-1] or folder
button = AutoSizeButton(folder_name, icon=self.theme_manager.get_icon("folder"))
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
button.clicked.connect(lambda checked, path=folder: self.change_drive(path))
self.drives_layout.addWidget(button)
self.drive_buttons.append(button)
# Добавляем растяжку, чтобы выровнять элементы
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
self.drives_layout.addWidget(spacer)
def select_drive(self): def select_drive(self):
"""Handle drive 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_path = None drive_name = focused_widget.text().strip() # Удаляем пробелы
for drive in self.get_mounted_drives(): logger.debug(f"Выбрано имя: {drive_name}")
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
if drive_name == focused_widget.text(): # Специальная обработка корневого каталога
drive_path = drive if drive_name == "/":
break if os.path.isdir("/") and os.access("/", os.R_OK):
if drive_path and os.path.isdir(drive_path) and os.access(drive_path, os.R_OK): self.current_path = "/"
self.current_path = os.path.normpath(drive_path) self.update_file_list()
self.update_file_list() logger.info("Выбран корневой каталог: /")
else: return
logger.warning(f"Путь диска недоступен: {drive_path}") else:
logger.warning("Корневой каталог недоступен: недостаточно прав или ошибка пути")
return
# Проверяем избранные папки
favorite_folders = read_favorite_folders()
logger.debug(f"Избранные папки: {favorite_folders}")
for folder in favorite_folders:
folder_name = os.path.basename(os.path.normpath(folder)) or folder # Для корневых путей
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.update_file_list()
logger.info(f"Выбрана избранная папка: {self.current_path}")
return
# Проверяем смонтированные диски
mounted_drives = self.get_mounted_drives()
logger.debug(f"Смонтированные диски: {mounted_drives}")
for drive in mounted_drives:
drive_basename = os.path.basename(os.path.normpath(drive)) or drive # Для корневых путей
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.update_file_list()
logger.info(f"Выбран смонтированный диск: {self.current_path}")
return
logger.warning(f"Путь недоступен: {drive_name}.")
def change_drive(self, drive_path): def change_drive(self, drive_path):
"""Переход к выбранному диску""" """Переход к выбранному диску"""

View File

@@ -4,7 +4,7 @@ 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
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
from PySide6.QtGui import QKeyEvent from PySide6.QtGui import QKeyEvent
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
@@ -161,7 +161,20 @@ class InputManager(QObject):
def handle_file_explorer_button(self, button_code): def handle_file_explorer_button(self, button_code):
try: try:
popup = QApplication.activePopupWidget()
if isinstance(popup, QMenu):
if button_code in BUTTONS['confirm']: # A button (BTN_SOUTH)
if popup.activeAction():
popup.activeAction().trigger()
popup.close()
return
elif button_code in BUTTONS['back']: # B button
popup.close()
return
return # Skip other handling if menu is open
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'):
logger.debug("No file explorer or file_list available")
return return
focused_widget = QApplication.focusWidget() focused_widget = QApplication.focusWidget()
@@ -169,27 +182,37 @@ class InputManager(QObject):
if isinstance(focused_widget, AutoSizeButton) and hasattr(self.file_explorer, 'drive_buttons') and focused_widget in self.file_explorer.drive_buttons: 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 self.file_explorer.select_drive() # Select the focused drive
elif self.file_explorer.file_list.count() == 0: elif self.file_explorer.file_list.count() == 0:
logger.debug("File list is empty")
return return
else: else:
selected = self.file_explorer.file_list.currentItem().text() selected = self.file_explorer.file_list.currentItem().text()
full_path = os.path.join(self.file_explorer.current_path, selected) full_path = os.path.join(self.file_explorer.current_path, selected)
if os.path.isdir(full_path): if os.path.isdir(full_path):
# Открываем директорию
self.file_explorer.current_path = os.path.normpath(full_path) self.file_explorer.current_path = os.path.normpath(full_path)
self.file_explorer.update_file_list() self.file_explorer.update_file_list()
elif not self.file_explorer.directory_only: elif not self.file_explorer.directory_only:
# Выбираем файл, если directory_only=False
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path)) self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
self.file_explorer.accept() self.file_explorer.accept()
else: else:
logger.debug("Selected item is not a directory, cannot select: %s", full_path) logger.debug("Selected item is not a directory, cannot select: %s", full_path)
elif button_code in BUTTONS['context_menu']: # Start button (BTN_START)
if self.file_explorer.file_list.count() == 0:
logger.debug("File list is empty, cannot show context menu")
return
current_item = self.file_explorer.file_list.currentItem()
if current_item:
item_rect = self.file_explorer.file_list.visualItemRect(current_item)
pos = item_rect.center() # Use local coordinates for itemAt check
self.file_explorer.show_folder_context_menu(pos)
else:
logger.debug("No item selected for context menu")
elif button_code in BUTTONS['add_game']: # X button elif button_code in BUTTONS['add_game']: # X button
if self.file_explorer.file_list.count() == 0: if self.file_explorer.file_list.count() == 0:
logger.debug("File list is empty")
return return
selected = self.file_explorer.file_list.currentItem().text() selected = self.file_explorer.file_list.currentItem().text()
full_path = os.path.join(self.file_explorer.current_path, selected) full_path = os.path.join(self.file_explorer.current_path, selected)
if os.path.isdir(full_path): if os.path.isdir(full_path):
# Подтверждаем выбор директории
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path)) self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
self.file_explorer.accept() self.file_explorer.accept()
else: else:
@@ -202,12 +225,29 @@ class InputManager(QObject):
if self.original_button_handler: if self.original_button_handler:
self.original_button_handler(button_code) self.original_button_handler(button_code)
except Exception as e: except Exception as e:
logger.error(f"Error in FileExplorer button handler: {e}") logger.error("Error in FileExplorer button handler: %s", e)
def handle_file_explorer_dpad(self, code, value, current_time): def handle_file_explorer_dpad(self, code, value, current_time):
"""Обработка движения D-pad и левого стика для FileExplorer""" """Обработка движения D-pad и левого стика для FileExplorer"""
try: try:
popup = QApplication.activePopupWidget()
if isinstance(popup, QMenu):
if code == ecodes.ABS_HAT0Y and value != 0:
actions = popup.actions()
if not actions:
return
current_action = popup.activeAction()
current_idx = actions.index(current_action) if current_action in actions else -1
if value > 0: # Down
next_idx = (current_idx + 1) % len(actions) if current_idx != -1 else 0
popup.setActiveAction(actions[next_idx])
elif value < 0: # Up
next_idx = (current_idx - 1) % len(actions) if current_idx != -1 else len(actions) - 1
popup.setActiveAction(actions[next_idx])
return # Skip other handling if menu is open
if not self.file_explorer or not hasattr(self.file_explorer, 'file_list') or not self.file_explorer.file_list: if not self.file_explorer or not hasattr(self.file_explorer, 'file_list') or not self.file_explorer.file_list:
logger.debug("No file explorer or file_list available")
return return
focused_widget = QApplication.focusWidget() focused_widget = QApplication.focusWidget()
@@ -216,14 +256,17 @@ class InputManager(QObject):
if not isinstance(focused_widget, AutoSizeButton) or focused_widget not in self.file_explorer.drive_buttons: 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 # If not focused on a drive button, focus the first one
self.file_explorer.drive_buttons[0].setFocus() self.file_explorer.drive_buttons[0].setFocus()
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[0])
return return
current_idx = self.file_explorer.drive_buttons.index(focused_widget) current_idx = self.file_explorer.drive_buttons.index(focused_widget)
if value < 0: # Left if value < 0: # Left
next_idx = max(current_idx - 1, 0) next_idx = max(current_idx - 1, 0)
self.file_explorer.drive_buttons[next_idx].setFocus() self.file_explorer.drive_buttons[next_idx].setFocus()
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[next_idx])
elif value > 0: # Right elif value > 0: # Right
next_idx = min(current_idx + 1, len(self.file_explorer.drive_buttons) - 1) next_idx = min(current_idx + 1, len(self.file_explorer.drive_buttons) - 1)
self.file_explorer.drive_buttons[next_idx].setFocus() self.file_explorer.drive_buttons[next_idx].setFocus()
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[next_idx])
elif code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y): elif code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y):
if isinstance(focused_widget, AutoSizeButton) and focused_widget in self.file_explorer.drive_buttons: 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 # Move focus to file list if navigating down from drive buttons
@@ -264,7 +307,7 @@ class InputManager(QObject):
elif self.original_dpad_handler: elif self.original_dpad_handler:
self.original_dpad_handler(code, value, current_time) self.original_dpad_handler(code, value, current_time)
except Exception as e: except Exception as e:
logger.error(f"Error in FileExplorer dpad handler: {e}") logger.error("Error in FileExplorer dpad handler: %s", e)
def handle_navigation_repeat(self): def handle_navigation_repeat(self):
"""Плавное повторение движения с переменной скоростью для FileExplorer""" """Плавное повторение движения с переменной скоростью для FileExplorer"""
@@ -742,6 +785,11 @@ class InputManager(QObject):
if not app: if not app:
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
# Ensure obj is a QObject
if not isinstance(obj, QObject):
logger.debug(f"Skipping event filter for non-QObject: {type(obj).__name__}")
return False
# Handle key press and release events # Handle key press and release events
if not isinstance(event, QKeyEvent): if not isinstance(event, QKeyEvent):
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
@@ -754,6 +802,54 @@ class InputManager(QObject):
# Handle key press events # Handle key press events
if event.type() == QEvent.Type.KeyPress: if event.type() == QEvent.Type.KeyPress:
# Handle FileExplorer specific logic
if self.file_explorer:
# Handle drive buttons in FileExplorer
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
if isinstance(focused, AutoSizeButton) and hasattr(self.file_explorer, 'drive_buttons') and focused in self.file_explorer.drive_buttons:
self.file_explorer.select_drive()
return True
elif isinstance(focused, QListWidget) and focused == self.file_explorer.file_list:
current_item = focused.currentItem()
if current_item:
selected = current_item.text()
full_path = os.path.join(self.file_explorer.current_path, selected)
if os.path.isdir(full_path):
if selected == "../":
self.file_explorer.previous_dir()
else:
self.file_explorer.current_path = os.path.normpath(full_path)
self.file_explorer.update_file_list()
elif not self.file_explorer.directory_only:
self.file_explorer.file_signal.file_selected.emit(os.path.normpath(full_path))
self.file_explorer.accept()
return True
else:
self._parent.activateFocusedWidget()
return True
# Handle FileExplorer navigation with right arrow key
if key == Qt.Key.Key_Right:
try:
if hasattr(self.file_explorer, 'drive_buttons') and self.file_explorer.drive_buttons:
if not isinstance(focused, AutoSizeButton) or focused not in self.file_explorer.drive_buttons:
self.file_explorer.drive_buttons[0].setFocus()
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[0])
else:
current_idx = self.file_explorer.drive_buttons.index(focused)
next_idx = min(current_idx + 1, len(self.file_explorer.drive_buttons) - 1)
self.file_explorer.drive_buttons[next_idx].setFocus()
self.file_explorer.ensure_button_visible(self.file_explorer.drive_buttons[next_idx])
return True
except Exception as e:
logger.error(f"Error handling right arrow in FileExplorer: {e}")
return True
# Handle Backspace for FileExplorer navigation
if key == Qt.Key.Key_Backspace:
self.file_explorer.previous_dir()
return True
# Handle QLineEdit cursor movement with Left/Right arrows # Handle QLineEdit cursor movement with Left/Right arrows
if isinstance(focused, QLineEdit) and key in (Qt.Key.Key_Left, Qt.Key.Key_Right): if isinstance(focused, QLineEdit) and key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
if key == Qt.Key.Key_Left: if key == Qt.Key.Key_Left:
@@ -778,10 +874,13 @@ class InputManager(QObject):
self.file_explorer.previous_dir() self.file_explorer.previous_dir()
return True return True
# Close AddGameDialog with Escape # Close Dialogs with Escape
if key == Qt.Key.Key_Escape and isinstance(popup, QDialog): if key == Qt.Key.Key_Escape:
popup.reject() if isinstance(focused, QLineEdit):
return True return False
if isinstance(active_win, QDialog):
active_win.reject()
return True
# FullscreenDialog navigation # FullscreenDialog navigation
if isinstance(active_win, FullscreenDialog): if isinstance(active_win, FullscreenDialog):
@@ -797,7 +896,7 @@ class InputManager(QObject):
return True # Consume event to prevent tab switching return True # Consume event to prevent tab switching
# Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit # Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and (not isinstance(focused, GameCard | QLineEdit) or focused is None): if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and not isinstance(focused, GameCard | QLineEdit) and not self.file_explorer:
idx = self._parent.stackedWidget.currentIndex() idx = self._parent.stackedWidget.currentIndex()
total = len(self._parent.tabButtons) total = len(self._parent.tabButtons)
if key == Qt.Key.Key_Left: if key == Qt.Key.Key_Left:

View File

@@ -27,7 +27,7 @@ classifiers = [
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"babel>=2.17.0", "babel>=2.17.0",
"beautifulsoup4>=4.13.4", "beautifulsoup4>=4.13.5",
"evdev>=1.9.2", "evdev>=1.9.2",
"icoextract>=0.2.0", "icoextract>=0.2.0",
"numpy>=2.2.4", "numpy>=2.2.4",
@@ -36,7 +36,7 @@ dependencies = [
"psutil>=7.0.0", "psutil>=7.0.0",
"pyside6>=6.9.1", "pyside6>=6.9.1",
"pyudev>=0.24.3", "pyudev>=0.24.3",
"requests>=2.32.4", "requests>=2.32.5",
"tqdm>=4.67.1", "tqdm>=4.67.1",
"vdf>=3.4", "vdf>=3.4",
"websocket-client>=1.8.0", "websocket-client>=1.8.0",
@@ -105,5 +105,5 @@ ignore = [
dev = [ dev = [
"pre-commit>=4.3.0", "pre-commit>=4.3.0",
"pyaspeller>=2.0.2", "pyaspeller>=2.0.2",
"pyright>=1.1.403", "pyright>=1.1.404",
] ]

View File

@@ -19,7 +19,7 @@
"enabled": false "enabled": false
}, },
{ {
"matchManagers": ["github-actions", "pre-commit"], "matchManagers": ["github-actions", "pre-commit", "poetry"],
"enabled": false "enabled": false
}, },
{ {