forked from Boria138/PortProtonQt
Compare commits
2 Commits
main
...
0a4284191b
Author | SHA1 | Date | |
---|---|---|---|
0a4284191b | |||
d05f2fccd6 |
@@ -94,7 +94,7 @@ jobs:
|
||||
name: Build Arch Package
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
|
||||
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
|
@@ -138,7 +138,7 @@ jobs:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
||||
container:
|
||||
image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
|
||||
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
|
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
|
||||
container: ghcr.io/renovatebot/renovate:latest@sha256:dd5721b9a686a40d81687643e4b71b82a0ca31fb653fd727538af69104fd388d
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
|
@@ -7,19 +7,14 @@
|
||||
|
||||
### Added
|
||||
- Возможность скроллинга библиотеки мышью или пальцем
|
||||
- Импорт и экспорт бекапа префикса
|
||||
- Диалог для управление Winetricks
|
||||
- Кнопки для удаления префикса, wine или proton
|
||||
|
||||
### Changed
|
||||
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
|
||||
- В диалог выбора файлов в режиме directory_only (при выборе куда сохранить бекап префикса) добавлена кнопка ./ обозначающая нынешнюю папку
|
||||
|
||||
### Fixed
|
||||
- Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений
|
||||
- Исправлено зависание при добавлении или удалении игры в Wayland
|
||||
- Исправлено зависание при поиске игр
|
||||
- Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity)
|
||||
|
||||
### Contributors
|
||||
|
||||
|
@@ -21,9 +21,9 @@ Current translation status:
|
||||
|
||||
| Locale | Progress | Translated |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 232 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 232 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 232 of 232 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 193 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 193 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 of 193 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -21,9 +21,9 @@
|
||||
|
||||
| Локаль | Прогресс | Переведено |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 232 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 232 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 232 из 232 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 193 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 193 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 из 193 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -2,13 +2,11 @@ import os
|
||||
import tempfile
|
||||
import re
|
||||
from typing import cast, TYPE_CHECKING
|
||||
from PySide6.QtGui import QPixmap, QIcon, QTextCursor
|
||||
from PySide6.QtGui import QPixmap, QIcon
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
|
||||
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget
|
||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller
|
||||
)
|
||||
|
||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
|
||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot
|
||||
from icoextract import IconExtractor, IconExtractorError
|
||||
from PIL import Image
|
||||
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
|
||||
@@ -17,7 +15,6 @@ from portprotonqt.logger import get_logger
|
||||
from portprotonqt.theme_manager import ThemeManager
|
||||
from portprotonqt.custom_widgets import AutoSizeButton
|
||||
from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.preloader import Preloader
|
||||
import psutil
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -600,16 +597,6 @@ class FileExplorer(QDialog):
|
||||
self.thumbnail_cache.clear() # Clear cache when changing directories
|
||||
self.pending_thumbnails.clear() # Clear pending thumbnails
|
||||
try:
|
||||
if self.directory_only:
|
||||
item = QListWidgetItem("./")
|
||||
folder_icon = theme_manager.get_icon("folder")
|
||||
# Ensure the icon is a QIcon
|
||||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
||||
folder_icon = QIcon(folder_icon)
|
||||
elif not isinstance(folder_icon, QIcon):
|
||||
folder_icon = QIcon() # Fallback to empty icon
|
||||
item.setIcon(folder_icon)
|
||||
self.file_list.addItem(item)
|
||||
if self.current_path != "/":
|
||||
item = QListWidgetItem("../")
|
||||
folder_icon = theme_manager.get_icon("folder")
|
||||
@@ -980,464 +967,3 @@ Icon={icon_path}
|
||||
"""
|
||||
|
||||
return desktop_entry, desktop_path
|
||||
|
||||
class WinetricksDialog(QDialog):
|
||||
"""Dialog for managing Winetricks components in a prefix."""
|
||||
|
||||
def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None):
|
||||
super().__init__(parent)
|
||||
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||
self.prefix_path: str | None = prefix_path
|
||||
self.wine_use: str | None = wine_use
|
||||
self.portproton_path = get_portproton_location()
|
||||
if self.portproton_path is None:
|
||||
logger.error("PortProton location not found")
|
||||
return
|
||||
self.tmp_path = os.path.join(self.portproton_path, "data", "tmp")
|
||||
os.makedirs(self.tmp_path, exist_ok=True)
|
||||
self.winetricks_path = os.path.join(self.tmp_path, "winetricks")
|
||||
if self.prefix_path is None:
|
||||
logger.error("Prefix path not provided")
|
||||
return
|
||||
self.log_path = os.path.join(self.prefix_path, "winetricks.log")
|
||||
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
|
||||
if not os.path.exists(self.log_path):
|
||||
open(self.log_path, 'a').close()
|
||||
|
||||
self.downloader = Downloader(max_workers=4)
|
||||
self.apply_process: QProcess | None = None
|
||||
|
||||
self.setWindowTitle(_("Prefix Manager"))
|
||||
self.setModal(True)
|
||||
self.resize(700, 700)
|
||||
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
|
||||
|
||||
self.update_winetricks()
|
||||
self.setup_ui()
|
||||
self.load_lists()
|
||||
|
||||
def update_winetricks(self):
|
||||
"""Update the winetricks script."""
|
||||
if not self.downloader.has_internet():
|
||||
logger.warning("No internet connection, skipping winetricks update")
|
||||
return
|
||||
|
||||
url = "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks"
|
||||
temp_path = os.path.join(self.tmp_path, "winetricks_temp")
|
||||
|
||||
try:
|
||||
self.downloader.download(url, temp_path)
|
||||
with open(temp_path) as f:
|
||||
ext_content = f.read()
|
||||
ext_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', ext_content)
|
||||
ext_ver = ext_ver_match.group(1) if ext_ver_match else None
|
||||
logger.info(f"External winetricks version: {ext_ver}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get external version: {e}")
|
||||
ext_ver = None
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
return
|
||||
|
||||
int_ver = None
|
||||
if os.path.exists(self.winetricks_path):
|
||||
try:
|
||||
with open(self.winetricks_path) as f:
|
||||
int_content = f.read()
|
||||
int_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', int_content)
|
||||
int_ver = int_ver_match.group(1) if int_ver_match else None
|
||||
logger.info(f"Internal winetricks version: {int_ver}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read internal winetricks version: {e}")
|
||||
|
||||
update_needed = not os.path.exists(self.winetricks_path) or (int_ver != ext_ver and ext_ver)
|
||||
|
||||
if update_needed:
|
||||
try:
|
||||
self.downloader.download(url, self.winetricks_path)
|
||||
os.chmod(self.winetricks_path, 0o755)
|
||||
logger.info(f"Winetricks updated to version {ext_ver}")
|
||||
self.apply_modifications(self.winetricks_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update winetricks: {e}")
|
||||
elif os.path.exists(self.winetricks_path):
|
||||
self.apply_modifications(self.winetricks_path)
|
||||
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
def apply_modifications(self, file_path):
|
||||
"""Apply custom modifications to the winetricks script."""
|
||||
if not os.path.exists(file_path):
|
||||
return
|
||||
try:
|
||||
with open(file_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Apply sed-like replacements
|
||||
content = re.sub(r'w_metadata vcrun2015 dlls \\', r'w_metadata !dont_use_2015! dlls \\', content)
|
||||
content = re.sub(r'w_metadata vcrun2017 dlls \\', r'w_metadata !dont_use_2017! dlls \\', content)
|
||||
content = re.sub(r'w_metadata vcrun2019 dlls \\', r'w_metadata !dont_use_2019! dlls \\', content)
|
||||
content = re.sub(r'w_set_winver win2k3', r'w_set_winver win7', content)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
logger.info("Winetricks modifications applied")
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying modifications to winetricks: {e}")
|
||||
|
||||
def setup_ui(self):
|
||||
"""Set up the user interface with tabs and tables."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
||||
main_layout.setSpacing(10)
|
||||
|
||||
# Log output
|
||||
self.log_output = QTextEdit()
|
||||
self.log_output.setReadOnly(True)
|
||||
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
|
||||
main_layout.addWidget(self.log_output)
|
||||
|
||||
# Tab widget
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet(self.theme.WINETRICKS_TAB_STYLE)
|
||||
|
||||
table_base_style = self.theme.WINETRICKS_TABBLE_STYLE
|
||||
|
||||
# DLLs tab
|
||||
self.dll_table = QTableWidget()
|
||||
self.dll_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.dll_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.dll_table.setColumnCount(3)
|
||||
self.dll_table.setHorizontalHeaderLabels([_("Set"), _("Libraries"), _("Information")])
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.dll_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.dll_table.setStyleSheet(table_base_style)
|
||||
|
||||
self.dll_preloader = Preloader()
|
||||
dll_preloader_container = QWidget()
|
||||
dll_preloader_layout = QVBoxLayout(dll_preloader_container)
|
||||
dll_preloader_layout.addStretch()
|
||||
dll_preloader_hlayout = QHBoxLayout()
|
||||
dll_preloader_hlayout.addStretch()
|
||||
dll_preloader_hlayout.addWidget(self.dll_preloader)
|
||||
dll_preloader_hlayout.addStretch()
|
||||
dll_preloader_layout.addLayout(dll_preloader_hlayout)
|
||||
dll_preloader_layout.addStretch()
|
||||
dll_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
||||
dll_preloader_layout.setSpacing(0)
|
||||
|
||||
self.dll_container = QStackedWidget()
|
||||
self.dll_container.addWidget(dll_preloader_container)
|
||||
self.dll_container.addWidget(self.dll_table)
|
||||
self.tab_widget.addTab(self.dll_container, "DLLs")
|
||||
|
||||
# Fonts tab
|
||||
self.fonts_table = QTableWidget()
|
||||
self.fonts_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.fonts_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.fonts_table.setColumnCount(3)
|
||||
self.fonts_table.setHorizontalHeaderLabels([_("Set"), _("Fonts"), _("Information")])
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.fonts_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.fonts_table.setStyleSheet(table_base_style)
|
||||
|
||||
self.fonts_preloader = Preloader()
|
||||
fonts_preloader_container = QWidget()
|
||||
fonts_preloader_layout = QVBoxLayout(fonts_preloader_container)
|
||||
fonts_preloader_layout.addStretch()
|
||||
fonts_preloader_hlayout = QHBoxLayout()
|
||||
fonts_preloader_hlayout.addStretch()
|
||||
fonts_preloader_hlayout.addWidget(self.fonts_preloader)
|
||||
fonts_preloader_hlayout.addStretch()
|
||||
fonts_preloader_layout.addLayout(fonts_preloader_hlayout)
|
||||
fonts_preloader_layout.addStretch()
|
||||
fonts_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
||||
fonts_preloader_layout.setSpacing(0)
|
||||
|
||||
self.fonts_container = QStackedWidget()
|
||||
self.fonts_container.addWidget(fonts_preloader_container)
|
||||
self.fonts_container.addWidget(self.fonts_table)
|
||||
self.tab_widget.addTab(self.fonts_container, _("Fonts"))
|
||||
|
||||
# Settings tab
|
||||
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([_("Set"), _("Settings"), _("Information")])
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.settings_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.settings_table.setStyleSheet(table_base_style)
|
||||
|
||||
self.settings_preloader = Preloader()
|
||||
settings_preloader_container = QWidget()
|
||||
settings_preloader_layout = QVBoxLayout(settings_preloader_container)
|
||||
settings_preloader_layout.addStretch()
|
||||
settings_preloader_hlayout = QHBoxLayout()
|
||||
settings_preloader_hlayout.addStretch()
|
||||
settings_preloader_hlayout.addWidget(self.settings_preloader)
|
||||
settings_preloader_hlayout.addStretch()
|
||||
settings_preloader_layout.addLayout(settings_preloader_hlayout)
|
||||
settings_preloader_layout.addStretch()
|
||||
settings_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
||||
settings_preloader_layout.setSpacing(0)
|
||||
|
||||
self.settings_container = QStackedWidget()
|
||||
self.settings_container.addWidget(settings_preloader_container)
|
||||
self.settings_container.addWidget(self.settings_table)
|
||||
self.tab_widget.addTab(self.settings_container, _("Settings"))
|
||||
|
||||
self.containers = {
|
||||
"dlls": self.dll_container,
|
||||
"fonts": self.fonts_container,
|
||||
"settings": self.settings_container
|
||||
}
|
||||
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||
self.force_button = AutoSizeButton(_("Force Install"), icon=theme_manager.get_icon("apply"))
|
||||
self.install_button = AutoSizeButton(_("Install"), icon=theme_manager.get_icon("apply"))
|
||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
self.force_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
self.install_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
button_layout.addWidget(self.force_button)
|
||||
button_layout.addWidget(self.install_button)
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
|
||||
self.install_button.clicked.connect(lambda: self.install_selected(force=False))
|
||||
|
||||
def load_lists(self):
|
||||
"""Load and populate the lists for DLLs, Fonts, and Settings"""
|
||||
if not os.path.exists(self.winetricks_path):
|
||||
QMessageBox.warning(self, _("Error"), _("Winetricks not found. Please try again."))
|
||||
self.reject()
|
||||
return
|
||||
|
||||
assert self.prefix_path is not None
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", self.prefix_path)
|
||||
if self.wine_use is not None:
|
||||
env.insert("WINE", self.wine_use)
|
||||
|
||||
cwd = os.path.dirname(self.winetricks_path)
|
||||
|
||||
# DLLs
|
||||
self.containers["dlls"].setCurrentIndex(0)
|
||||
self._start_list_process("dlls", self.dll_table, self.get_dll_exclusions(), env, cwd)
|
||||
|
||||
# Fonts
|
||||
self.containers["fonts"].setCurrentIndex(0)
|
||||
self._start_list_process("fonts", self.fonts_table, self.get_fonts_exclusions(), env, cwd)
|
||||
|
||||
# Settings
|
||||
self.containers["settings"].setCurrentIndex(0)
|
||||
self._start_list_process("settings", self.settings_table, self.get_settings_exclusions(), env, cwd)
|
||||
|
||||
def _start_list_process(self, category, table, exclusion_pattern, env, cwd):
|
||||
"""Запускает QProcess для списка."""
|
||||
process = QProcess(self)
|
||||
process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
|
||||
process.setProcessEnvironment(env)
|
||||
process.finished.connect(lambda exit_code, exit_status: self._on_list_finished(category, table, exclusion_pattern, process, exit_code, exit_status))
|
||||
process.start(self.winetricks_path, [category, "list"])
|
||||
|
||||
def _on_list_finished(self, category, table, exclusion_pattern, process: QProcess | None, exit_code, exit_status):
|
||||
"""Обработчик завершения списка."""
|
||||
if process is None:
|
||||
logger.error(f"Process is None for {category}")
|
||||
self.containers[category].setCurrentIndex(1)
|
||||
return
|
||||
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
|
||||
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
|
||||
self.populate_table(table, output, exclusion_pattern, self.log_path)
|
||||
# Restore focus after populating
|
||||
if table.rowCount() > 0:
|
||||
table.setCurrentCell(0, 0)
|
||||
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
else:
|
||||
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
|
||||
logger.error(f"Failed to list {category}: {error_output}")
|
||||
|
||||
self.containers[category].setCurrentIndex(1)
|
||||
|
||||
def get_dll_exclusions(self):
|
||||
"""Get regex pattern for DLL exclusions."""
|
||||
return r'(d3d|directx9|dont_use|dxvk|vkd3d|galliumnine|faudio1|Foundation)'
|
||||
|
||||
def get_fonts_exclusions(self):
|
||||
"""Get regex pattern for Fonts exclusions."""
|
||||
return r'dont_use'
|
||||
|
||||
def get_settings_exclusions(self):
|
||||
"""Get regex pattern for Settings exclusions."""
|
||||
return r'(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home)'
|
||||
|
||||
def populate_table(self, table, output, exclusion_pattern, log_path):
|
||||
"""Populate the table with items from output, checking installation status."""
|
||||
table.setRowCount(0)
|
||||
table.verticalHeader().setVisible(False)
|
||||
lines = output.strip().split('\n')
|
||||
installed = set()
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path) as f:
|
||||
for line in f:
|
||||
installed.add(line.strip())
|
||||
|
||||
# regex-парсинг (имя - первое слово, остальное - описание)
|
||||
line_re = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or re.search(exclusion_pattern, line, re.I):
|
||||
continue
|
||||
|
||||
line = line.split('(', 1)[0].strip()
|
||||
|
||||
match = line_re.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
_status, name, info = match.groups()
|
||||
# Очищаем info от мусора
|
||||
info = re.sub(r'\[.*?\]', '', info).strip() # Удаляем [скачивания] и т.п.
|
||||
|
||||
# To match bash desc extraction: after name, substr(2) to trim leading space
|
||||
if info.startswith(' '):
|
||||
info = info[1:].lstrip()
|
||||
|
||||
# Фильтр служебных строк
|
||||
if '/' in name or '\\' in name or name.lower() in ('executing', 'using', 'warning:') or name.endswith(':'):
|
||||
continue
|
||||
|
||||
checked = Qt.CheckState.Checked if name in installed else Qt.CheckState.Unchecked
|
||||
|
||||
row = table.rowCount()
|
||||
table.insertRow(row)
|
||||
|
||||
# Checkbox
|
||||
checkbox = QTableWidgetItem()
|
||||
checkbox.setCheckState(checked)
|
||||
table.setItem(row, 0, checkbox)
|
||||
|
||||
# Name
|
||||
name_item = QTableWidgetItem(name)
|
||||
table.setItem(row, 1, name_item)
|
||||
|
||||
# Info
|
||||
info_item = QTableWidgetItem(info)
|
||||
table.setItem(row, 2, info_item)
|
||||
|
||||
def install_selected(self, force=False):
|
||||
"""Install selected components."""
|
||||
selected = []
|
||||
for table in [self.dll_table, self.fonts_table, self.settings_table]:
|
||||
for row in range(table.rowCount()):
|
||||
checkbox = table.item(row, 0)
|
||||
if checkbox is not None and checkbox.checkState() == Qt.CheckState.Checked:
|
||||
name_item = table.item(row, 1)
|
||||
if name_item is not None:
|
||||
name = name_item.text()
|
||||
if name and name not in selected:
|
||||
selected.append(name)
|
||||
|
||||
# Load installed
|
||||
installed = set()
|
||||
if os.path.exists(self.log_path):
|
||||
with open(self.log_path) as f:
|
||||
for line in f:
|
||||
installed.add(line.strip())
|
||||
|
||||
# Filter to new selected
|
||||
new_selected = [name for name in selected if name not in installed]
|
||||
|
||||
if not new_selected:
|
||||
QMessageBox.information(self, _("Warning"), _("No components selected."))
|
||||
return
|
||||
|
||||
self.install_button.setEnabled(False)
|
||||
self.force_button.setEnabled(False)
|
||||
self.cancel_button.setEnabled(False)
|
||||
|
||||
self._start_install_process(new_selected, force)
|
||||
|
||||
def _start_install_process(self, selected, force):
|
||||
"""Запускает QProcess для установки."""
|
||||
assert self.prefix_path is not None
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", self.prefix_path)
|
||||
if self.wine_use is not None:
|
||||
env.insert("WINE", self.wine_use)
|
||||
|
||||
self.apply_process = QProcess(self)
|
||||
self.apply_process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
|
||||
self.apply_process.setProcessEnvironment(env)
|
||||
self.apply_process.readyReadStandardOutput.connect(self._on_ready_read)
|
||||
self.apply_process.finished.connect(lambda exit_code, exit_status: self._on_install_finished(exit_code, exit_status, selected))
|
||||
args = ["--unattended"] + (["--force"] if force else []) + selected
|
||||
self.apply_process.start(self.winetricks_path, args)
|
||||
|
||||
def _on_ready_read(self):
|
||||
"""Handle ready read for install process."""
|
||||
if self.apply_process is None:
|
||||
return
|
||||
data = self.apply_process.readAllStandardOutput().data()
|
||||
message = bytes(data).decode('utf-8', 'ignore').strip()
|
||||
self._log(message)
|
||||
|
||||
def _on_install_finished(self, exit_code, exit_status, selected):
|
||||
"""Обработчик завершения установки."""
|
||||
error_message = ""
|
||||
if self.apply_process is not None:
|
||||
# Читаем вывод в зависимости от режима каналов
|
||||
if self.apply_process.processChannelMode() == QProcess.ProcessChannelMode.MergedChannels:
|
||||
# Если каналы объединены, читаем из StandardOutput
|
||||
output_data = self.apply_process.readAllStandardOutput().data()
|
||||
error_message = bytes(output_data).decode('utf-8', 'ignore')
|
||||
else:
|
||||
# Если каналы разделены, читаем из StandardError
|
||||
error_data = self.apply_process.readAllStandardError().data()
|
||||
error_message = bytes(error_data).decode('utf-8', 'ignore')
|
||||
|
||||
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||
logger.error(f"Winetricks install failed: {error_message}")
|
||||
QMessageBox.warning(self, _("Error"), _("Installation failed. Check logs."))
|
||||
else:
|
||||
if os.path.exists(self.log_path):
|
||||
with open(self.log_path) as f:
|
||||
existing = {line.strip() for line in f if line.strip()}
|
||||
else:
|
||||
existing = set()
|
||||
with open(self.log_path, 'a') as f:
|
||||
for name in selected:
|
||||
if name not in existing:
|
||||
f.write(f"{name}\n")
|
||||
logger.info("Winetricks installation completed successfully.")
|
||||
QMessageBox.information(self, _("Success"), _("Components installed successfully."))
|
||||
self.load_lists()
|
||||
|
||||
# Разблокировка
|
||||
self.install_button.setEnabled(True)
|
||||
self.force_button.setEnabled(True)
|
||||
self.cancel_button.setEnabled(True)
|
||||
|
||||
def _log(self, message):
|
||||
"""Добавляет в лог."""
|
||||
self.log_output.append(message)
|
||||
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
||||
|
@@ -35,7 +35,6 @@ class MainWindowProtocol(Protocol):
|
||||
_last_card_width: int
|
||||
current_hovered_card: GameCard | None
|
||||
current_focused_card: GameCard | None
|
||||
gamesListWidget: QWidget | None
|
||||
|
||||
class GameLibraryManager:
|
||||
def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None):
|
||||
|
@@ -5,7 +5,7 @@ from typing import Protocol, cast
|
||||
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
||||
from enum import Enum
|
||||
from pyudev import Context, Monitor, MonitorObserver, Device
|
||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView
|
||||
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.QtGui import QKeyEvent, QMouseEvent
|
||||
from portprotonqt.logger import get_logger
|
||||
@@ -13,7 +13,7 @@ from portprotonqt.image_utils import FullscreenDialog
|
||||
from portprotonqt.custom_widgets import NavLabel, AutoSizeButton
|
||||
from portprotonqt.game_card import GameCard
|
||||
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config
|
||||
from portprotonqt.dialogs import AddGameDialog, WinetricksDialog
|
||||
from portprotonqt.dialogs import AddGameDialog
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -446,6 +446,7 @@ class InputManager(QObject):
|
||||
if not self._gamepad_handling_enabled:
|
||||
return
|
||||
try:
|
||||
|
||||
app = QApplication.instance()
|
||||
active = QApplication.activeWindow()
|
||||
focused = QApplication.focusWidget()
|
||||
@@ -550,38 +551,6 @@ class InputManager(QObject):
|
||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||
return
|
||||
|
||||
if isinstance(active, WinetricksDialog):
|
||||
if button_code in BUTTONS['confirm']: # A button - toggle checkbox
|
||||
current_table = active.tab_widget.currentWidget()
|
||||
if isinstance(current_table, QTableWidget):
|
||||
current_row = current_table.currentRow()
|
||||
if current_row >= 0:
|
||||
checkbox = current_table.item(current_row, 0)
|
||||
if checkbox:
|
||||
checkbox.setCheckState(
|
||||
Qt.CheckState.Unchecked if checkbox.checkState() == Qt.CheckState.Checked else Qt.CheckState.Checked
|
||||
)
|
||||
return
|
||||
elif button_code in BUTTONS['add_game']: # X button - install
|
||||
active.install_selected(force=False)
|
||||
return
|
||||
elif button_code in BUTTONS['prev_dir']: # Y button - force install
|
||||
active.install_selected(force=True)
|
||||
return
|
||||
elif button_code in BUTTONS['back']: # B button - close dialog
|
||||
active.reject()
|
||||
return
|
||||
elif button_code in BUTTONS['prev_tab']: # LB - previous tab
|
||||
current_idx = active.tab_widget.currentIndex()
|
||||
new_idx = (current_idx - 1) % active.tab_widget.count()
|
||||
active.tab_widget.setCurrentIndex(new_idx)
|
||||
return
|
||||
elif button_code in BUTTONS['next_tab']: # RB - next tab
|
||||
current_idx = active.tab_widget.currentIndex()
|
||||
new_idx = (current_idx + 1) % active.tab_widget.count()
|
||||
active.tab_widget.setCurrentIndex(new_idx)
|
||||
return
|
||||
|
||||
# Standard navigation
|
||||
if button_code in BUTTONS['confirm']:
|
||||
self._parent.activateFocusedWidget()
|
||||
@@ -612,7 +581,6 @@ class InputManager(QObject):
|
||||
new_value = max(size_slider.value() - 10, size_slider.minimum())
|
||||
size_slider.setValue(new_value)
|
||||
self._parent.on_slider_released()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
||||
|
||||
@@ -629,9 +597,6 @@ class InputManager(QObject):
|
||||
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
||||
if not self._gamepad_handling_enabled:
|
||||
return
|
||||
if not hasattr(self._parent, 'gamesListWidget') or self._parent.gamesListWidget is None:
|
||||
logger.error("gamesListWidget not available yet, skipping D-pad navigation")
|
||||
return
|
||||
try:
|
||||
|
||||
app = QApplication.instance()
|
||||
@@ -673,7 +638,7 @@ class InputManager(QObject):
|
||||
elif value < 0: # Left
|
||||
active.focusPreviousChild()
|
||||
return
|
||||
elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0 and not isinstance(focused, QTableWidget): # Skip if focused on table
|
||||
elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: # Keep up/down for other dialogs
|
||||
if not focused or not active.focusWidget():
|
||||
# If no widget is focused, focus the first focusable widget
|
||||
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||
@@ -726,52 +691,6 @@ class InputManager(QObject):
|
||||
active.show_next()
|
||||
return
|
||||
|
||||
# Table navigation
|
||||
if isinstance(focused, QTableWidget):
|
||||
row_count = focused.rowCount()
|
||||
if row_count <= 0:
|
||||
return
|
||||
current_row = focused.currentRow()
|
||||
if current_row < 0:
|
||||
current_row = 0
|
||||
focused.setCurrentCell(0, 0)
|
||||
|
||||
if code == ecodes.ABS_HAT0Y and value != 0:
|
||||
# Vertical navigation
|
||||
if value > 0: # Down
|
||||
new_row = min(current_row + 1, row_count - 1)
|
||||
elif value < 0: # Up
|
||||
new_row = max(current_row - 1, 0)
|
||||
else:
|
||||
return
|
||||
|
||||
focused.setCurrentCell(new_row, focused.currentColumn())
|
||||
item = focused.item(new_row, focused.currentColumn())
|
||||
if item:
|
||||
focused.scrollToItem(
|
||||
item,
|
||||
QAbstractItemView.ScrollHint.PositionAtCenter
|
||||
)
|
||||
focused.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
elif code == ecodes.ABS_HAT0X and value != 0:
|
||||
# Horizontal navigation
|
||||
col_count = focused.columnCount()
|
||||
current_col = focused.currentColumn()
|
||||
if current_col < 0:
|
||||
current_col = 0
|
||||
|
||||
if value < 0: # Left
|
||||
new_col = max(current_col - 1, 0)
|
||||
elif value > 0: # Right
|
||||
new_col = min(current_col + 1, col_count - 1)
|
||||
else:
|
||||
return
|
||||
|
||||
focused.setCurrentCell(focused.currentRow(), new_col)
|
||||
focused.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
|
||||
# Library tab navigation (index 0)
|
||||
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
||||
focused = QApplication.focusWidget()
|
||||
|
Binary file not shown.
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-07 15:45+0500\n"
|
||||
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -191,10 +191,6 @@ msgstr ""
|
||||
msgid "Failed to delete custom data: {error}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{game_name}' successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game name and executable path are required"
|
||||
msgstr ""
|
||||
|
||||
@@ -308,45 +304,6 @@ msgstr ""
|
||||
msgid "No cover selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fonts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
msgid "No components selected."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -425,95 +382,13 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Uninstaller"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Load Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Compatibility Tool"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start restore process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore failed."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete prefix: {}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Compatibility tool '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete compatibility tool: {}"
|
||||
msgid "Various Wine parameters and versions..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Main PortProton parameters..."
|
||||
|
Binary file not shown.
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-07 15:45+0500\n"
|
||||
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -191,10 +191,6 @@ msgstr ""
|
||||
msgid "Failed to delete custom data: {error}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{game_name}' successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game name and executable path are required"
|
||||
msgstr ""
|
||||
|
||||
@@ -308,45 +304,6 @@ msgstr ""
|
||||
msgid "No cover selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fonts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
msgid "No components selected."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -425,95 +382,13 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Uninstaller"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Load Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Compatibility Tool"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start restore process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore failed."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete prefix: {}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Compatibility tool '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete compatibility tool: {}"
|
||||
msgid "Various Wine parameters and versions..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Main PortProton parameters..."
|
||||
|
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-07 15:45+0500\n"
|
||||
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -189,10 +189,6 @@ msgstr ""
|
||||
msgid "Failed to delete custom data: {error}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{game_name}' successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game name and executable path are required"
|
||||
msgstr ""
|
||||
|
||||
@@ -306,45 +302,6 @@ msgstr ""
|
||||
msgid "No cover selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fonts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
msgid "No components selected."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -423,95 +380,13 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Uninstaller"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Load Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Compatibility Tool"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start restore process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore failed."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete prefix: {}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Compatibility tool '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete compatibility tool: {}"
|
||||
msgid "Various Wine parameters and versions..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Main PortProton parameters..."
|
||||
|
Binary file not shown.
@@ -9,17 +9,18 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-07 15:45+0500\n"
|
||||
"PO-Revision-Date: 2025-10-07 15:44+0500\n"
|
||||
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||
"PO-Revision-Date: 2025-09-23 22:23+0500\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: ru_RU\n"
|
||||
"Language-Team: ru_RU <LL@li.org>\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"Language: ru_RU\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 "
|
||||
"&& (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
"X-Generator: Poedit 3.6\n"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "Ошибка"
|
||||
@@ -86,11 +87,11 @@ msgstr "Успешно"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"'{game_name}' was added to Steam. Please restart Steam for changes to "
|
||||
"take effect."
|
||||
"'{game_name}' was added to Steam. Please restart Steam for changes to take "
|
||||
"effect."
|
||||
msgstr ""
|
||||
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите "
|
||||
"Steam, чтобы изменения вступили в силу."
|
||||
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите Steam, "
|
||||
"чтобы изменения вступили в силу."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Executable not found for game: {game_name}"
|
||||
@@ -178,11 +179,11 @@ msgstr "Подтвердите удаление"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Are you sure you want to delete '{game_name}'? This will remove the "
|
||||
".desktop file and custom data."
|
||||
"Are you sure you want to delete '{game_name}'? This will remove the .desktop "
|
||||
"file and custom data."
|
||||
msgstr ""
|
||||
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению "
|
||||
"файла .desktop и пользовательских данных."
|
||||
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению файла ."
|
||||
"desktop и пользовательских данных."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete .desktop file: {error}"
|
||||
@@ -196,10 +197,6 @@ msgstr "'{game_name}' был(а) успешно удалён(а)"
|
||||
msgid "Failed to delete custom data: {error}"
|
||||
msgstr "Не удалось удалить пользовательские данные: {error}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{game_name}' successfully"
|
||||
msgstr "'{game_name}' успешно добавлен(а)"
|
||||
|
||||
msgid "Game name and executable path are required"
|
||||
msgstr "Требуются название игры и путь к исполняемому файлу"
|
||||
|
||||
@@ -228,11 +225,11 @@ msgstr "Не удалось добавить '{game_name}' в Steam: {error}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"'{game_name}' was removed from Steam. Please restart Steam for changes to"
|
||||
" take effect."
|
||||
"'{game_name}' was removed from Steam. Please restart Steam for changes to take "
|
||||
"effect."
|
||||
msgstr ""
|
||||
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam,"
|
||||
" чтобы изменения вступили в силу."
|
||||
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam, чтобы "
|
||||
"изменения вступили в силу."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to remove game '{game_name}' from Steam: {error}"
|
||||
@@ -277,7 +274,7 @@ msgstr "Путь: "
|
||||
|
||||
#, python-format
|
||||
msgid "Access denied: %s"
|
||||
msgstr "Доступ запрещён: %s"
|
||||
msgstr "Доступ запрещен: %s"
|
||||
|
||||
msgid "Edit Game"
|
||||
msgstr "Редактировать игру"
|
||||
@@ -315,45 +312,6 @@ msgstr "Скачивание обложки..."
|
||||
msgid "No cover selected"
|
||||
msgstr "Обложка не выбрана"
|
||||
|
||||
msgid "Prefix Manager"
|
||||
msgstr "Менеджер префиксов"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Выбор"
|
||||
|
||||
msgid "Libraries"
|
||||
msgstr "Библиотеки"
|
||||
|
||||
msgid "Information"
|
||||
msgstr "Описание"
|
||||
|
||||
msgid "Fonts"
|
||||
msgstr "Шрифты"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr "Принудительно установить"
|
||||
|
||||
msgid "Install"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr "Winetricks не найден. Повторите попытку."
|
||||
|
||||
msgid "Warning"
|
||||
msgstr "Предупреждение"
|
||||
|
||||
msgid "No components selected."
|
||||
msgstr "Не выбрано ни одного компонента."
|
||||
|
||||
msgid "Installation failed. Check logs."
|
||||
msgstr "Установка не удалась. Проверьте журналы."
|
||||
|
||||
msgid "Components installed successfully."
|
||||
msgstr "Компоненты успешно установлены."
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr "Загрузка игр из Epic Games Store..."
|
||||
|
||||
@@ -432,96 +390,14 @@ msgstr "Игровая библиотека"
|
||||
msgid "Find Games ..."
|
||||
msgstr "Найти игры..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr "'{name}' добавлен(а)"
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr "Здесь можно настроить автоматическую установку игр..."
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr "Список доступных эмуляторов и их настройка..."
|
||||
|
||||
msgid "Compatibility tool:"
|
||||
msgstr "Инструмент совместимости:"
|
||||
|
||||
msgid "Prefix:"
|
||||
msgstr "Префикс:"
|
||||
|
||||
msgid "Wine Configuration"
|
||||
msgstr "Конфигурация Wine"
|
||||
|
||||
msgid "Registry Editor"
|
||||
msgstr "Редактор реестра"
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr "Панель управления"
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr "Диспетчер задач"
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr "Командная строка"
|
||||
|
||||
msgid "Uninstaller"
|
||||
msgstr "Удаление программ"
|
||||
|
||||
msgid "Create Prefix Backup"
|
||||
msgstr "Создать резервную копию префикса"
|
||||
|
||||
msgid "Load Prefix Backup"
|
||||
msgstr "Загрузить резервную копию префикса"
|
||||
|
||||
msgid "Delete Compatibility Tool"
|
||||
msgstr "Удалить Инструмент совместимости"
|
||||
|
||||
msgid "Delete Prefix"
|
||||
msgstr "Удалить Префикс"
|
||||
|
||||
msgid "Clear Prefix"
|
||||
msgstr "Очистить Префикс"
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr "Не удалось запустить процесс резервного копирования."
|
||||
|
||||
msgid "Failed to start restore process."
|
||||
msgstr "Не удалось запустить процесс восстановления."
|
||||
|
||||
msgid "Prefix backup completed."
|
||||
msgstr "Резервное копирование префикса завершено."
|
||||
|
||||
msgid "Prefix backup failed."
|
||||
msgstr "Сбой резервного копирования префикса."
|
||||
|
||||
msgid "Prefix restore completed."
|
||||
msgstr "Восстановление префикса завершено."
|
||||
|
||||
msgid "Prefix restore failed."
|
||||
msgstr "Восстановление префикса не удалось."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete prefix '{}'?"
|
||||
msgstr "Вы уверены, что хотите удалить префикс «{}»?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' deleted."
|
||||
msgstr "Префикс «{}» удален."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete prefix: {}"
|
||||
msgstr "Не удалось удалить префикс: {}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
||||
msgstr "Вы уверены, что хотите удалить инструмент совместимости «{}»?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Compatibility tool '{}' deleted."
|
||||
msgstr "Инструмент совместимости «{}» удален."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete compatibility tool: {}"
|
||||
msgstr "Не удалось удалить инструмент совместимости: {}"
|
||||
msgid "Various Wine parameters and versions..."
|
||||
msgstr "Различные параметры и версии wine..."
|
||||
|
||||
msgid "Main PortProton parameters..."
|
||||
msgstr "Основные параметры PortProton..."
|
||||
@@ -606,8 +482,7 @@ msgstr "Подтвердите удаление"
|
||||
|
||||
msgid "Are you sure you want to reset all settings? This action cannot be undone."
|
||||
msgstr ""
|
||||
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя "
|
||||
"отменить."
|
||||
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя отменить."
|
||||
|
||||
msgid "Settings reset. Restarting..."
|
||||
msgstr "Настройки сброшены. Перезапуск..."
|
||||
@@ -779,4 +654,3 @@ msgstr "Нет избранных"
|
||||
|
||||
msgid "No recent games"
|
||||
msgstr "Нет недавних игр"
|
||||
|
||||
|
@@ -7,13 +7,14 @@ import sys
|
||||
import psutil
|
||||
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
||||
from portprotonqt.game_card import GameCard
|
||||
from portprotonqt.animations import DetailPageAnimations
|
||||
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel
|
||||
from portprotonqt.portproton_api import PortProtonAPI
|
||||
from portprotonqt.input_manager import InputManager
|
||||
from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit
|
||||
from portprotonqt.preloader import Preloader
|
||||
from portprotonqt.system_overlay import SystemOverlay
|
||||
from portprotonqt.input_manager import GamepadType
|
||||
|
||||
@@ -38,8 +39,8 @@ from portprotonqt.game_library_manager import GameLibraryManager
|
||||
|
||||
|
||||
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout)
|
||||
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot, QProcess
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy)
|
||||
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot
|
||||
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
|
||||
from typing import cast
|
||||
from collections.abc import Callable
|
||||
@@ -219,6 +220,17 @@ class MainWindow(QMainWindow):
|
||||
self.resize(width, height)
|
||||
else:
|
||||
self.showNormal()
|
||||
self._preloader = Preloader(parent=self)
|
||||
self._update_preloader_position()
|
||||
|
||||
def _update_preloader_position(self):
|
||||
if self._preloader:
|
||||
self._preloader.move(self.rect().center() - self._preloader.rect().center())
|
||||
|
||||
def _close_preloader(self):
|
||||
if self._preloader:
|
||||
self._preloader.close()
|
||||
self._preloader = None
|
||||
|
||||
def on_slider_released(self) -> None:
|
||||
"""Delegate to game library manager."""
|
||||
@@ -434,6 +446,7 @@ class MainWindow(QMainWindow):
|
||||
def on_games_loaded(self, games: list[tuple]):
|
||||
self.game_library_manager.set_games(games)
|
||||
self.progress_bar.setVisible(False)
|
||||
self._close_preloader()
|
||||
|
||||
def open_portproton_forum_topic(self, topic_name: str):
|
||||
"""Open the PortProton forum topic or search page for this game."""
|
||||
@@ -769,7 +782,6 @@ class MainWindow(QMainWindow):
|
||||
def createInstalledTab(self):
|
||||
self.gamesLibraryWidget = self.game_library_manager.create_games_library_widget()
|
||||
self.stackedWidget.addWidget(self.gamesLibraryWidget)
|
||||
self.gamesListWidget = self.game_library_manager.gamesListWidget
|
||||
self.game_library_manager.update_game_grid()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
@@ -787,6 +799,7 @@ class MainWindow(QMainWindow):
|
||||
self._last_width = self.width()
|
||||
if abs(self.width() - self._last_width) > 10:
|
||||
self._last_width = self.width()
|
||||
self._update_preloader_position()
|
||||
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
@@ -947,6 +960,7 @@ class MainWindow(QMainWindow):
|
||||
# Trigger visible images load
|
||||
QTimer.singleShot(200, self.game_library_manager.load_visible_images)
|
||||
|
||||
self.update_status_message.emit(_("Enriching from Steam..."), 3000)
|
||||
from portprotonqt.steam_api import get_steam_game_info_async
|
||||
get_steam_game_info_async(final_name, exec_line, on_steam_info)
|
||||
|
||||
@@ -1005,245 +1019,14 @@ class MainWindow(QMainWindow):
|
||||
self.wineTitle.setObjectName("tabTitle")
|
||||
layout.addWidget(self.wineTitle)
|
||||
|
||||
if self.portproton_location is None:
|
||||
return
|
||||
|
||||
dist_path = os.path.join(self.portproton_location, "data", "dist")
|
||||
prefixes_path = os.path.join(self.portproton_location, "data", "prefixes")
|
||||
|
||||
if not os.path.exists(dist_path):
|
||||
return
|
||||
|
||||
formLayout = QFormLayout()
|
||||
formLayout.setContentsMargins(0, 10, 0, 0)
|
||||
formLayout.setSpacing(10)
|
||||
formLayout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
self.wine_versions = [d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))]
|
||||
self.wineCombo = QComboBox()
|
||||
self.wineCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.wineCombo.addItems(self.wine_versions)
|
||||
self.wineCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||
self.wineCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.wineTitleLabel = QLabel(_("Compatibility tool:"))
|
||||
self.wineTitleLabel.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
self.wineTitleLabel.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
if self.wine_versions:
|
||||
self.wineCombo.setCurrentIndex(0)
|
||||
formLayout.addRow(self.wineTitleLabel, self.wineCombo)
|
||||
|
||||
self.prefixes = [d for d in os.listdir(prefixes_path) if os.path.isdir(os.path.join(prefixes_path, d))] if os.path.exists(prefixes_path) else []
|
||||
self.prefixCombo = QComboBox()
|
||||
self.prefixCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.prefixCombo.addItems(self.prefixes)
|
||||
self.prefixCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||
self.prefixCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.prefixTitleLabel = QLabel(_("Prefix:"))
|
||||
self.prefixTitleLabel.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
self.prefixTitleLabel.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
if self.prefixes:
|
||||
self.prefixCombo.setCurrentIndex(0)
|
||||
formLayout.addRow(self.prefixTitleLabel, self.prefixCombo)
|
||||
|
||||
layout.addLayout(formLayout)
|
||||
|
||||
# --- Wine Tools ---
|
||||
tools_grid = QGridLayout()
|
||||
tools_grid.setSpacing(6)
|
||||
|
||||
tools = [
|
||||
("winecfg", _("Wine Configuration")),
|
||||
("regedit", _("Registry Editor")),
|
||||
("control", _("Control Panel")),
|
||||
("taskmgr", _("Task Manager")),
|
||||
("explorer", _("File Explorer")),
|
||||
("cmd", _("Command Prompt")),
|
||||
("uninstaller", _("Uninstaller")),
|
||||
]
|
||||
|
||||
for i, (_tool_cmd, tool_name) in enumerate(tools):
|
||||
row = i // 3
|
||||
col = i % 3
|
||||
btn = AutoSizeButton(tool_name, update_size=False)
|
||||
btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
tools_grid.addWidget(btn, row, col)
|
||||
|
||||
for col in range(3):
|
||||
tools_grid.setColumnStretch(col, 1)
|
||||
|
||||
layout.addLayout(tools_grid)
|
||||
|
||||
# --- Additional Tools ---
|
||||
additional_grid = QGridLayout()
|
||||
additional_grid.setSpacing(6)
|
||||
|
||||
additional_buttons = [
|
||||
("Winetricks", self.open_winetricks),
|
||||
(_("Create Prefix Backup"), self.create_prefix_backup),
|
||||
(_("Load Prefix Backup"), self.load_prefix_backup),
|
||||
(_("Delete Compatibility Tool"), self.delete_compat_tool),
|
||||
(_("Delete Prefix"), self.delete_prefix),
|
||||
(_("Clear Prefix"), None),
|
||||
]
|
||||
|
||||
for i, (text, callback) in enumerate(additional_buttons):
|
||||
row = i // 3
|
||||
col = i % 3
|
||||
btn = AutoSizeButton(text, update_size=False)
|
||||
btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
if callback:
|
||||
btn.clicked.connect(callback)
|
||||
additional_grid.addWidget(btn, row, col)
|
||||
|
||||
for col in range(3):
|
||||
additional_grid.setColumnStretch(col, 1)
|
||||
|
||||
layout.addLayout(additional_grid)
|
||||
tools_grid.setContentsMargins(10, 4, 10, 0)
|
||||
additional_grid.setContentsMargins(10, 6, 10, 0)
|
||||
self.wineContent = QLabel(_("Various Wine parameters and versions..."))
|
||||
self.wineContent.setStyleSheet(self.theme.CONTENT_STYLE)
|
||||
self.wineContent.setObjectName("tabContent")
|
||||
layout.addWidget(self.wineContent)
|
||||
layout.addStretch(1)
|
||||
|
||||
self.stackedWidget.addWidget(self.wineWidget)
|
||||
|
||||
def create_prefix_backup(self):
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
if not selected_prefix:
|
||||
return
|
||||
file_explorer = FileExplorer(self, directory_only=True)
|
||||
file_explorer.file_signal.file_selected.connect(lambda path: self._perform_backup(path, selected_prefix))
|
||||
file_explorer.exec()
|
||||
|
||||
def _perform_backup(self, backup_dir, prefix_name):
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
if not self.portproton_location:
|
||||
return
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if not os.path.exists(start_sh):
|
||||
return
|
||||
self.backup_process = QProcess(self)
|
||||
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
|
||||
cmd = [start_sh, "--backup-prefix", prefix_name, backup_dir]
|
||||
self.backup_process.start(cmd[0], cmd[1:])
|
||||
if not self.backup_process.waitForStarted():
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
|
||||
|
||||
def load_prefix_backup(self):
|
||||
file_explorer = FileExplorer(self, file_filter='.ppack')
|
||||
file_explorer.file_signal.file_selected.connect(self._perform_restore)
|
||||
file_explorer.exec()
|
||||
|
||||
def _perform_restore(self, file_path):
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return
|
||||
if not self.portproton_location:
|
||||
return
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if not os.path.exists(start_sh):
|
||||
return
|
||||
self.restore_process = QProcess(self)
|
||||
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
|
||||
cmd = [start_sh, "--restore-prefix", file_path]
|
||||
self.restore_process.start(cmd[0], cmd[1:])
|
||||
if not self.restore_process.waitForStarted():
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
|
||||
|
||||
def _on_backup_finished(self, exitCode):
|
||||
if exitCode == 0:
|
||||
QMessageBox.information(self, _("Success"), _("Prefix backup completed."))
|
||||
else:
|
||||
QMessageBox.warning(self, _("Error"), _("Prefix backup failed."))
|
||||
|
||||
def _on_restore_finished(self, exitCode):
|
||||
if exitCode == 0:
|
||||
QMessageBox.information(self, _("Success"), _("Prefix restore completed."))
|
||||
else:
|
||||
QMessageBox.warning(self, _("Error"), _("Prefix restore failed."))
|
||||
|
||||
def delete_prefix(self):
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
if not self.portproton_location:
|
||||
return
|
||||
|
||||
if not selected_prefix:
|
||||
return
|
||||
|
||||
prefix_path = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
||||
if not os.path.exists(prefix_path):
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
_("Confirm Deletion"),
|
||||
_("Are you sure you want to delete prefix '{}'?").format(selected_prefix),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
shutil.rmtree(prefix_path)
|
||||
QMessageBox.information(self, _("Success"), _("Prefix '{}' deleted.").format(selected_prefix))
|
||||
# обновляем список
|
||||
self.prefixCombo.clear()
|
||||
self.prefixes = [d for d in os.listdir(os.path.join(self.portproton_location, "data", "prefixes"))
|
||||
if os.path.isdir(os.path.join(self.portproton_location, "data", "prefixes", d))]
|
||||
self.prefixCombo.addItems(self.prefixes)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to delete prefix: {}").format(str(e)))
|
||||
|
||||
def delete_compat_tool(self):
|
||||
"""Удаляет выбранный Wine/Proton дистрибутив из каталога dist."""
|
||||
if not self.portproton_location:
|
||||
return
|
||||
|
||||
selected_tool = self.wineCombo.currentText()
|
||||
if not selected_tool:
|
||||
return
|
||||
|
||||
tool_path = os.path.join(self.portproton_location, "data", "dist", selected_tool)
|
||||
if not os.path.exists(tool_path):
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
_("Confirm Deletion"),
|
||||
_("Are you sure you want to delete compatibility tool '{}'?").format(selected_tool),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
shutil.rmtree(tool_path)
|
||||
QMessageBox.information(self, _("Success"), _("Compatibility tool '{}' deleted.").format(selected_tool))
|
||||
# обновляем список
|
||||
self.wineCombo.clear()
|
||||
self.wine_versions = [d for d in os.listdir(os.path.join(self.portproton_location, "data", "dist"))
|
||||
if os.path.isdir(os.path.join(self.portproton_location, "data", "dist", d))]
|
||||
self.wineCombo.addItems(self.wine_versions)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to delete compatibility tool: {}").format(str(e)))
|
||||
|
||||
def open_winetricks(self):
|
||||
"""Open the Winetricks dialog for the selected prefix and wine."""
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
if not selected_prefix:
|
||||
return
|
||||
|
||||
selected_wine = self.wineCombo.currentText()
|
||||
if not selected_wine:
|
||||
return
|
||||
|
||||
assert self.portproton_location is not None
|
||||
prefix_path = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
||||
wine_path = os.path.join(self.portproton_location, "data", "dist", selected_wine, "bin", "wine")
|
||||
|
||||
# Open Winetricks dialog
|
||||
dialog = WinetricksDialog(self, self.theme, prefix_path, wine_path)
|
||||
dialog.exec()
|
||||
|
||||
def createPortProtonTab(self):
|
||||
"""Вкладка 'PortProton Settings'."""
|
||||
self.portProtonWidget = QWidget()
|
||||
|
@@ -4,6 +4,7 @@ from PySide6.QtCore import QRect
|
||||
from PySide6.QtGui import QPainter, QPen, QBrush, Qt, QColor, QConicalGradient
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
|
||||
class Preloader(QWidget):
|
||||
def __init__(self, speed=180.0, line_line_width=20, color=QColor(0, 120, 215), parent=None):
|
||||
super().__init__(parent)
|
||||
|
@@ -211,28 +211,14 @@ def normalize_name(s):
|
||||
|
||||
def is_valid_candidate(candidate):
|
||||
"""
|
||||
Determines whether a given candidate string is valid for use as a game name.
|
||||
|
||||
The function performs the following checks:
|
||||
1. Normalizes the candidate using `normalize_name()`.
|
||||
2. Rejects the candidate if the normalized name is exactly "game"
|
||||
(to avoid overly generic names).
|
||||
3. Removes spaces and checks for forbidden substrings:
|
||||
- "win32"
|
||||
- "win64"
|
||||
- "gamelauncher"
|
||||
These are checked in the space-free version of the string.
|
||||
4. Returns True only if none of the forbidden conditions are met.
|
||||
|
||||
Args:
|
||||
candidate (str): The candidate string to validate.
|
||||
|
||||
Returns:
|
||||
bool: True if the candidate is valid, False otherwise.
|
||||
Checks if a candidate contains forbidden substrings:
|
||||
- win32
|
||||
- win64
|
||||
- gamelauncher
|
||||
Additionally checks the string without spaces.
|
||||
Returns True if the candidate is valid, otherwise False.
|
||||
"""
|
||||
normalized_candidate = normalize_name(candidate)
|
||||
if normalized_candidate == "game":
|
||||
return False
|
||||
normalized_no_space = normalized_candidate.replace(" ", "")
|
||||
forbidden = ["win32", "win64", "gamelauncher"]
|
||||
for token in forbidden:
|
||||
|
@@ -916,96 +916,6 @@ SETTINGS_CHECKBOX_STYLE = f"""
|
||||
}}
|
||||
"""
|
||||
|
||||
WINETRICKS_TAB_STYLE = f"""
|
||||
QTabWidget::pane {{
|
||||
border: 1px solid {color_d};
|
||||
background: {color_b};
|
||||
border-radius: {border_radius_a};
|
||||
}}
|
||||
QTabBar::tab {{
|
||||
background: {color_c};
|
||||
color: {color_f};
|
||||
padding: 8px 16px;
|
||||
border-top-left-radius: {border_radius_a};
|
||||
border-top-right-radius: {border_radius_a};
|
||||
margin-right: 2px;
|
||||
}}
|
||||
QTabBar::tab:selected {{
|
||||
background: {color_a};
|
||||
color: {color_f};
|
||||
}}
|
||||
QTabBar::tab:hover {{
|
||||
background: {color_e};
|
||||
}}
|
||||
"""
|
||||
|
||||
WINETRICKS_TABBLE_STYLE = f"""
|
||||
QTableWidget {{
|
||||
background: {color_c};
|
||||
color: {color_f};
|
||||
gridline-color: {color_d};
|
||||
alternate-background-color: {color_d};
|
||||
border: {border_a};
|
||||
border-radius: {border_radius_a};
|
||||
font-family: '{font_family}';
|
||||
font-size: {font_size_a};
|
||||
}}
|
||||
QHeaderView::section {{
|
||||
background: {color_d};
|
||||
color: {color_f};
|
||||
padding: 5px;
|
||||
border: {border_a};
|
||||
font-weight: bold;
|
||||
}}
|
||||
QTableWidget::item {{
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid {color_d};
|
||||
}}
|
||||
QTableWidget::item:selected {{
|
||||
background: {color_a};
|
||||
color: {color_f};
|
||||
}}
|
||||
QTableWidget::item:hover {{
|
||||
background: {color_e};
|
||||
}}
|
||||
QTableWidget::indicator {{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: {border_b} {color_a};
|
||||
border-radius: {border_radius_a};
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}}
|
||||
QTableWidget::indicator:unchecked {{
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
image: none;
|
||||
}}
|
||||
QTableWidget::indicator:checked {{
|
||||
background: {color_a};
|
||||
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
|
||||
border: {border_b} {color_f};
|
||||
}}
|
||||
QTableWidget::indicator:hover {{
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: {border_b} {color_a};
|
||||
}}
|
||||
QTableWidget::indicator:focus {{
|
||||
border: {border_c} {color_a};
|
||||
}}
|
||||
{SCROLL_AREA_STYLE}
|
||||
"""
|
||||
|
||||
WINETRICKS_LOG_STYLE = f"""
|
||||
QTextEdit {{
|
||||
background: {color_c};
|
||||
border: {border_a};
|
||||
border-radius: {border_radius_a};
|
||||
color: {color_f};
|
||||
font-family: '{font_family}';
|
||||
font-size: {font_size_a};
|
||||
padding: 5px;
|
||||
}}
|
||||
"""
|
||||
|
||||
FILE_EXPLORER_STYLE = f"""
|
||||
QListView {{
|
||||
font-size: {font_size_a};
|
||||
|
Reference in New Issue
Block a user