Compare commits

...

2 Commits

Author SHA1 Message Date
3ab943bb60 feat(dialogs): added styles for standart theme to FileExplorer
Some checks failed
Code and build check / Check code (pull_request) Failing after 1m43s
Code and build check / Build with uv (pull_request) Successful in 1m2s
2025-06-27 19:58:49 +00:00
4a48ec302d refactor(context_menu): clean code
All checks were successful
Code and build check / Check code (push) Successful in 1m36s
Code and build check / Build with uv (push) Successful in 52s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-28 00:08:10 +05:00
7 changed files with 209 additions and 200 deletions

View File

@ -5,16 +5,14 @@ import shutil
import subprocess
import threading
import logging
import re
import orjson
import vdf
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
from PySide6.QtGui import QDesktopServices
from portprotonqt.localization import _
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam, get_steam_home, get_last_steam_user, convert_steam_id
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable
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.dialogs import AddGameDialog, generate_thumbnail
logger = logging.getLogger(__name__)
@ -893,124 +891,7 @@ Icon={icon_path}
)
if game_source == "epic":
# For EGS games, construct the script path used in Steam shortcuts.vdf
if not self.portproton_location:
self.signals.show_warning_dialog.emit(
_("Error"),
_("PortProton directory not found")
)
return
steam_scripts_dir = os.path.join(self.portproton_location, "steam_scripts")
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}_egs.sh")
quoted_script_path = f'"{script_path}"'
# Directly remove the shortcut by matching AppName and Exe
try:
steam_home = get_steam_home()
if not steam_home:
self.signals.show_warning_dialog.emit(_("Error"), _("Steam directory not found"))
return
last_user = get_last_steam_user(steam_home)
if not last_user or 'SteamID' not in last_user:
self.signals.show_warning_dialog.emit(_("Error"), _("Failed to get Steam user ID"))
return
userdata_dir = os.path.join(steam_home, "userdata")
user_id = last_user['SteamID']
unsigned_id = convert_steam_id(user_id)
user_dir = os.path.join(userdata_dir, str(unsigned_id))
steam_shortcuts_path = os.path.join(user_dir, "config", "shortcuts.vdf")
backup_path = f"{steam_shortcuts_path}.backup"
if not os.path.exists(steam_shortcuts_path):
self.signals.show_warning_dialog.emit(
_("Error"),
_("Steam shortcuts file not found")
)
return
# Backup shortcuts.vdf
try:
shutil.copy2(steam_shortcuts_path, backup_path)
logger.info("Created backup of shortcuts.vdf at %s", backup_path)
except Exception as e:
self.signals.show_warning_dialog.emit(
_("Error"),
_("Failed to create backup of shortcuts.vdf: {error}").format(error=str(e))
)
return
# Load shortcuts.vdf
try:
with open(steam_shortcuts_path, 'rb') as f:
shortcuts_data = vdf.binary_load(f)
except Exception as e:
self.signals.show_warning_dialog.emit(
_("Error"),
_("Failed to load shortcuts.vdf: {error}").format(error=str(e))
)
return
shortcuts = shortcuts_data.get("shortcuts", {})
modified = False
new_shortcuts = {}
index = 0
for _key, entry in shortcuts.items():
if entry.get("AppName") == game_name and entry.get("Exe") == quoted_script_path:
modified = True
logger.info("Removing EGS game '%s' from Steam shortcuts", game_name)
continue
new_shortcuts[str(index)] = entry
index += 1
if not modified:
self.signals.show_warning_dialog.emit(
_("Error"),
_("Game '{game_name}' not found in Steam shortcuts").format(game_name=game_name)
)
return
# Save updated shortcuts.vdf
try:
with open(steam_shortcuts_path, 'wb') as f:
vdf.binary_dump({"shortcuts": new_shortcuts}, f)
logger.info("Updated shortcuts.vdf, removed '%s'", game_name)
on_remove_from_steam_result((True, "Game '{game_name}' was removed from Steam. Please restart Steam for changes to take effect."))
except Exception as e:
self.signals.show_warning_dialog.emit(
_("Error"),
_("Failed to update shortcuts.vdf: {error}").format(error=str(e))
)
if os.path.exists(backup_path):
try:
shutil.copy2(backup_path, steam_shortcuts_path)
logger.info("Restored shortcuts.vdf from backup")
except Exception as restore_err:
logger.error("Failed to restore shortcuts.vdf: %s", restore_err)
on_remove_from_steam_result((False, "Failed to update shortcuts.vdf: {error}"))
return
# Optionally, remove the script file
if os.path.exists(script_path):
try:
os.remove(script_path)
logger.info("Removed EGS script: %s", script_path)
except OSError as e:
logger.warning(f"Failed to remove EGS script '{script_path}': {str(e)}")
except Exception as e:
self.signals.show_warning_dialog.emit(
_("Error"),
_("Failed to remove EGS game '{game_name}' from Steam: {error}").format(
game_name=game_name, error=str(e)
)
)
on_remove_from_steam_result((False, "Failed to remove EGS game '{game_name}' from Steam: {error}"))
return
remove_egs_from_steam(game_name, self.portproton_location, on_remove_from_steam_result)
else:
# For non-EGS games, use steam_api
exec_line = self._get_exec_line(game_name, exec_line)

View File

@ -13,7 +13,8 @@ from portprotonqt.config_utils import get_portproton_location
from portprotonqt.localization import _
from portprotonqt.logger import get_logger
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.themes.standart.styles import FileExplorerStyles
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.custom_widgets import AutoSizeButton
if TYPE_CHECKING:
from portprotonqt.main_window import MainWindow
@ -88,8 +89,10 @@ class FileSelectedSignal(QObject):
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
class FileExplorer(QDialog):
def __init__(self, parent=None, file_filter=None):
def __init__(self, parent=None, theme=None, file_filter=None):
super().__init__(parent)
self.theme = theme if theme else default_styles
self.theme_manager = ThemeManager()
self.file_signal = FileSelectedSignal()
self.file_filter = file_filter # Store the file filter
self.mime_db = QMimeDatabase() # Initialize QMimeDatabase for mimetype detection
@ -98,7 +101,6 @@ class FileExplorer(QDialog):
# Настройки окна
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
self.setStyleSheet(FileExplorerStyles.WINDOW_STYLE)
# Find InputManager from parent
self.input_manager = None
@ -153,28 +155,28 @@ class FileExplorer(QDialog):
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.drives_scroll.setStyleSheet(self.theme.SCROLL_AREA_STYLE)
self.drives_scroll.setFixedHeight(60)
self.main_layout.addWidget(self.drives_scroll)
# Путь
self.path_label = QLabel()
self.path_label.setStyleSheet(FileExplorerStyles.PATH_LABEL_STYLE)
self.path_label.setStyleSheet(self.theme.FILE_EXPLORER_PATH_LABEL_STYLE)
self.main_layout.addWidget(self.path_label)
# Список файлов
self.file_list = QListWidget()
self.file_list.setStyleSheet(FileExplorerStyles.LIST_STYLE)
self.file_list.setStyleSheet(self.theme.FILE_EXPLORER_STYLE)
self.file_list.itemClicked.connect(self.handle_item_click)
self.main_layout.addWidget(self.file_list)
# Кнопки
self.button_layout = QHBoxLayout()
self.button_layout.setSpacing(10)
self.select_button = QPushButton(_("Select"))
self.cancel_button = QPushButton(_("Cancel"))
self.select_button.setStyleSheet(FileExplorerStyles.BUTTON_STYLE)
self.cancel_button.setStyleSheet(FileExplorerStyles.BUTTON_STYLE)
self.select_button = AutoSizeButton(_("Select"), icon=self.theme_manager.get_icon("apply"))
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.button_layout.addWidget(self.select_button)
self.button_layout.addWidget(self.cancel_button)
self.main_layout.addLayout(self.button_layout)
@ -246,8 +248,8 @@ class FileExplorer(QDialog):
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 = AutoSizeButton(drive_name, icon=self.theme_manager.get_icon("mount_point"))
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
self.drives_layout.addWidget(button)
self.drives_layout.addStretch()
@ -266,7 +268,7 @@ class FileExplorer(QDialog):
try:
if self.current_path != "/":
item = QListWidgetItem("../")
item.setIcon(QIcon.fromTheme("folder-symbolic"))
item.setIcon(self.theme_manager.get_icon("folder"))
self.file_list.addItem(item)
items = os.listdir(self.current_path)
@ -282,7 +284,7 @@ class FileExplorer(QDialog):
for d in sorted(dirs):
item = QListWidgetItem(f"{d}/")
item.setIcon(QIcon.fromTheme("folder-symbolic"))
item.setIcon(self.theme_manager.get_icon("folder"))
self.file_list.addItem(item)
for f in sorted(files):

View File

@ -27,18 +27,12 @@ from PySide6.QtGui import QPixmap
logger = get_logger(__name__)
downloader = Downloader()
def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None:
"""Получает путь к исполняемому файлу EGS-игры из installed.json с использованием orjson."""
def read_installed_json(legendary_config_path: str) -> dict | None:
"""Читает installed.json и возвращает словарь с данными или None в случае ошибки."""
installed_json_path = os.path.join(legendary_config_path, "installed.json")
try:
with open(installed_json_path, "rb") as f:
installed_data = orjson.loads(f.read())
if app_name in installed_data:
install_path = installed_data[app_name].get("install_path", "").decode('utf-8') if isinstance(installed_data[app_name].get("install_path"), bytes) else installed_data[app_name].get("install_path", "")
executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "")
if install_path and executable:
return os.path.join(install_path, executable)
return None
return orjson.loads(f.read())
except FileNotFoundError:
logger.error(f"installed.json not found at {installed_json_path}")
return None
@ -49,6 +43,17 @@ def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None:
logger.error(f"Error reading installed.json: {e}")
return None
def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None:
"""Получает путь к исполняемому файлу EGS-игры из installed.json."""
installed_data = read_installed_json(legendary_config_path)
if installed_data is None or app_name not in installed_data:
return None
install_path = installed_data[app_name].get("install_path", "").decode('utf-8') if isinstance(installed_data[app_name].get("install_path"), bytes) else installed_data[app_name].get("install_path", "")
executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "")
if install_path and executable:
return os.path.join(install_path, executable)
return None
def get_cache_dir() -> Path:
"""Returns the path to the cache directory, creating it if necessary."""
xdg_cache_home = os.getenv(
@ -59,6 +64,108 @@ def get_cache_dir() -> Path:
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir
def remove_egs_from_steam(game_name: str, portproton_dir: str, callback: Callable[[tuple[bool, str]], None]) -> None:
"""
Removes an EGS game from Steam by modifying shortcuts.vdf and deleting the launch script.
Calls the callback with (success, message).
Args:
game_name: The display name of the game.
portproton_dir: Path to the PortProton directory.
callback: Callback function to handle the result (success, message).
"""
if not portproton_dir:
logger.error("PortProton directory not found")
callback((False, "PortProton directory not found"))
return
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}_egs.sh")
quoted_script_path = f'"{script_path}"'
steam_home = get_steam_home()
if not steam_home:
logger.error("Steam directory not found")
callback((False, "Steam directory not found"))
return
last_user = get_last_steam_user(steam_home)
if not last_user or 'SteamID' not in last_user:
logger.error("Failed to retrieve Steam user ID")
callback((False, "Failed to get Steam user ID"))
return
userdata_dir = os.path.join(steam_home, "userdata")
user_id = last_user['SteamID']
unsigned_id = convert_steam_id(user_id)
user_dir = os.path.join(userdata_dir, str(unsigned_id))
steam_shortcuts_path = os.path.join(user_dir, "config", "shortcuts.vdf")
backup_path = f"{steam_shortcuts_path}.backup"
if not os.path.exists(steam_shortcuts_path):
logger.error("Steam shortcuts file not found")
callback((False, "Steam shortcuts file not found"))
return
try:
shutil.copy2(steam_shortcuts_path, backup_path)
logger.info("Created backup of shortcuts.vdf at %s", backup_path)
except Exception as e:
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
callback((False, f"Failed to create backup of shortcuts.vdf: {e}"))
return
try:
with open(steam_shortcuts_path, 'rb') as f:
shortcuts_data = vdf.binary_load(f)
except Exception as e:
logger.error(f"Failed to load shortcuts.vdf: {e}")
callback((False, f"Failed to load shortcuts.vdf: {e}"))
return
shortcuts = shortcuts_data.get("shortcuts", {})
modified = False
new_shortcuts = {}
index = 0
for _key, entry in shortcuts.items():
if entry.get("AppName") == game_name and entry.get("Exe") == quoted_script_path:
modified = True
logger.info("Removing EGS game '%s' from Steam shortcuts", game_name)
continue
new_shortcuts[str(index)] = entry
index += 1
if not modified:
logger.error("Game '%s' not found in Steam shortcuts", game_name)
callback((False, f"Game '{game_name}' not found in Steam shortcuts"))
return
try:
with open(steam_shortcuts_path, 'wb') as f:
vdf.binary_dump({"shortcuts": new_shortcuts}, f)
logger.info("Updated shortcuts.vdf, removed '%s'", game_name)
except Exception as e:
logger.error(f"Failed to update shortcuts.vdf: {e}")
if os.path.exists(backup_path):
try:
shutil.copy2(backup_path, steam_shortcuts_path)
logger.info("Restored shortcuts.vdf from backup")
except Exception as restore_err:
logger.error(f"Failed to restore shortcuts.vdf: {restore_err}")
callback((False, f"Failed to update shortcuts.vdf: {e}"))
return
if os.path.exists(script_path):
try:
os.remove(script_path)
logger.info("Removed EGS script: %s", script_path)
except OSError as e:
logger.warning(f"Failed to remove EGS script '{script_path}': {str(e)}")
callback((True, f"Game '{game_name}' was removed from Steam. Please restart Steam for changes to take effect."))
def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callback: Callable[[tuple[bool, str]], None]) -> None:
"""
Asynchronously adds an EGS game to Steam via shortcuts.vdf with PortProton tag.

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m5.8957 13.164-4.8957-4.8957 1.2239-1.2239 3.6718 3.6718 7.8804-7.8804 1.2239 1.2239z" fill="#fff" stroke-width=".014444"/></svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style id="current-color-scheme" type="text/css">.ColorScheme-Text { color: #fcfcfc; } </style><style type="text/css">
.ColorScheme-Text { color:#4d4d4d; }
</style></defs><path d="m7.7202 1c-3.1678 0-4.7522-1.88e-4 -5.7363 0.98394-0.88825 0.88825-0.97414 2.2778-0.98256 4.8669 0.21843-0.66601 0.73947-0.95582 1.4337-0.95582h2.4746c3.4015-0.41346 2.8119-2.0399 4.9766-2.0399h3.6416c0.60291 0 1.1406 0.22096 1.407 0.75012-0.09519-1.227-0.33057-2.0331-0.9188-2.6213-0.98413-0.98412-2.5685-0.98394-5.7363-0.98394zm5.8056 4.0496-3.6409 6.86e-4c-2.1647 0-1.5751 1.5271-4.9766 1.9151h-2.4746c-0.69375 0-1.2145 0.27113-1.433 0.8948-5e-5 0.10542-6.86e-4 0.19915-6.86e-4 0.30855v0.52454c0 2.9728-1.88e-4 4.4597 0.98394 5.3832 0.98413 0.92352 2.5685 0.9236 5.7363 0.9236h0.55951c3.1678 0 4.7522-7.4e-5 5.7363-0.9236 0.98412-0.92354 0.98394-2.4104 0.98394-5.3832v-0.52454c0-0.92272-0.0035-1.6886-0.03291-2.3525-0.25332-0.54334-0.81238-0.76658-1.4413-0.76658z" fill="#fff" stroke-width=".13562"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><style id="current-color-scheme" type="text/css">.ColorScheme-Text { color: #fcfcfc; } </style><style type="text/css">
.ColorScheme-Text { color:#4d4d4d; }
</style></defs><path d="m3.1 10.8a1.4 1.4 0 0 0-1.4 1.4v1.4a1.4 1.4 0 0 0 1.4 1.4h9.8a1.4 1.4 0 0 0 1.4-1.4v-1.4a1.4 1.4 0 0 0-1.4-1.4zm2.1 2.625h-1.4a0.525 0.525 0 0 1 0-1.05h1.4a0.525 0.525 0 0 1 0 1.05zm9.1-3.6354a2.7673 2.7673 0 0 0-1.4-0.38955h-9.8a2.7673 2.7673 0 0 0-1.4 0.38955v-7.3896a1.4 1.4 0 0 1 1.4-1.4h9.8a1.4 1.4 0 0 1 1.4 1.4z" fill="#fff" stroke-width=".7"/></svg>

After

Width:  |  Height:  |  Size: 667 B

View File

@ -856,6 +856,72 @@ SETTINGS_CHECKBOX_STYLE = f"""
}}
"""
FILE_EXPLORER_STYLE = f"""
QListView {{
font-size: {font_size_a};
font-family: {font_family};
background-color: {color_c};
color: {color_f};
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}}
QListView::item {{
padding: 8px;
margin: 0px 5px;
border-bottom: {border_b} rgba(255, 255, 255, 0.1);
}}
QListView::item:selected {{
background-color: {color_a};
color: {color_f};
border-radius: {border_radius_a};
}}
QListView::item:hover {{
background-color: {color_a};
color: {color_f};
border-radius: {border_radius_a};
}}
QListView::item:focus {{
background-color: {color_a};
color: {color_f};
border-radius: {border_radius_a};
}}
QScrollBar:vertical {{
width: 10px;
border: {border_a};
border-radius: 5px;
background: {color_c};
}}
QScrollBar::handle:vertical {{
background: #bebebe;
border: {border_a};
border-radius: 5px;
}}
QScrollBar::add-line:vertical {{
border: {border_a};
background: {color_c};
border-bottom-right-radius: 5px;
}}
QScrollBar::sub-line:vertical {{
border: {border_a};
background: {color_c};
border-top-right-radius: 5px;
}}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {{
border: {border_a};
width: 3px;
height: 3px;
background: none;
}}
"""
FILE_EXPLORER_PATH_LABEL_STYLE = f"""
QLabel {{
color: {color_a};
font-size: {font_size_a};
font-family: {font_family};
}}
"""
# ФУНКЦИЯ ДЛЯ ДИНАМИЧЕСКОГО ГРАДИЕНТА (ДЕТАЛИ ИГР)
# Функции из этой темы срабатывает всегда вне зависимости от выбранной темы, функции из других тем работают только в этих темах
def detail_page_style(stops):
@ -866,57 +932,3 @@ def detail_page_style(stops):
border-radius: {border_radius_b};
}}
"""
class FileExplorerStyles:
WINDOW_STYLE = """
QDialog {
background-color: #2d2d2d;
color: #ffffff;
font-family: "Arial";
font-size: 14px;
}
"""
PATH_LABEL_STYLE = """
QLabel {
color: #3daee9;
font-size: 16px;
padding: 5px;
}
"""
LIST_STYLE = """
QListWidget {
font-size: 16px;
background-color: #353535;
color: #eee;
border: 1px solid #444;
border-radius: 4px;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #444;
}
QListWidget::item:selected {
background-color: #3daee9;
color: white;
border-radius: 2px;
}
"""
BUTTON_STYLE = """
QPushButton {
background-color: #3daee9;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}
QPushButton:hover {
background-color: #2c9fd8;
}
QPushButton:pressed {
background-color: #1a8fc7;
}
"""