10 Commits
main ... main

Author SHA1 Message Date
dec24429f5 chore: separate start.sh to new function
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-28 15:34:01 +05:00
4a758f3b3c chore: use flatpak run for flatpak not start.sh
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-27 23:13:48 +05:00
0853dd1579 chore: use CLI for clear pfx
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-27 22:36:14 +05:00
bbb87c0455 feat(settings): added icon to button thanks to @Dervart
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-27 12:17:00 +05:00
b32a71a125 feat(settings): block settings
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-27 12:16:54 +05:00
Renovate Bot
bddf9f850a chore(deps): update pre-commit hook astral-sh/uv-pre-commit to v0.9.5 2025-10-26 00:01:29 +00:00
Renovate Bot
a9c3cfa167 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.2 2025-10-26 00:01:19 +00:00
7675bc4cdc feat: added initial exe settings
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-26 00:12:00 +05:00
ffa203f019 feat: restore instance from tray
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-22 15:46:57 +05:00
3eed25ecee feat: update grid on update_favorite_icon
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-21 20:41:21 +05:00
10 changed files with 533 additions and 194 deletions

View File

@@ -11,12 +11,12 @@ repos:
- id: check-yaml - id: check-yaml
- repo: https://github.com/astral-sh/uv-pre-commit - repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.8.22 rev: 0.9.5
hooks: hooks:
- id: uv-lock - id: uv-lock
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.1 rev: v0.14.2
hooks: hooks:
- id: ruff-check - id: ruff-check

View File

@@ -1,11 +1,17 @@
import sys import sys
import os import os
import subprocess import subprocess
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo, QTimer, Qt
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtNetwork import QLocalServer, QLocalSocket
from portprotonqt.main_window import MainWindow from portprotonqt.main_window import MainWindow
from portprotonqt.config_utils import save_fullscreen_config, get_portproton_location from portprotonqt.config_utils import (
save_fullscreen_config,
read_fullscreen_config,
get_portproton_start_command
)
from portprotonqt.logger import get_logger, setup_logger from portprotonqt.logger import get_logger, setup_logger
from portprotonqt.cli import parse_args from portprotonqt.cli import parse_args
@@ -16,25 +22,24 @@ __app_version__ = "0.1.8"
def get_version(): def get_version():
try: try:
commit = subprocess.check_output( commit = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD'], ["git", "rev-parse", "--short", "HEAD"],
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL,
).decode('utf-8').strip() ).decode("utf-8").strip()
return f"{__app_version__} ({commit})" return f"{__app_version__} ({commit})"
except (subprocess.CalledProcessError, FileNotFoundError, OSError): except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return __app_version__ return __app_version__
def main(): def main():
os.environ['PW_CLI'] = '1' os.environ["PW_CLI"] = "1"
os.environ['PROCESS_LOG'] = '1' os.environ["PROCESS_LOG"] = "1"
os.environ['START_FROM_STEAM'] = '1' os.environ["START_FROM_STEAM"] = "1"
portproton_path = get_portproton_location() start_sh = get_portproton_start_command()
if portproton_path is None: if start_sh is None:
return return
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh') subprocess.run(start_sh + ["cli", "--initial"])
subprocess.run([script_path, 'cli', '--initial'])
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setWindowIcon(QIcon.fromTheme(__app_id__)) app.setWindowIcon(QIcon.fromTheme(__app_id__))
@@ -43,41 +48,116 @@ def main():
app.setApplicationVersion(__app_version__) app.setApplicationVersion(__app_version__)
args = parse_args() args = parse_args()
# Setup logger with specified debug level
setup_logger(args.debug_level) setup_logger(args.debug_level)
# Reinitialize logger after setup to ensure it uses the new configuration
logger = get_logger(__name__) logger = get_logger(__name__)
# --- Single-instance logic ---
server_name = __app_id__
socket = QLocalSocket()
socket.connectToServer(server_name)
if socket.waitForConnected(200):
# Второй экземпляр — передаём команду первому
fullscreen = args.fullscreen or read_fullscreen_config()
msg = b"show:fullscreen" if fullscreen else b"show"
socket.write(msg)
socket.flush()
socket.waitForBytesWritten(500)
socket.disconnectFromServer()
logger.info("Restored existing instance from tray")
return
# Если старый сокет остался — удалить
QLocalServer.removeServer(server_name)
local_server = QLocalServer()
if not local_server.listen(server_name):
logger.warning(f"Failed to start local server: {local_server.errorString()}")
return
# --- Qt translations ---
system_locale = QLocale.system() system_locale = QLocale.system()
qt_translator = QTranslator() qt_translator = QTranslator()
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath) translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
if qt_translator.load(system_locale, "qtbase", "_", translations_path): if qt_translator.load(system_locale, "qtbase", "_", translations_path):
app.installTranslator(qt_translator) app.installTranslator(qt_translator)
else: else:
logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language") logger.warning(
f"Qt translations for {system_locale.name()} not found in {translations_path}, using English"
)
# --- Main Window ---
version = get_version() version = get_version()
window = MainWindow(app_name=__app_name__, version=version) window = MainWindow(app_name=__app_name__, version=version)
if args.fullscreen: # --- Handle incoming connections ---
logger.info("Launching in fullscreen mode due to --fullscreen flag") def handle_new_connection():
conn = local_server.nextPendingConnection()
if not conn:
return
if conn.waitForReadyRead(1000):
data = conn.readAll().data()
msg = bytes(data).decode("utf-8", errors="ignore")
logger.info(f"IPC message received: {msg}")
def restore_window():
try:
if msg.startswith("show"):
if hasattr(window, "restore_from_tray"):
window.restore_from_tray() # type: ignore[attr-defined]
else:
window.showNormal()
window.raise_()
window.activateWindow()
window.setWindowState(
window.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive
)
if ":fullscreen" in msg:
logger.info("Switching to fullscreen via IPC")
save_fullscreen_config(True) save_fullscreen_config(True)
window.showFullScreen() window.showFullScreen()
else:
logger.info("Switching to normal window via IPC")
save_fullscreen_config(False)
window.showNormal()
except Exception as e:
logger.warning(f"Failed to restore window: {e}")
# Выполняем в основном потоке
QTimer.singleShot(0, restore_window)
conn.disconnectFromServer()
local_server.newConnection.connect(handle_new_connection)
# --- Initial fullscreen state ---
launch_fullscreen = args.fullscreen or read_fullscreen_config()
if launch_fullscreen:
logger.info(
f"Launching in fullscreen mode ({'--fullscreen' if args.fullscreen else 'config'})"
)
save_fullscreen_config(True)
window.showFullScreen()
else:
logger.info("Launching in normal mode")
save_fullscreen_config(False)
window.showNormal()
# --- Cleanup ---
def cleanup_on_exit(): def cleanup_on_exit():
nonlocal window try:
app.aboutToQuit.disconnect() local_server.close()
QLocalServer.removeServer(server_name)
if window: if window:
window.close() window.close()
app.quit() except Exception as e:
logger.warning(f"Cleanup error: {e}")
app.aboutToQuit.connect(cleanup_on_exit) app.aboutToQuit.connect(cleanup_on_exit)
window.show()
sys.exit(app.exec()) sys.exit(app.exec())
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -1,11 +1,13 @@
import os import os
import configparser import configparser
import shutil import shutil
import subprocess
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
_portproton_location = None _portproton_location = None
_portproton_start_sh = None
# Paths to configuration files # Paths to configuration files
CONFIG_FILE = os.path.join( CONFIG_FILE = os.path.join(
@@ -101,14 +103,14 @@ def read_file_content(file_path):
return f.read().strip() return f.read().strip()
def get_portproton_location(): def get_portproton_location():
"""Returns the path to the PortProton directory. """Возвращает путь к PortProton каталогу (строку) или None."""
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
If the path is invalid, uses the default directory.
"""
global _portproton_location global _portproton_location
if _portproton_location is not None: if _portproton_location is not None:
return _portproton_location return _portproton_location
location = None
if os.path.isfile(PORTPROTON_CONFIG_FILE): if os.path.isfile(PORTPROTON_CONFIG_FILE):
try: try:
location = read_file_content(PORTPROTON_CONFIG_FILE).strip() location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
@@ -116,19 +118,46 @@ def get_portproton_location():
_portproton_location = location _portproton_location = location
logger.info(f"PortProton path from configuration: {location}") logger.info(f"PortProton path from configuration: {location}")
return _portproton_location return _portproton_location
logger.warning(f"Invalid PortProton path in configuration: {location}, using default path") logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
except (OSError, PermissionError) as e: except (OSError, PermissionError) as e:
logger.warning(f"Failed to read PortProton configuration file: {e}, using default path") logger.warning(f"Failed to read PortProton configuration file: {e}")
default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton") default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
if os.path.isdir(default_dir): if os.path.isdir(default_flatpak_dir):
_portproton_location = default_dir _portproton_location = default_flatpak_dir
logger.info(f"Using flatpak PortProton directory: {default_dir}") logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}")
return _portproton_location return _portproton_location
logger.warning("PortProton configuration and flatpak directory not found") logger.warning("PortProton configuration and Flatpak directory not found")
return None return None
def get_portproton_start_command():
"""Возвращает список команд для запуска PortProton (start.sh или flatpak run)."""
portproton_path = get_portproton_location()
if not portproton_path:
return None
try:
result = subprocess.run(
["flatpak", "list"],
capture_output=True,
text=True,
check=False
)
if "ru.linux_gaming.PortProton" in result.stdout:
logger.info("Detected Flatpak installation")
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
except Exception:
pass
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
if os.path.exists(start_sh_path):
return [start_sh_path]
logger.warning("Neither flatpak nor start.sh found for PortProton")
return None
def parse_desktop_entry(file_path): def parse_desktop_entry(file_path):
"""Reads and parses a .desktop file using configparser. """Reads and parses a .desktop file using configparser.
Returns None if the [Desktop Entry] section is missing. Returns None if the [Desktop Entry] section is missing.

View File

@@ -11,7 +11,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, read_favorite_folders, save_favorite_folders from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders, get_portproton_start_command
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
@@ -406,16 +406,7 @@ class ContextMenuManager:
) )
return return
# Construct EGS launch command # Construct EGS launch command
wrapper = "flatpak run ru.linux_gaming.PortProton" wrapper = get_portproton_start_command()
start_sh_path = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
if self.portproton_location and ".var" not in self.portproton_location:
wrapper = start_sh_path
if not os.path.exists(start_sh_path):
self.signals.show_warning_dialog.emit(
_("Error"),
_("start.sh not found at {path}").format(path=start_sh_path)
)
return
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"' exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
else: else:
exec_line = self._get_exec_line(game_card.name, game_card.exec_line) exec_line = self._get_exec_line(game_card.name, game_card.exec_line)

View File

@@ -2,7 +2,7 @@ import os
import tempfile import tempfile
import re import re
from typing import cast, TYPE_CHECKING from typing import cast, TYPE_CHECKING
from PySide6.QtGui import QPixmap, QIcon, QTextCursor from PySide6.QtGui import QPixmap, QIcon, QTextCursor, QColor
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller, QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget
@@ -1674,3 +1674,308 @@ class WinetricksDialog(QDialog):
if self.input_manager: if self.input_manager:
self.input_manager.disable_winetricks_mode() self.input_manager.disable_winetricks_mode()
super().reject() super().reject()
class ExeSettingsDialog(QDialog):
def __init__(self, parent=None, theme=None, exe_path=None):
super().__init__(parent)
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
self.exe_path = exe_path
if not self.exe_path:
logger.error("Exe path not provided")
return
self.portproton_path = get_portproton_location()
if self.portproton_path is None:
logger.error("PortProton location not found")
return
base_path = os.path.join(self.portproton_path, "data")
self.start_sh = os.path.join(base_path, "scripts", "start.sh")
self.ppdb_path = self.exe_path + ".ppdb" if not self.exe_path.endswith('.ppdb') else self.exe_path
self.current_settings = {}
self.value_widgets = {}
self.original_values = {}
self.available_keys = set()
self.blocked_keys = set()
self.branch_name = _("Unknown")
self.setWindowTitle(_("Exe Settings"))
self.setModal(True)
self.resize(900, 600)
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
self.init_toggle_settings()
self.setup_ui()
# Find input_manager and main_window
self.input_manager = None
self.main_window = None
parent = self.parent()
while parent:
if hasattr(parent, 'input_manager'):
self.input_manager = cast("MainWindow", parent).input_manager
self.main_window = parent
parent = parent.parent()
self.current_theme_name = read_theme_from_config()
# Create hints widget using common function
self.hints_widget, self.hints_labels = create_dialog_hints_widget(
self.theme, self.main_window, self.input_manager, context='winetricks'
)
self.main_layout.addWidget(self.hints_widget)
# Connect signals
if self.input_manager:
self.input_manager.button_event.connect(
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
)
self.input_manager.dpad_moved.connect(
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
)
update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
# Load current settings (includes list-db)
self.load_current_settings()
def init_toggle_settings(self):
"""Initialize predefined toggle settings with descriptions."""
self.toggle_settings = {
'PW_MANGOHUD': _("Using FPS and system load monitoring (Turns on and off by the key combination - right Shift + F12)"),
'PW_MANGOHUD_USER_CONF': _("Forced use of MANGOHUD system settings (GOverlay, etc.)"),
'PW_VKBASALT': _("Enable vkBasalt by default to improve graphics in games running on Vulkan. (The HOME hotkey disables vkbasalt)"),
'PW_VKBASALT_USER_CONF': _("Forced use of VKBASALT system settings (GOverlay, etc.)"),
'PW_DGVOODOO2': _("Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) on all 3D API."),
'PW_GAMESCOPE': _("Super + F : Toggle fullscreen\nSuper + N : Toggle nearest neighbour filtering\nSuper + U : Toggle FSR upscaling\nSuper + Y : Toggle NIS upscaling\nSuper + I : Increase FSR sharpness by 1\nSuper + O : Decrease FSR sharpness by 1\nSuper + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\nSuper + G : Toggle keyboard grab\nSuper + C : Update clipboard"),
'PW_USE_ESYNC': _("Enable in-process synchronization primitives based on eventfd."),
'PW_USE_FSYNC': _("Enable futex-based in-process synchronization primitives."),
'PW_USE_NTSYNC': _("Enable in-process synchronization via the Linux ntsync driver."),
'PW_USE_RAY_TRACING': _("Enable vkd3d support - Ray Tracing"),
'PW_USE_NVAPI_AND_DLSS': _("Enable DLSS on supported NVIDIA graphics cards"),
'PW_USE_OPTISCALER': _("Enable OptiScaler (replacement upscaler / frame generator)"),
'PW_USE_LS_FRAME_GEN': _("Enable Lossless Scaling frame generation (experimental)"),
'PW_WINE_FULLSCREEN_FSR': _("FSR upscaling in fullscreen with ProtonGE below native resolution"),
'PW_HIDE_NVIDIA_GPU': _("Disguise all NVIDIA GPU features"),
'PW_VIRTUAL_DESKTOP': _("Run the application in WINE virtual desktop"),
'PW_USE_TERMINAL': _("Run the application in a terminal"),
'PW_GUI_DISABLED_CS': _("Disable startup mode and WINE version selector window"),
'PW_USE_GAMEMODE': _("Use system GameMode for performance optimization"),
'PW_USE_D3D_EXTRAS': _("Enable forced use of third-party DirectX libraries"),
'PW_FIX_VIDEO_IN_GAME': _("Fix pink-tinted video playback in some games"),
'PW_REDUCE_PULSE_LATENCY': _("Reduce PulseAudio latency to fix intermittent sound"),
'PW_USE_US_LAYOUT': _("Force US keyboard layout"),
'PW_USE_GSTREAMER': _("Use GStreamer for in-game clips (WMF support)"),
'PW_USE_SHADER_CACHE': _("Use WINE shader caching"),
'PW_USE_WINE_DXGI': _("Force use of built-in DXGI library"),
'PW_USE_EAC_AND_BE': _("Enable Easy Anti-Cheat and BattlEye runtimes"),
'PW_USE_SYSTEM_VK_LAYERS': _("Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"),
'PW_USE_OBS_VKCAPTURE': _("Enable OBS Studio capture via obs-vkcapture"),
'PW_DISABLE_COMPOSITING': _("Disable desktop compositing for performance"),
'PW_USE_RUNTIME': _("Use container launch mode (recommended default)"),
'PW_DINPUT_PROTOCOL': _("Force DirectInput protocol instead of XInput"),
'PW_USE_NATIVE_WAYLAND': _("Enable experimental native Wayland support"),
'PW_USE_DXVK_HDR': _("Enable HDR settings under native Wayland"),
'PW_USE_GALLIUM_ZINK': _("Use Gallium Zink (OpenGL via Vulkan)"),
'PW_USE_GALLIUM_NINE': _("Use Gallium Nine (native DirectX 9 for Mesa)"),
'PW_USE_WINED3D_VULKAN': _("Use WineD3D Vulkan backend (Damavand)"),
'PW_USE_SUPPLIED_DXVK_VKD3D': _("Use bundled dxvk/vkd3d from Wine/Proton"),
'PW_USE_SAREK_ASYNC': _("Use async dxvk-sarek (experimental)")
}
def setup_ui(self):
"""Set up the user interface."""
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(10, 10, 10, 10)
self.main_layout.setSpacing(10)
# Метка с текущей веткой (STABLE / DEVEL)
self.branch_label = QLabel(_("Detected branch: Unknown"))
self.branch_label.setStyleSheet("font-weight: bold;")
self.main_layout.addWidget(self.branch_label)
# Таблица настроек
self.settings_table = QTableWidget()
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.settings_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.settings_table.setColumnCount(3)
self.settings_table.setHorizontalHeaderLabels([_("Setting"), _("Value"), _("Description")])
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.settings_table.horizontalHeader().resizeSection(1, 100)
self.settings_table.setWordWrap(True)
self.settings_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.settings_table.setTextElideMode(Qt.TextElideMode.ElideNone)
self.settings_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
self.main_layout.addWidget(self.settings_table)
# Кнопки
button_layout = QHBoxLayout()
self.apply_button = AutoSizeButton(_("Apply"), icon=theme_manager.get_icon("apply"))
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
self.apply_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button_layout.addWidget(self.apply_button)
button_layout.addWidget(self.cancel_button)
self.main_layout.addLayout(button_layout)
self.apply_button.clicked.connect(self.apply_changes)
self.cancel_button.clicked.connect(self.reject)
def load_current_settings(self):
"""Load available toggles first, then current settings."""
process = QProcess(self)
process.finished.connect(self.on_list_db_finished)
process.start(self.start_sh, ["cli", "--list-db"])
def on_list_db_finished(self, exit_code, exit_status):
"""Handle --list-db output and extract available keys."""
process = cast(QProcess, self.sender())
self.available_keys = set()
self.blocked_keys = set()
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
for line in output.splitlines():
if "Branch in used:" in line:
self.branch_name = line.split(":", 1)[1].strip()
self.branch_label.setText(_("Detected branch: ") + self.branch_name)
continue
stripped_line = line.strip()
if stripped_line.startswith("PW_"):
parts = stripped_line.split(maxsplit=1)
key = parts[0]
self.available_keys.add(key)
if len(parts) > 1 and parts[1] == "blocked":
self.blocked_keys.add(key)
# Показываем только пересечение
self.available_keys &= set(self.toggle_settings.keys())
logger.debug(f"Filtered available keys (intersection): {self.available_keys}")
else:
logger.warning("Failed to get --list-db output; showing all toggles")
self.available_keys = set(self.toggle_settings.keys())
# Загружаем текущие настройки
process = QProcess(self)
process.finished.connect(self.on_show_ppdb_finished)
process.start(self.start_sh, ["cli", "--show-ppdb", self.ppdb_path])
def on_show_ppdb_finished(self, exit_code, exit_status):
"""Handle --show-ppdb output."""
process = cast(QProcess, self.sender())
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
logger.warning("Failed to load settings, using defaults")
else:
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore').strip()
self.current_settings = {}
for line in output.split('\n'):
if '=' in line and line.strip().startswith('PW_'):
key, val = line.split('=', 1)
self.current_settings[key.strip()] = val.strip()
logger.debug(f"Loaded current settings: {self.current_settings}")
# Force blocked settings to '0'
for key in self.blocked_keys:
self.current_settings[key] = '0'
self.populate_table()
def populate_table(self):
"""Populate the table with settings that are available in both lists."""
self.settings_table.setRowCount(0)
self.value_widgets.clear()
self.original_values.clear()
self.settings_table.verticalHeader().setVisible(False)
visible_keys = sorted(self.available_keys) if self.available_keys else sorted(self.toggle_settings.keys())
for toggle in visible_keys:
description = self.toggle_settings.get(toggle)
if not description:
continue
row = self.settings_table.rowCount()
self.settings_table.insertRow(row)
name_item = QTableWidgetItem(toggle)
name_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
current_val = self.current_settings.get(toggle, '0')
is_blocked = toggle in self.blocked_keys
checkbox = QTableWidgetItem()
checkbox.setFlags(checkbox.flags() | Qt.ItemFlag.ItemIsUserCheckable)
check_state = Qt.CheckState.Checked if current_val == '1' and not is_blocked else Qt.CheckState.Unchecked
checkbox.setCheckState(check_state)
checkbox.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
if is_blocked:
checkbox.setFlags(checkbox.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
checkbox.setBackground(QColor(240, 240, 240))
name_item.setForeground(QColor(128, 128, 128))
self.settings_table.setItem(row, 1, checkbox)
desc_item = QTableWidgetItem(description)
desc_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
desc_item.setToolTip(description)
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
if is_blocked:
desc_item.setForeground(QColor(128, 128, 128))
self.settings_table.setItem(row, 2, desc_item)
self.settings_table.setItem(row, 0, name_item)
self.value_widgets[(row, 1)] = checkbox
self.original_values[toggle] = current_val
self.settings_table.resizeRowsToContents()
if self.settings_table.rowCount() > 0:
self.settings_table.setCurrentCell(0, 0)
self.settings_table.setFocus(Qt.FocusReason.OtherFocusReason)
def apply_changes(self):
"""Apply changes by collecting diffs and running --edit-db."""
changes = []
for key, orig_val in self.original_values.items():
if key in self.blocked_keys:
continue # Skip blocked keys
row = -1
for r in range(self.settings_table.rowCount()):
item0 = self.settings_table.item(r, 0)
if item0 and item0.text() == key:
row = r
break
if row == -1:
continue
item = self.settings_table.item(row, 1)
if not item:
continue
new_val = '1' if item.checkState() == Qt.CheckState.Checked else '0'
if new_val != orig_val:
changes.append(f"{key}={new_val}")
if not changes:
QMessageBox.information(self, _("Info"), _("No changes to apply."))
return
process = QProcess(self)
process.finished.connect(self.on_edit_db_finished)
args = ["cli", "--edit-db", self.exe_path] + changes
process.start(self.start_sh, args)
self.apply_button.setEnabled(False)
def on_edit_db_finished(self, exit_code, exit_status):
"""Handle --edit-db output."""
process = cast(QProcess, self.sender())
self.apply_button.setEnabled(True)
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
QMessageBox.warning(self, _("Error"), _("Failed to apply changes. Check logs."))
logger.error(f"Failed to apply changes: {error_output}")
else:
self.load_current_settings()
QMessageBox.information(self, _("Success"), _("Settings updated successfully."))
def closeEvent(self, event):
super().closeEvent(event)
def reject(self):
super().reject()

View File

@@ -13,7 +13,7 @@ from portprotonqt.localization import get_egs_language, _
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.image_utils import load_pixmap_async from portprotonqt.image_utils import load_pixmap_async
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
from portprotonqt.config_utils import get_portproton_location from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
from portprotonqt.steam_api import ( from portprotonqt.steam_api import (
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async, get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
@@ -254,14 +254,7 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba
return return
# Determine wrapper # Determine wrapper
wrapper = "flatpak run ru.linux_gaming.PortProton" wrapper = get_portproton_start_command()
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
if portproton_dir is not None and ".var" not in portproton_dir:
wrapper = start_sh_path
if not os.path.exists(start_sh_path):
logger.error(f"start.sh not found at {start_sh_path}")
callback((False, f"start.sh not found at {start_sh_path}"))
return
# Create launch script # Create launch script
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts") steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")

View File

@@ -1,5 +1,5 @@
from PySide6.QtGui import QPainter, QColor, QDesktopServices from PySide6.QtGui import QPainter, QColor, QDesktopServices
from PySide6.QtCore import Signal, Property, Qt, QUrl from PySide6.QtCore import Signal, Property, Qt, QUrl, QTimer
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
from collections.abc import Callable from collections.abc import Callable
from portprotonqt.image_utils import load_pixmap_async, round_corners from portprotonqt.image_utils import load_pixmap_async, round_corners
@@ -404,6 +404,13 @@ class GameCard(QFrame):
self.favoriteLabel.setText("") self.favoriteLabel.setText("")
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE) self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
parent = self.parent()
while parent:
if hasattr(parent, 'game_library_manager'):
QTimer.singleShot(0, parent.game_library_manager.update_game_grid) # type: ignore[attr-defined]
break
parent = parent.parent()
def toggle_favorite(self): def toggle_favorite(self):
favorites = read_favorites() favorites = read_favorites()
if self.is_favorite: if self.is_favorite:

View File

@@ -8,7 +8,7 @@ import psutil
import re import re
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog
from portprotonqt.game_card import GameCard from portprotonqt.game_card import GameCard
from portprotonqt.animations import DetailPageAnimations from portprotonqt.animations import DetailPageAnimations
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
@@ -30,7 +30,7 @@ from portprotonqt.config_utils import (
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config, save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config, save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray, clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray,
read_auto_card_size, save_auto_card_size read_auto_card_size, save_auto_card_size, get_portproton_start_command
) )
from portprotonqt.localization import _, get_egs_language, read_metadata_translations from portprotonqt.localization import _, get_egs_language, read_metadata_translations
from portprotonqt.howlongtobeat_api import HowLongToBeat from portprotonqt.howlongtobeat_api import HowLongToBeat
@@ -74,6 +74,7 @@ class MainWindow(QMainWindow):
self.target_exe = None self.target_exe = None
self.current_running_button = None self.current_running_button = None
self.portproton_location = get_portproton_location() self.portproton_location = get_portproton_location()
self.start_sh = get_portproton_start_command()
self.game_library_manager = GameLibraryManager(self, self.theme, None) self.game_library_manager = GameLibraryManager(self, self.theme, None)
@@ -458,11 +459,11 @@ class MainWindow(QMainWindow):
self.current_install_script = script_name self.current_install_script = script_name
self.seen_progress = False self.seen_progress = False
self.current_percent = 0.0 self.current_percent = 0.0
start_sh = os.path.join(self.portproton_location or "", "data", "scripts", "start.sh") if self.portproton_location else "" start_sh = self.start_sh
if not os.path.exists(start_sh): if not start_sh:
self.installing = False self.installing = False
return return
cmd = [start_sh, "cli", "--autoinstall", script_name] cmd = start_sh + ["cli", "--autoinstall", script_name]
self.install_process = QProcess(self) self.install_process = QProcess(self)
self.install_process.finished.connect(self.on_install_finished) self.install_process.finished.connect(self.on_install_finished)
self.install_process.errorOccurred.connect(self.on_install_error) self.install_process.errorOccurred.connect(self.on_install_error)
@@ -1424,12 +1425,10 @@ class MainWindow(QMainWindow):
prefix = self.prefixCombo.currentText() prefix = self.prefixCombo.currentText()
if not wine or not prefix: if not wine or not prefix:
return return
if not self.portproton_location: if not self.portproton_location or not self.start_sh:
return return
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh") start_sh = self.start_sh
if not os.path.exists(start_sh): cmd = start_sh + ["cli", cli_arg, wine, prefix]
return
cmd = [start_sh, "cli", cli_arg, wine, prefix]
# Показываем прогресс-бар перед запуском # Показываем прогресс-бар перед запуском
self.wine_progress_bar.setVisible(True) self.wine_progress_bar.setVisible(True)
@@ -1508,12 +1507,13 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}") QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
def clear_prefix(self): def clear_prefix(self):
"""Очистка префикса (позже удалить).""" """Очищает префикс"""
selected_prefix = self.prefixCombo.currentText() selected_prefix = self.prefixCombo.currentText()
selected_wine = self.wineCombo.currentText() selected_wine = self.wineCombo.currentText()
if not selected_prefix or not selected_wine: if not selected_prefix or not selected_wine:
return return
if not self.portproton_location: if not self.portproton_location or not self.start_sh:
return return
reply = QMessageBox.question( reply = QMessageBox.question(
@@ -1526,98 +1526,35 @@ class MainWindow(QMainWindow):
if reply != QMessageBox.StandardButton.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
prefix_dir = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix) start_sh = self.start_sh
if not os.path.exists(prefix_dir):
self.wine_progress_bar.setVisible(True)
self.update_status_message.emit(_("Clearing prefix..."), 0)
self.clear_process = QProcess(self)
self.clear_process.finished.connect(lambda exitCode, exitStatus: self._on_clear_prefix_finished(exitCode))
self.clear_process.errorOccurred.connect(lambda error: self._on_clear_prefix_error(error))
cmd = start_sh + ["cli", "--clear_pfx", selected_wine, selected_prefix]
self.clear_process.start(cmd[0], cmd[1:])
if not self.clear_process.waitForStarted(5000):
self.wine_progress_bar.setVisible(False)
self.update_status_message.emit("", 0)
QMessageBox.warning(self, _("Error"), _("Failed to start prefix clear process."))
return return
success = True def _on_clear_prefix_finished(self, exitCode):
errors = [] self.wine_progress_bar.setVisible(False)
self.update_status_message.emit("", 0)
# Удаление файлов if exitCode == 0:
files_to_remove = [ QMessageBox.information(self, _("Success"), _("Prefix cleared successfully."))
os.path.join(prefix_dir, "*.dot*"),
os.path.join(prefix_dir, "*.prog*"),
os.path.join(prefix_dir, ".wine_ver"),
os.path.join(prefix_dir, "system.reg"),
os.path.join(prefix_dir, "user.reg"),
os.path.join(prefix_dir, "userdef.reg"),
os.path.join(prefix_dir, "winetricks.log"),
os.path.join(prefix_dir, ".update-timestamp"),
os.path.join(prefix_dir, "drive_c", ".windows-serial"),
]
import glob
for pattern in files_to_remove:
if "*" in pattern: # Глобальный паттерн
matches = glob.glob(pattern)
for file_path in matches:
try:
if os.path.exists(file_path):
os.remove(file_path)
except Exception as e:
success = False
errors.append(str(e))
else: # Конкретный файл
try:
if os.path.exists(pattern):
os.remove(pattern)
except Exception as e:
success = False
errors.append(str(e))
# Удаление директорий
dirs_to_remove = [
os.path.join(prefix_dir, "drive_c", "windows"),
os.path.join(prefix_dir, "drive_c", "ProgramData", "Setup"),
os.path.join(prefix_dir, "drive_c", "ProgramData", "Windows"),
os.path.join(prefix_dir, "drive_c", "ProgramData", "WindowsTask"),
os.path.join(prefix_dir, "drive_c", "ProgramData", "Package Cache"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Microsoft"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Temp"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Temporary Internet Files"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "Microsoft"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "wine_gecko"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Temp"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Microsoft"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Temp"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Temporary Internet Files"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "Microsoft"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "wine_gecko"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Temp"),
os.path.join(prefix_dir, "drive_c", "Program Files", "Internet Explorer"),
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows Media Player"),
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows NT"),
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Internet Explorer"),
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows Media Player"),
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows NT"),
]
import shutil
for dir_path in dirs_to_remove:
try:
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
except Exception as e:
success = False
errors.append(str(e))
tmp_path = os.path.join(self.portproton_location, "data", "tmp")
if os.path.exists(tmp_path):
import glob
bin_files = glob.glob(os.path.join(tmp_path, "*.bin"))
foz_files = glob.glob(os.path.join(tmp_path, "*.foz"))
for file_path in bin_files + foz_files:
try:
os.remove(file_path)
except Exception as e:
success = False
errors.append(str(e))
if success:
QMessageBox.information(self, _("Success"), _("Prefix '{}' cleared successfully.").format(selected_prefix))
else: else:
error_msg = _("Prefix '{}' cleared with errors:\n{}").format(selected_prefix, "\n".join(errors[:5])) QMessageBox.warning(self, _("Error"), _("Prefix clear failed with exit code {}.").format(exitCode))
QMessageBox.warning(self, _("Warning"), error_msg)
def _on_clear_prefix_error(self, error):
self.wine_progress_bar.setVisible(False)
self.update_status_message.emit("", 0)
QMessageBox.warning(self, _("Error"), _("Failed to run clear prefix command: {}").format(error))
def create_prefix_backup(self): def create_prefix_backup(self):
selected_prefix = self.prefixCombo.currentText() selected_prefix = self.prefixCombo.currentText()
@@ -1629,14 +1566,12 @@ class MainWindow(QMainWindow):
def _perform_backup(self, backup_dir, prefix_name): def _perform_backup(self, backup_dir, prefix_name):
os.makedirs(backup_dir, exist_ok=True) os.makedirs(backup_dir, exist_ok=True)
if not self.portproton_location: if not self.portproton_location or not self.start_sh:
return
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
if not os.path.exists(start_sh):
return return
start_sh = self.start_sh
self.backup_process = QProcess(self) self.backup_process = QProcess(self)
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode)) self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
cmd = [start_sh, "--backup-prefix", prefix_name, backup_dir] cmd = start_sh + ["--backup-prefix", prefix_name, backup_dir]
self.backup_process.start(cmd[0], cmd[1:]) self.backup_process.start(cmd[0], cmd[1:])
if not self.backup_process.waitForStarted(): if not self.backup_process.waitForStarted():
QMessageBox.warning(self, _("Error"), _("Failed to start backup process.")) QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
@@ -1649,14 +1584,12 @@ class MainWindow(QMainWindow):
def _perform_restore(self, file_path): def _perform_restore(self, file_path):
if not file_path or not os.path.exists(file_path): if not file_path or not os.path.exists(file_path):
return return
if not self.portproton_location: if not self.portproton_location or not self.start_sh:
return
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
if not os.path.exists(start_sh):
return return
start_sh = self.start_sh
self.restore_process = QProcess(self) self.restore_process = QProcess(self)
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode)) self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
cmd = [start_sh, "--restore-prefix", file_path] cmd = start_sh + ["--restore-prefix", file_path]
self.restore_process.start(cmd[0], cmd[1:]) self.restore_process.start(cmd[0], cmd[1:])
if not self.restore_process.waitForStarted(): if not self.restore_process.waitForStarted():
QMessageBox.warning(self, _("Error"), _("Failed to start restore process.")) QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
@@ -2326,6 +2259,14 @@ class MainWindow(QMainWindow):
def darkenColor(self, color, factor=200): def darkenColor(self, color, factor=200):
return color.darker(factor) return color.darker(factor)
def open_exe_settings(self, exe_path):
"""Open the ExeSettingsDialog for the given executable."""
if not os.path.exists(exe_path):
QMessageBox.warning(self, _("Error"), _("Executable not found: {0}").format(exe_path))
return
dialog = ExeSettingsDialog(self, self.theme, exe_path)
dialog.exec()
def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""): def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""):
detailPage = QWidget() detailPage = QWidget()
self._animations = {} self._animations = {}
@@ -2628,8 +2569,6 @@ class MainWindow(QMainWindow):
clear_layout(hltbLayout) clear_layout(hltbLayout)
has_data = False has_data = False
if main_story_time is not None: if main_story_time is not None:
@@ -2713,6 +2652,14 @@ class MainWindow(QMainWindow):
playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton)) playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton))
detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft) detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft)
# Settings button
settings_icon = self.theme_manager.get_icon("settings")
settings_button = AutoSizeButton(_("Settings"), icon=settings_icon)
settings_button.setFixedSize(120, 40)
settings_button.setStyleSheet(self.theme.PLAY_BUTTON_STYLE)
settings_button.clicked.connect(lambda: self.open_exe_settings(file_to_check))
detailsLayout.addWidget(settings_button, alignment=Qt.AlignmentFlag.AlignLeft)
contentFrameLayout.addWidget(detailsWidget) contentFrameLayout.addWidget(detailsWidget)
mainLayout.addStretch() mainLayout.addStretch()
@@ -2935,10 +2882,7 @@ class MainWindow(QMainWindow):
env_vars = os.environ.copy() env_vars = os.environ.copy()
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
wrapper = "flatpak run ru.linux_gaming.PortProton" wrapper = self.start_sh or ""
if self.portproton_location is not None and ".var" not in self.portproton_location:
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
wrapper = start_sh
cmd = [wrapper, game_exe] cmd = [wrapper, game_exe]
@@ -3032,13 +2976,6 @@ class MainWindow(QMainWindow):
exe_name = os.path.splitext(current_exe)[0] exe_name = os.path.splitext(current_exe)[0]
env_vars = os.environ.copy() env_vars = os.environ.copy()
if entry_exec_split[0] == "env" and len(entry_exec_split) > 1 and 'data/scripts/start.sh' in entry_exec_split[1]:
env_vars['START_FROM_STEAM'] = '1'
env_vars['PROCESS_LOG'] = '1'
elif entry_exec_split[0] == "flatpak":
env_vars['START_FROM_STEAM'] = '1'
env_vars['PROCESS_LOG'] = '1'
# Запускаем игру # Запускаем игру
try: try:
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid) process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)

View File

@@ -13,7 +13,7 @@ from portprotonqt.logger import get_logger
from portprotonqt.localization import get_steam_language from portprotonqt.localization import get_steam_language
from portprotonqt.downloader import Downloader from portprotonqt.downloader import Downloader
from portprotonqt.dialogs import generate_thumbnail from portprotonqt.dialogs import generate_thumbnail
from portprotonqt.config_utils import get_portproton_location from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
from collections.abc import Callable from collections.abc import Callable
import re import re
import shutil import shutil
@@ -1004,7 +1004,8 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
return (False, f"Executable file not found: {exe_path}") return (False, f"Executable file not found: {exe_path}")
portproton_dir = get_portproton_location() portproton_dir = get_portproton_location()
if not portproton_dir: start_sh = get_portproton_start_command()
if not portproton_dir or not start_sh:
logger.error("PortProton directory not found") logger.error("PortProton directory not found")
return (False, "PortProton directory not found") return (False, "PortProton directory not found")
@@ -1013,17 +1014,12 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip()) safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh") script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
if not os.path.exists(start_sh_path):
logger.error(f"start.sh not found at {start_sh_path}")
return (False, f"start.sh not found at {start_sh_path}")
if not os.path.exists(script_path): if not os.path.exists(script_path):
script_content = f"""#!/usr/bin/env bash script_content = f"""#!/usr/bin/env bash
export LD_PRELOAD= export LD_PRELOAD=
export START_FROM_STEAM=1 export START_FROM_STEAM=1
"{start_sh_path}" "{exe_path}" "$@" "{start_sh}" "{exe_path}" "$@"
""" """
try: try:
with open(script_path, "w", encoding="utf-8") as f: with open(script_path, "w", encoding="utf-8") as f:

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="m8.0005 1c-0.38761 0-0.77522 0.0327-1.1588 0.0979-0.16351 0.0281-0.30273 0.13627-0.37209 0.28935l-0.39088 0.86264c-0.49378 0.16682-0.96454 0.39759-1.4007 0.68616 2.5e-4 0-0.90672-0.2272-0.90672-0.2272-0.161-0.0403-0.33098 3e-3 -0.45442 0.11569-0.57867 0.5285-1.0672 1.1514-1.4451 1.8432-0.0804 0.14721-0.0841 0.32549-0.01 0.47628l0.41938 0.84865c-0.17954 0.49666-0.29567 1.0147-0.346 1.5417l-0.73995 0.57946c-0.13121 0.10289-0.20407 0.26514-0.19431 0.4335 0.0453 0.78981 0.21961 1.5666 0.51558 2.2983 0.0631 0.15587 0.1978 0.27003 0.36005 0.30467l0.91397 0.19559c0.26993 0.45234 0.59572 0.86802 0.96931 1.2363l-0.0161 0.94973c-3e-3 0.16861 0.0766 0.32755 0.21183 0.42484 0.63551 0.45642 1.3414 0.80207 2.0884 1.0229 0.15926 0.0471 0.33077 0.0109 0.45872-0.0963l0.72016-0.60485c0.51582 0.0674 1.0384 0.0674 1.5544 0l0.72016 0.60485c0.12796 0.10722 0.29946 0.14343 0.45872 0.0963 0.74693-0.22083 1.4528-0.56648 2.0883-1.0229 0.13521-0.0973 0.21465-0.25623 0.21189-0.42484l-0.0161-0.94973c0.37359-0.36829 0.69939-0.78372 0.96932-1.2363l0.91396-0.19559c0.16226-0.0347 0.29695-0.1488 0.36005-0.30467 0.29597-0.73174 0.47026-1.5085 0.51558-2.2983 0.01-0.16836-0.0631-0.33061-0.1943-0.4335l-0.73996-0.57946c-0.0501-0.52671-0.16652-1.045-0.34606-1.5417l0.41944-0.84865c0.0746-0.15079 0.0709-0.32907-0.01-0.47628-0.37785-0.69176-0.86638-1.3147-1.445-1.8432-0.12345-0.11258-0.29343-0.15594-0.45443-0.11569l-0.90697 0.2272c-0.43594-0.28857-0.9067-0.51908-1.4005-0.68616l-0.39088-0.86264c-0.0694-0.15308-0.20858-0.26132-0.37209-0.28935-0.38361-0.0653-0.77121-0.0979-1.1588-0.0979zm0 4.1365a2.8152 2.8635 0 0 1 2.8152 2.8636 2.8152 2.8635 0 0 1-2.8152 2.8635 2.8152 2.8635 0 0 1-2.8152-2.8635 2.8152 2.8635 0 0 1 2.8152-2.8636z" fill="#fff" stroke-width=".25254"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB