36 Commits
v0.1.8 ... main

Author SHA1 Message Date
80a2c06b5a feat: added refresh button
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 17:07:58 +05:00
f0a4ace735 perf: add config and icon caching to reduce I/O and improve UI responsiveness
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 16:57:40 +05:00
7dfaee6831 feat(settings): added proton, 3d_api and prefixes settings
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 13:35:35 +05:00
5481cd80d7 chore: added null pixmaps check
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 12:25:01 +05:00
a016cfa810 chore: convert list to set for optimize
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 12:13:42 +05:00
8fc097ccaf chore: remove broken styles
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 12:03:18 +05:00
ad3eeb6e06 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-15 21:39:05 +05:00
92631cd2c6 chore: separate settings list to new module
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-15 21:39:03 +05:00
4477679f2d chore: replace emulataion buttons to xbox + start
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-15 21:38:07 +05:00
Renovate Bot
b6644eeee5 fix(deps): update dependency pillow to v12 2025-11-15 21:38:07 +05:00
Renovate Bot
2e921226c4 chore(deps): update https://gitea.com/actions/setup-python action to v6 2025-11-15 21:38:06 +05:00
Renovate Bot
4fc1ea73d3 chore(deps): update https://gitea.com/actions/setup-node action to v6 2025-11-15 21:38:06 +05:00
Renovate Bot
3c15cbe495 chore(deps): update archlinux:base-devel docker digest to 943bdad 2025-11-15 21:38:06 +05:00
fed6aafed5 feat: trigger emulation by Xbox + B
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-15 21:38:06 +05:00
2e8be13437 WINETRICKS_TABBLE_STYLE more fixes 2025-11-15 16:22:59 +07:00
ea272c29b6 WINETRICKS_TABBLE_STYLE reworked 2025-11-13 15:43:13 +07:00
17262f6c9f Play Button & Settings Button in row 2025-11-07 12:17:36 +07:00
e07f3f06bc chore(build): return QtSvg to appimage
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-04 12:28:09 +05:00
16a3f4e09a chore(build): added udev rule to allow create virtual devices
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-04 11:14:17 +05:00
a448ba29b0 feat(input_manager): added mouse emulation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-03 12:34:27 +05:00
06e55db54d feat(settings): update styles
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-02 16:05:22 +05:00
5fce23f261 chore: disable pre-commit auto update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-02 15:31:01 +05:00
Renovate Bot
96ad40d625 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.3 2025-11-02 00:01:26 +00:00
Gitea Actions
a30f6f2e74 chore: update steam apps list 2025-11-01T00:01:57Z 2025-11-01 00:01:58 +00:00
0231073b19 feat(settings): added advanced
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-30 16:27:45 +05:00
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
3736bb279e feat: use SGDB for cover too
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-20 13:07:09 +05:00
48 changed files with 15177 additions and 2181 deletions

View File

@@ -62,7 +62,7 @@ jobs:
- name: Install build dependencies
run: |
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
python3-build pyproject-rpm-macros python3-setuptools \
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
redhat-rpm-config nodejs npm
- name: Setup rpmbuild environment
@@ -94,7 +94,7 @@ jobs:
name: Build Arch Package
runs-on: ubuntu-22.04
container:
image: archlinux:base-devel@sha256:87a967f07ba6319fc35c8c4e6ce6acdb4343b57aa817398a5d2db57bd8edc731
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
volumes:
- /usr:/usr-host
- /opt:/opt-host

View File

@@ -119,7 +119,7 @@ jobs:
- name: Install build dependencies
run: |
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
python3-build pyproject-rpm-macros python3-setuptools \
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
redhat-rpm-config nodejs npm
- name: Setup rpmbuild environment

View File

@@ -19,7 +19,7 @@ jobs:
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Python
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: https://gitea.com/actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
with:
python-version-file: "pyproject.toml"

View File

@@ -138,7 +138,7 @@ jobs:
needs: changes
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
container:
image: archlinux:base-devel@sha256:87a967f07ba6319fc35c8c4e6ce6acdb4343b57aa817398a5d2db57bd8edc731
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
volumes:
- /usr:/usr-host
- /opt:/opt-host

View File

@@ -23,7 +23,7 @@ jobs:
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Node.js
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: https://gitea.com/actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
node-version: 20

View File

@@ -14,7 +14,7 @@ jobs:
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Python
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: https://gitea.com/actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
with:
python-version-file: "pyproject.toml"

View File

@@ -13,7 +13,7 @@ jobs:
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Node.js
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: https://gitea.com/actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
node-version: 20

View File

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

View File

@@ -6,11 +6,12 @@ script:
- uv pip install --no-cache-dir ../
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
- cp -r share AppDir/usr
- cp -r lib AppDir/usr
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetwork*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
- shopt -s extglob
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Network*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
AppDir:
path: ./AppDir
after_bundle:

View File

@@ -20,4 +20,5 @@ package() {
cd "$srcdir/PortProtonQt"
python -m installer --destdir="$pkgdir" dist/*.whl
cp -r build-aux/share "$pkgdir/usr/"
cp -r build-aux/lib "$pkgdir/usr/"
}

View File

@@ -25,4 +25,5 @@ package() {
cd "$srcdir/PortProtonQt"
python -m installer --destdir="$pkgdir" dist/*.whl
cp -r build-aux/share "$pkgdir/usr/"
cp -r build-aux/lib "$pkgdir/usr/"
}

View File

@@ -22,6 +22,7 @@ BuildRequires: python3-build
BuildRequires: pyproject-rpm-macros
BuildRequires: python3dist(setuptools)
BuildRequires: git
BuildRequires: systemd-rpm-macros
%description
%{summary}
@@ -69,11 +70,13 @@ cd %{oname}
%pyproject_install
%pyproject_save_files %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
%{_bindir}/%{pypi_name}
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
%{_udevrulesdir}/60-portprotonqt.rules
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%{bash_completions_dir}/portprotonqt

View File

@@ -19,6 +19,7 @@ BuildRequires: python3-build
BuildRequires: pyproject-rpm-macros
BuildRequires: python3dist(setuptools)
BuildRequires: git
BuildRequires: systemd-rpm-macros
%description
%{summary}
@@ -68,11 +69,13 @@ cd %{oname}
%pyproject_install
%pyproject_save_files %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name} -f %{pyproject_files}
%{_bindir}/%{pypi_name}
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
%{_udevrulesdir}/60-portprotonqt.rules
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%{bash_completions_dir}/portprotonqt

View File

@@ -0,0 +1 @@
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"

View File

@@ -1021,7 +1021,7 @@
},
{
"normalized_name": "farlight 84",
"status": "Supported"
"status": "Denied"
},
{
"normalized_name": "riders republic",
@@ -1436,8 +1436,8 @@
"status": "Broken"
},
{
"normalized_name": "blue protocol",
"status": "Broken"
"normalized_name": "blue protocol star resonance",
"status": "Running"
},
{
"normalized_name": "dark and darker",

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,4 +1,108 @@
[
{
"normalized_title": "split/second",
"slug": "split-second"
},
{
"normalized_title": "warzone 2100",
"slug": "warzone-2100"
},
{
"normalized_title": "foundation",
"slug": "foundation"
},
{
"normalized_title": "земский собор [демо]",
"slug": "zemskij-sobor-demo"
},
{
"normalized_title": "crusader kings 3",
"slug": "crusader-kings-3"
},
{
"normalized_title": "nadir a grimdark deck builder",
"slug": "nadir-a-grimdark-deck-builder"
},
{
"normalized_title": "oriental empires",
"slug": "oriental-empires"
},
{
"normalized_title": "vampire the masquerade bloodlines 2",
"slug": "vampire-the-masquerade-bloodlines-2"
},
{
"normalized_title": "escape from duckov",
"slug": "escape-from-duckov"
},
{
"normalized_title": "xiii",
"slug": "xiii"
},
{
"normalized_title": "saints row 2",
"slug": "saints-row-2"
},
{
"normalized_title": "frozenheim",
"slug": "frozenheim"
},
{
"normalized_title": "saints row (2022)",
"slug": "saints-row-2022"
},
{
"normalized_title": "iron harvest",
"slug": "iron-harvest"
},
{
"normalized_title": "tom clancy's splinter cell blacklist",
"slug": "tom-clancys-splinter-cell-blacklist"
},
{
"normalized_title": "painkiller overdose",
"slug": "painkiller-overdose"
},
{
"normalized_title": "ancestors legacy",
"slug": "ancestors-legacy"
},
{
"normalized_title": "bye sweet carole",
"slug": "bye-sweet-carole"
},
{
"normalized_title": "painkiller black",
"slug": "painkiller-black-edition"
},
{
"normalized_title": "hogwarts legacy",
"slug": "hogwarts-legacy"
},
{
"normalized_title": "active matter",
"slug": "active-matter"
},
{
"normalized_title": "tom clancy's splinter cell",
"slug": "tom-clancys-splinter-cell"
},
{
"normalized_title": "sniper ghost warrior",
"slug": "sniper-ghost-warrior"
},
{
"normalized_title": "fate undiscovered realms",
"slug": "fate-undiscovered-realms"
},
{
"normalized_title": "dying light the beast deluxe",
"slug": "dying-light-the-beast-deluxe-edition"
},
{
"normalized_title": "spellforce platinum",
"slug": "spellforce-platinum-edition"
},
{
"normalized_title": "dirt rally 2.0 game of the year",
"slug": "dirt-rally-2-0-game-of-the-year-edition"
@@ -271,10 +375,6 @@
"normalized_title": "steins;gate the distant valhalla",
"slug": "steins-gate-the-distant-valhalla"
},
{
"normalized_title": "hogwarts legacy",
"slug": "hogwarts-legacy"
},
{
"normalized_title": "osu!",
"slug": "osu"

Binary file not shown.

View File

@@ -17,17 +17,31 @@ import json
class PySide6DependencyAnalyzer:
def __init__(self):
def __init__(self, project_root: Path = None):
# Системные библиотеки, которые нужно всегда оставлять
self.system_libs = {
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
'libQt6Svg'
}
self.critical_modules = {
'QtSvg',
}
self.real_dependencies = {}
self.used_modules_code = set()
self.used_modules_ldd = set()
self.all_required_modules = set()
# Определяем корень проекта
if project_root is None:
# Корень проекта - две директории выше от скрипта
self.project_root = Path(__file__).parent.parent
else:
self.project_root = project_root
self.venv_path = self.project_root / ".venv"
self.build_path = self.project_root / "build-aux"
def find_python_files(self, directory: Path) -> List[Path]:
"""Находит все Python файлы в директории"""
@@ -44,24 +58,61 @@ class PySide6DependencyAnalyzer:
"""Находит все PySide6 библиотеки (.so файлы)"""
libs = {}
# Поиск в единственной локации
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
print(f"Поиск PySide6 библиотек в: {search_path}")
# Ищем venv в корне проекта
venv_candidates = [
self.venv_path, # .venv
self.project_root / "venv",
self.project_root / ".virtualenv",
]
if search_path.exists():
# Ищем .so файлы модулей
for so_file in search_path.glob("Qt*.*.so"):
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
if module_name.startswith('Qt'):
libs[module_name] = so_file
pyside6_path = None
# Также ищем в подпапках
for subdir in search_path.iterdir():
if subdir.is_dir() and subdir.name.startswith('Qt'):
for so_file in subdir.glob("*.so*"):
if 'Qt' in so_file.name:
libs[subdir.name] = so_file
break
# Пробуем найти PySide6 в venv
for venv in venv_candidates:
if venv.exists():
# Ищем Python версию
lib_path = venv / "lib"
if lib_path.exists():
for python_dir in lib_path.iterdir():
if python_dir.name.startswith('python'):
candidate = python_dir / "site-packages" / "PySide6"
if candidate.exists():
pyside6_path = candidate
print(f"Найден PySide6 в: {candidate}")
break
if pyside6_path:
break
if not pyside6_path:
print(f"Предупреждение: PySide6 не найден в venv, проверяем AppDir...")
# Если не нашли в venv, пробуем в AppDir
if base_path:
appdir_candidate = base_path / "AppDir/usr/local/lib"
if appdir_candidate.exists():
for python_dir in appdir_candidate.iterdir():
if python_dir.name.startswith('python'):
candidate = python_dir / "dist-packages" / "PySide6"
if candidate.exists():
pyside6_path = candidate
print(f"Найден PySide6 в AppDir: {candidate}")
break
if not pyside6_path:
return libs
# Ищем .so файлы модулей
for so_file in pyside6_path.glob("Qt*.*.so"):
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
if module_name.startswith('Qt'):
libs[module_name] = so_file
# Также ищем в подпапках
for subdir in pyside6_path.iterdir():
if subdir.is_dir() and subdir.name.startswith('Qt'):
for so_file in subdir.glob("*.so*"):
if 'Qt' in so_file.name:
libs[subdir.name] = so_file
break
return libs
@@ -257,8 +308,10 @@ class PySide6DependencyAnalyzer:
# Модули для удаления
if removable_modules:
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
if removable_filtered:
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_filtered)])
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
# Генерируем команду для удаления нативных библиотек с сохранением нужных
required_libs = set()
@@ -276,39 +329,82 @@ class PySide6DependencyAnalyzer:
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
])
# Заменяем блок очистки в рецепте
import re
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
# Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
# Паттерн: после " - cp -r lib AppDir/usr\n" идут команды rm, а затем "AppDir:"
pattern = r'( - cp -r lib AppDir/usr\n)((?: - (?:rm|shopt).*\n)*?)(?=AppDir:)'
new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines)
match = re.search(pattern, recipe_content)
updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL)
if not match:
print("ПРЕДУПРЕЖДЕНИЕ: Не удалось найти блок очистки в рецепте")
print("Добавляем команды очистки перед блоком AppDir:")
# Просто вставим команды перед AppDir:
appdir_pos = recipe_content.find('AppDir:')
if appdir_pos != -1:
new_content = (
recipe_content[:appdir_pos] +
'\n'.join(cleanup_lines) + '\n' +
recipe_content[appdir_pos:]
)
return new_content
else:
print("ОШИБКА: Не найден блок AppDir: в рецепте")
return ""
# Создаем замену - группа 1 (cp -r lib) + новые команды очистки
replacement = r'\1' + '\n'.join(cleanup_lines) + '\n'
updated_recipe = re.sub(pattern, replacement, recipe_content, count=1)
return updated_recipe
def main():
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
parser.add_argument('project_path', help='Путь к проекту для анализа')
parser.add_argument('project_path', nargs='?', default='.',
help='Путь к проекту для анализа (по умолчанию: текущая директория)')
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')
args = parser.parse_args()
project_path = Path(args.project_path)
project_path = Path(args.project_path).resolve()
if not project_path.exists():
print(f"Ошибка: путь {project_path} не существует")
sys.exit(1)
appdir_path = Path(args.appdir) if args.appdir else None
appdir_path = Path(args.appdir).resolve() if args.appdir else None
if appdir_path and not appdir_path.exists():
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
appdir_path = None
analyzer = PySide6DependencyAnalyzer()
# Определяем корень проекта
# Если запущен из подпапки проекта, ищем корень
project_root = project_path
if (project_path / ".git").exists() or (project_path / "pyproject.toml").exists():
project_root = project_path
else:
# Пытаемся найти корень проекта
current = project_path
while current != current.parent:
if (current / ".git").exists() or (current / "pyproject.toml").exists():
project_root = current
break
current = current.parent
print(f"Корень проекта: {project_root}")
analyzer = PySide6DependencyAnalyzer(project_root=project_root)
# Если указан custom venv путь
if args.venv:
analyzer.venv_path = Path(args.venv).resolve()
print(f"Использую указанный venv: {analyzer.venv_path}")
results = analyzer.analyze_project(project_path, appdir_path)
# Сохраняем в анализатор для генерации команд
@@ -347,13 +443,13 @@ def main():
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
if args.verbose and results['real_dependencies']:
Devlin(f"\nРеальные зависимости (ldd):")
print(f"\nРеальные зависимости (ldd):")
for module, deps in results['real_dependencies'].items():
if deps:
print(f" {module}{', '.join(deps)}")
# Обновляем AppImage рецепт
recipe_path = Path("../build-aux/AppImageBuilder.yml")
recipe_path = analyzer.build_path / "AppImageBuilder.yml"
if recipe_path.exists():
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
if updated_recipe:

View File

@@ -21,9 +21,9 @@ Current translation status:
| Locale | Progress | Translated |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 249 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 249 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 249 of 249 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 323 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 323 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 323 of 323 |
---

View File

@@ -21,9 +21,9 @@
| Локаль | Прогресс | Переведено |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 249 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 249 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 249 из 249 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 323 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 323 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 323 из 323 |
---

View File

@@ -1,11 +1,17 @@
import sys
import os
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.QtGui import QIcon
from PySide6.QtNetwork import QLocalServer, QLocalSocket
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.cli import parse_args
@@ -16,25 +22,24 @@ __app_version__ = "0.1.8"
def get_version():
try:
commit = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD'],
stderr=subprocess.DEVNULL
).decode('utf-8').strip()
["git", "rev-parse", "--short", "HEAD"],
stderr=subprocess.DEVNULL,
).decode("utf-8").strip()
return f"{__app_version__} ({commit})"
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return __app_version__
def main():
os.environ['PW_CLI'] = '1'
os.environ['PROCESS_LOG'] = '1'
os.environ['START_FROM_STEAM'] = '1'
os.environ["PW_CLI"] = "1"
os.environ["PROCESS_LOG"] = "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
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh')
subprocess.run([script_path, 'cli', '--initial'])
subprocess.run(start_sh + ["cli", "--initial"])
app = QApplication(sys.argv)
app.setWindowIcon(QIcon.fromTheme(__app_id__))
@@ -43,41 +48,116 @@ def main():
app.setApplicationVersion(__app_version__)
args = parse_args()
# Setup logger with specified debug level
setup_logger(args.debug_level)
# Reinitialize logger after setup to ensure it uses the new configuration
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()
qt_translator = QTranslator()
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
app.installTranslator(qt_translator)
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()
window = MainWindow(app_name=__app_name__, version=version)
if args.fullscreen:
logger.info("Launching in fullscreen mode due to --fullscreen flag")
# --- Handle incoming connections ---
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)
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():
nonlocal window
app.aboutToQuit.disconnect()
if window:
window.close()
app.quit()
try:
local_server.close()
QLocalServer.removeServer(server_name)
if window:
window.close()
except Exception as e:
logger.warning(f"Cleanup error: {e}")
app.aboutToQuit.connect(cleanup_on_exit)
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,11 +1,17 @@
import os
import configparser
import shutil
import subprocess
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
_portproton_location = None
_portproton_start_sh = None
# Configuration cache for performance optimization
_config_cache = {}
_config_last_modified = {}
# Paths to configuration files
CONFIG_FILE = os.path.join(
@@ -26,13 +32,35 @@ THEMES_DIRS = [
]
def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails."""
cp = configparser.ConfigParser()
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails.
Uses caching to avoid repeated file reads for better performance.
"""
# Check if file exists
if not os.path.exists(config_file):
logger.debug(f"Configuration file {config_file} not found")
return None
# Get file modification time
try:
current_mtime = os.path.getmtime(config_file)
except OSError:
logger.warning(f"Failed to get modification time for {config_file}")
return None
# Check if we have a cached version that's still valid
if config_file in _config_cache and config_file in _config_last_modified:
if _config_last_modified[config_file] == current_mtime:
logger.debug(f"Using cached config for {config_file}")
return _config_cache[config_file]
# Read and parse the config file
cp = configparser.ConfigParser()
try:
cp.read(config_file, encoding="utf-8")
# Update cache
_config_cache[config_file] = cp
_config_last_modified[config_file] = current_mtime
logger.debug(f"Config file {config_file} loaded and cached")
return cp
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
logger.warning(f"Invalid configuration file format: {e}")
@@ -41,6 +69,14 @@ def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
logger.warning(f"Failed to read configuration file: {e}")
return None
def invalidate_config_cache(config_file: str = CONFIG_FILE):
"""Invalidates the cached configuration for the specified file."""
if config_file in _config_cache:
del _config_cache[config_file]
if config_file in _config_last_modified:
del _config_last_modified[config_file]
logger.debug(f"Config cache invalidated for {config_file}")
def read_config():
"""Reads the configuration file and returns a dictionary of parameters.
Example line in config (no sections):
@@ -75,6 +111,8 @@ def save_theme_to_config(theme_name):
cp["Appearance"]["theme"] = theme_name
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_time_config():
"""Reads time settings from the [Time] section of the configuration file.
@@ -94,6 +132,8 @@ def save_time_config(detail_level):
cp["Time"]["detail_level"] = detail_level
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_file_content(file_path):
"""Reads the content of a file and returns it as a string."""
@@ -101,14 +141,14 @@ def read_file_content(file_path):
return f.read().strip()
def get_portproton_location():
"""Returns the path to the PortProton directory.
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
If the path is invalid, uses the default directory.
"""
"""Возвращает путь к PortProton каталогу (строку) или None."""
global _portproton_location
if _portproton_location is not None:
return _portproton_location
location = None
if os.path.isfile(PORTPROTON_CONFIG_FILE):
try:
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
@@ -116,19 +156,46 @@ def get_portproton_location():
_portproton_location = location
logger.info(f"PortProton path from configuration: {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:
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")
if os.path.isdir(default_dir):
_portproton_location = default_dir
logger.info(f"Using flatpak PortProton directory: {default_dir}")
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
if os.path.isdir(default_flatpak_dir):
_portproton_location = default_flatpak_dir
logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}")
return _portproton_location
logger.warning("PortProton configuration and flatpak directory not found")
logger.warning("PortProton configuration and Flatpak directory not found")
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):
"""Reads and parses a .desktop file using configparser.
Returns None if the [Desktop Entry] section is missing.
@@ -176,6 +243,8 @@ def save_card_size(card_width):
cp["Cards"]["card_width"] = str(card_width)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_auto_card_size():
"""Reads the card size (width) for Auto Install from the [Cards] section.
@@ -195,6 +264,8 @@ def save_auto_card_size(card_width):
cp["Cards"]["auto_card_width"] = str(card_width)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_sort_method():
@@ -215,6 +286,8 @@ def save_sort_method(sort_method):
cp["Games"]["sort_method"] = sort_method
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_display_filter():
"""Reads the display_filter parameter from the [Games] section.
@@ -234,6 +307,8 @@ def save_display_filter(filter_value):
cp["Games"]["display_filter"] = filter_value
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_favorites():
"""Reads the list of favorite games from the [Favorites] section.
@@ -259,6 +334,8 @@ def save_favorites(favorites):
cp["Favorites"]["games"] = f'"{fav_str}"'
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_rumble_config():
"""Reads the gamepad rumble setting from the [Gamepad] section.
@@ -278,6 +355,8 @@ def save_rumble_config(rumble_enabled):
cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_gamepad_type():
"""Reads the gamepad type from the [Gamepad] section.
@@ -297,6 +376,8 @@ def save_gamepad_type(gpad_type):
cp["Gamepad"]["type"] = gpad_type
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def ensure_default_proxy_config():
"""Ensures the [Proxy] section exists in the configuration file.
@@ -341,6 +422,8 @@ def save_proxy_config(proxy_url="", proxy_user="", proxy_password=""):
cp["Proxy"]["proxy_password"] = proxy_password
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_fullscreen_config():
"""Reads the fullscreen mode setting from the [Display] section.
@@ -360,6 +443,8 @@ def save_fullscreen_config(fullscreen):
cp["Display"]["fullscreen"] = str(fullscreen)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_window_geometry() -> tuple[int, int]:
"""Reads the window width and height from the [MainWindow] section.
@@ -381,6 +466,8 @@ def save_window_geometry(width: int, height: int):
cp["MainWindow"]["height"] = str(height)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def reset_config():
"""Resets the configuration file by deleting it.
@@ -390,6 +477,8 @@ def reset_config():
try:
os.remove(CONFIG_FILE)
logger.info("Configuration file %s deleted", CONFIG_FILE)
# Invalidate cache after deletion
invalidate_config_cache()
except Exception as e:
logger.warning(f"Failed to delete configuration file: {e}")
@@ -404,6 +493,9 @@ def clear_cache():
except Exception as e:
logger.warning(f"Failed to delete cache: {e}")
# Also clear our internal config cache
invalidate_config_cache()
def read_auto_fullscreen_gamepad():
"""Reads the auto-fullscreen setting for gamepad from the [Display] section.
Returns False if the parameter is missing.
@@ -422,6 +514,8 @@ def save_auto_fullscreen_gamepad(auto_fullscreen):
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_favorite_folders():
"""Reads the list of favorite folders from the [FavoritesFolders] section.
@@ -447,6 +541,8 @@ def save_favorite_folders(folders):
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_minimize_to_tray():
"""Reads the minimize-to-tray setting from the [Display] section.
@@ -466,3 +562,5 @@ def save_minimize_to_tray(minimize_to_tray):
cp["Display"]["minimize_to_tray"] = str(minimize_to_tray)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()

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.QtGui import QDesktopServices, QIcon, QKeySequence
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.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
@@ -406,16 +406,7 @@ class ContextMenuManager:
)
return
# Construct EGS launch command
wrapper = "flatpak run ru.linux_gaming.PortProton"
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
wrapper = get_portproton_start_command()
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
else:
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)

View File

@@ -2,12 +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, QTextCursor, QColor
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
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget, QComboBox, QLineEdit
)
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
from icoextract import IconExtractor, IconExtractorError
from PIL import Image
@@ -19,6 +18,7 @@ from portprotonqt.custom_widgets import AutoSizeButton
from portprotonqt.downloader import Downloader
from portprotonqt.virtual_keyboard import VirtualKeyboard
from portprotonqt.preloader import Preloader
from portprotonqt.settings_manager import get_toggle_settings, get_advanced_settings, ADVANCED_SETTING_KEYS
import psutil
if TYPE_CHECKING:
@@ -1326,7 +1326,9 @@ class WinetricksDialog(QDialog):
# DLLs tab
self.dll_table = QTableWidget()
self.dll_table.setAlternatingRowColors(True)
self.dll_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.dll_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.dll_table.setColumnCount(3)
@@ -1357,7 +1359,9 @@ class WinetricksDialog(QDialog):
# Fonts tab
self.fonts_table = QTableWidget()
self.fonts_table.setAlternatingRowColors(True)
self.fonts_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.fonts_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.fonts_table.setColumnCount(3)
@@ -1388,7 +1392,9 @@ class WinetricksDialog(QDialog):
# Settings tab
self.settings_table = QTableWidget()
self.settings_table.setAlternatingRowColors(True)
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.settings_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.settings_table.setColumnCount(3)
@@ -1674,3 +1680,417 @@ class WinetricksDialog(QDialog):
if self.input_manager:
self.input_manager.disable_winetricks_mode()
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:
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.dist_options = []
self.prefix_options = []
if self.portproton_path:
dist_dir = os.path.join(self.portproton_path, 'dist')
if os.path.exists(dist_dir):
self.dist_options = [f for f in os.listdir(dist_dir) if os.path.isdir(os.path.join(dist_dir, f))]
prefixes_dir = os.path.join(self.portproton_path, 'prefixes')
if os.path.exists(prefixes_dir):
self.prefix_options = [f for f in os.listdir(prefixes_dir) if os.path.isdir(os.path.join(prefixes_dir, f))]
self.current_settings = {}
self.value_widgets = {}
self.original_values = {}
self.advanced_widgets = {}
self.original_display_values = {}
self.available_keys = set()
self.blocked_keys = set()
self.numa_nodes = {}
self.is_amd = False
self.locale_options = []
self.logical_core_options = []
self.amd_vulkan_drivers = []
self.setWindowTitle(_("Exe Settings"))
self.setModal(True)
self.resize(1100, 720)
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
# Load toggle settings from config module
self.toggle_settings = get_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()
# Load current settings (includes list-db)
self.load_current_settings()
def _get_process_args(self, subcommand_args):
"""Get the full arguments for QProcess.start, handling flatpak separator."""
if self.start_sh[0] == "flatpak":
return self.start_sh[1:] + ["--"] + subcommand_args
else:
return self.start_sh + subcommand_args
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)
# Tab widget
self.tab_widget = QTabWidget()
self.tab_widget.setStyleSheet(self.theme.WINETRICKS_TAB_STYLE)
self.main_tab = QWidget()
self.main_tab_layout = QVBoxLayout(self.main_tab)
self.advanced_tab = QWidget()
self.advanced_tab_layout = QVBoxLayout(self.advanced_tab)
self.tab_widget.addTab(self.main_tab, _("Main"))
self.tab_widget.addTab(self.advanced_tab, _("Advanced"))
# Main settings table
self.settings_table = QTableWidget()
self.settings_table.setAlternatingRowColors(True)
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
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_tab_layout.addWidget(self.settings_table)
# Advanced settings table
self.advanced_table = QTableWidget()
self.advanced_table.setAlternatingRowColors(True)
self.advanced_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.advanced_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.advanced_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
# self.advanced_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.advanced_table.setColumnCount(3)
self.advanced_table.setHorizontalHeaderLabels([_("Setting"), _("Value"), _("Description")])
self.advanced_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
self.advanced_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
self.advanced_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.advanced_table.horizontalHeader().resizeSection(1, 200)
self.advanced_table.setWordWrap(True)
self.advanced_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.advanced_table.setTextElideMode(Qt.TextElideMode.ElideNone)
self.advanced_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
self.advanced_tab_layout.addWidget(self.advanced_table)
self.main_layout.addWidget(self.tab_widget)
# Buttons
button_layout = QHBoxLayout()
self.apply_button = AutoSizeButton(_("Apply"), icon=ThemeManager().get_icon("apply"))
self.cancel_button = AutoSizeButton(_("Cancel"), icon=ThemeManager().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[0], ["cli", "--list-db"])
def on_list_db_finished(self, exit_code, exit_status):
"""Handle --list-db output and extract available keys and system info."""
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')
lines = output.splitlines()
self.numa_nodes = {}
self.is_amd = False
self.logical_core_options = []
self.locale_options = []
self.amd_vulkan_drivers = []
for line in lines:
line_stripped = line.strip()
if not line_stripped:
continue
if re.match(r'^[A-Z_0-9]+=[^=]+$', line_stripped) and not line_stripped.startswith('PW_'):
# System info
k, v = line_stripped.split('=', 1)
if k.startswith('NUMA_NODE_'):
node_id = k[10:]
self.numa_nodes[node_id] = v
elif k == 'IS_AMD':
self.is_amd = v.lower() == 'true'
elif k == 'LOGICAL_CORE_OPTIONS':
self.logical_core_options = v.split('!') if v else []
elif k == 'LOCALE_LIST':
self.locale_options = v.split('!') if v else []
elif k == 'AMD_VULKAN_DRIVER_LIST':
self.amd_vulkan_drivers = v.split('!') if v else []
continue
if line_stripped.startswith('PW_'):
parts = line_stripped.split(maxsplit=1)
key = parts[0]
self.available_keys.add(key)
if len(parts) > 1 and 'blocked' in parts[1]:
self.blocked_keys.add(key)
# Show only intersection
self.available_keys &= set(self.toggle_settings.keys())
# Load current settings
process = QProcess(self)
process.finished.connect(self.on_show_ppdb_finished)
process.start(self.start_sh[0], ["cli", "--show-ppdb", f"{self.exe_path}.ppdb"])
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:
# Fallback to defaults if load fails
for key in self.toggle_settings:
self.current_settings[key] = '0'
for adv_key in ADVANCED_SETTING_KEYS:
self.current_settings[adv_key] = 'disabled' if 'TOPOLOGY' in adv_key or 'SELECT' in adv_key or 'MODE' in adv_key or 'LEVEL' in adv_key or 'GL_VERSION' in adv_key or 'NUMA' in adv_key else ''
else:
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore').strip()
self.current_settings = {}
for line in output.split('\n'):
line_stripped = line.strip()
if '=' in line_stripped:
try:
key, val = line_stripped.split('=', 1)
if key in self.toggle_settings or key in ADVANCED_SETTING_KEYS:
self.current_settings[key] = val
except ValueError:
continue
# Force blocked settings to '0'
for key in self.blocked_keys:
self.current_settings[key] = '0'
self.original_values = self.current_settings.copy()
for key in set(self.toggle_settings.keys()):
self.original_values.setdefault(key, '0')
self.populate_table()
self.populate_advanced()
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.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.settings_table.resizeRowsToContents()
if self.settings_table.rowCount() > 0:
self.settings_table.setCurrentCell(0, 0)
self.settings_table.setFocus(Qt.FocusReason.OtherFocusReason)
def populate_advanced(self):
"""Populate the advanced tab with table format."""
self.advanced_table.setRowCount(0)
self.advanced_widgets.clear()
self.original_display_values = {}
self.advanced_table.verticalHeader().setVisible(False)
current = self.current_settings
disabled_text = _('disabled')
# Get advanced settings from config module
advanced_settings = get_advanced_settings(
disabled_text=disabled_text,
logical_core_options=self.logical_core_options,
locale_options=self.locale_options,
amd_vulkan_drivers=self.amd_vulkan_drivers,
is_amd=self.is_amd,
numa_nodes=self.numa_nodes,
dist_options=self.dist_options,
prefix_options=self.prefix_options
)
# Populate table
for setting in advanced_settings:
row = self.advanced_table.rowCount()
self.advanced_table.insertRow(row)
# Name column
name_item = QTableWidgetItem(setting['name'])
name_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
self.advanced_table.setItem(row, 0, name_item)
# Value column (widget)
if setting['type'] == 'combo':
combo = QComboBox()
combo.addItems(setting['options'])
# Get current value
current_raw = current.get(setting['key'], setting['default'])
if setting['key'] == 'PW_WINE_CPU_TOPOLOGY':
current_val = disabled_text if current_raw == 'disabled' else (current_raw.split(':')[0] if isinstance(current_raw, str) and ':' in current_raw else current_raw)
elif setting['key'] == 'PW_AMD_VULKAN_USE':
current_val = disabled_text if not current_raw or current_raw == '' else current_raw
else:
current_val = disabled_text if current_raw == 'disabled' else current_raw
if current_val not in setting['options']:
combo.addItem(current_val)
combo.setCurrentText(current_val)
# Block if only disabled option
if len(setting['options']) == 1:
combo.setEnabled(False)
self.advanced_table.setCellWidget(row, 1, combo)
self.advanced_widgets[setting['key']] = combo
self.original_display_values[setting['key']] = current_val
elif setting['type'] == 'text':
line_edit = QLineEdit()
current_val = current.get(setting['key'], setting['default'])
line_edit.setText(current_val)
self.advanced_table.setCellWidget(row, 1, line_edit)
self.advanced_widgets[setting['key']] = line_edit
self.original_display_values[setting['key']] = current_val
# Description column
desc_item = QTableWidgetItem(setting['description'])
desc_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
desc_item.setToolTip(setting['description'])
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
self.advanced_table.setItem(row, 2, desc_item)
def apply_changes(self):
"""Apply changes by collecting diffs from both main and advanced tabs."""
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}")
for key, widget in self.advanced_widgets.items():
orig_val = self.original_display_values.get(key, '')
if isinstance(widget, QComboBox):
new_val = widget.currentText()
if new_val.lower() == _('disabled').lower():
new_val = 'disabled'
elif isinstance(widget, QLineEdit):
new_val = widget.text().strip()
else:
continue
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[0], 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.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.config_utils import get_portproton_location
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
from portprotonqt.steam_api import (
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
@@ -254,14 +254,7 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba
return
# Determine wrapper
wrapper = "flatpak run ru.linux_gaming.PortProton"
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
wrapper = get_portproton_start_command()
# Create launch script
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.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 collections.abc import Callable
from portprotonqt.image_utils import load_pixmap_async, round_corners
@@ -102,7 +102,7 @@ class GameCard(QFrame):
self.favoriteLabel = ClickableLabel(self.coverWidget)
self.favoriteLabel.clicked.connect(self.toggle_favorite)
self.is_favorite = self.name in read_favorites()
self.is_favorite = self.name in set(read_favorites())
self.update_favorite_icon()
self.favoriteLabel.raise_()
@@ -203,7 +203,7 @@ class GameCard(QFrame):
self.update_cover_pixmap()
def update_cover_pixmap(self):
if self.base_pixmap:
if self.base_pixmap and not self.base_pixmap.isNull():
scaled_width = int(self.base_card_width * self._scale)
scaled_pixmap = self.base_pixmap.scaled(scaled_width, int(scaled_width * 1.5), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
@@ -404,14 +404,22 @@ class GameCard(QFrame):
self.favoriteLabel.setText("")
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):
favorites = read_favorites()
favorites_set = set(favorites)
if self.is_favorite:
if self.name in favorites:
if self.name in favorites_set:
favorites.remove(self.name)
self.is_favorite = False
else:
if self.name not in favorites:
if self.name not in favorites_set:
favorites.append(self.name)
self.is_favorite = True
save_favorites(favorites)

View File

@@ -267,8 +267,9 @@ class GameLibraryManager:
return (fav_order, -game[10] if game[10] else 0, -game[11] if game[11] else 0)
# Quick partition: Sort favorites and non-favorites separately, then merge
fav_games = [g for g in games_list if g[0] in favorites]
non_fav_games = [g for g in games_list if g[0] not in favorites]
favorites_set = set(favorites) # Convert to set for O(1) lookup
fav_games = [g for g in games_list if g[0] in favorites_set]
non_fav_games = [g for g in games_list if g[0] not in favorites_set]
sorted_fav = sorted(fav_games, key=partition_sort_key)
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
sorted_games = sorted_fav + sorted_non_fav

View File

@@ -36,11 +36,22 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
current_theme_name = read_theme_from_config()
def finish_with(pixmap: QPixmap):
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
x = (scaled.width() - width) // 2
y = (scaled.height() - height) // 2
cropped = scaled.copy(x, y, width, height)
callback(cropped)
# Check if pixmap is valid before attempting to scale it
if pixmap.isNull():
# Create a default placeholder pixmap instead of trying to scale a null pixmap
placeholder_pixmap = QPixmap(width, height)
placeholder_pixmap.fill(QColor("#333333"))
painter = QPainter(placeholder_pixmap)
painter.setPen(QPen(QColor("white")))
painter.drawText(placeholder_pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
painter.end()
callback(placeholder_pixmap)
else:
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
x = (scaled.width() - width) // 2
y = (scaled.height() - height) // 2
cropped = scaled.copy(x, y, width, height)
callback(cropped)
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images")
@@ -58,6 +69,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
local_path = os.path.join(image_folder, f"{appid}.jpg")
if os.path.exists(local_path):
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
@@ -69,6 +83,8 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else:
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333"))
@@ -83,11 +99,19 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
except Exception as e:
logger.error(f"Ошибка обработки URL {cover}: {e}")
if cover and cover.startswith(("http://", "https://")):
# SteamGridDB (SGDB)
if cover and cover.startswith("https://cdn2.steamgriddb.com"):
try:
local_path = os.path.join(image_folder, f"{app_name}.jpg")
parts = cover.split("/")
filename = parts[-1] if parts else "sgdb_cover.png"
# SGDB ссылки содержат уникальный хеш в названии — используем как имя
local_path = os.path.join(image_folder, filename)
if os.path.exists(local_path):
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
@@ -99,6 +123,45 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else:
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333"))
painter = QPainter(pixmap)
painter.setPen(QPen(QColor("white")))
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
painter.end()
finish_with(pixmap)
logger.info("Downloading SGDB cover for %s -> %s", app_name or "unknown", filename)
downloader.download_async(cover, local_path, timeout=5, callback=on_downloaded)
return
except Exception as e:
logger.error(f"Ошибка обработки SGDB URL {cover}: {e}")
if cover and cover.startswith(("http://", "https://")):
try:
local_path = os.path.join(image_folder, f"{app_name}.jpg")
if os.path.exists(local_path):
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
def on_downloaded(result: str | None):
pixmap = QPixmap()
if result and os.path.exists(result):
pixmap.load(result)
if pixmap.isNull():
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else:
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333"))
@@ -115,6 +178,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
if cover and QFile.exists(cover):
pixmap = QPixmap(cover)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {cover}")
finish_with(pixmap)
return
@@ -122,6 +188,8 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
pixmap = QPixmap()
if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else:
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333"))
@@ -141,7 +209,15 @@ def round_corners(pixmap, radius):
"""
if pixmap.isNull():
return pixmap
# Check if radius is valid to prevent issues
if radius <= 0:
return pixmap
size = pixmap.size()
if size.width() <= 0 or size.height() <= 0:
return pixmap
rounded = QPixmap(size)
rounded.fill(QColor(0, 0, 0, 0))
painter = QPainter(rounded)
@@ -244,20 +320,31 @@ class FullscreenDialog(QDialog):
QApplication.processEvents()
pixmap, caption = self.images[self.current_index]
# Учитываем devicePixelRatio для масштабирования высокого качества
device_pixel_ratio = get_device_pixel_ratio()
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
# Check if pixmap is valid before attempting to scale it
if pixmap.isNull():
# Create a default placeholder pixmap instead of trying to scale a null pixmap
placeholder_pixmap = QPixmap(self.FIXED_WIDTH - 80, self.FIXED_HEIGHT)
placeholder_pixmap.fill(QColor("#333333"))
painter = QPainter(placeholder_pixmap)
painter.setPen(QPen(QColor("white")))
painter.drawText(placeholder_pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
painter.end()
self.imageLabel.setPixmap(placeholder_pixmap)
else:
# Учитываем devicePixelRatio для масштабирования высокого качества
device_pixel_ratio = get_device_pixel_ratio()
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
# Масштабируем изображение из оригинального pixmap
scaled_pixmap = pixmap.scaled(
target_width,
target_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
self.imageLabel.setPixmap(scaled_pixmap)
# Масштабируем изображение из оригинального pixmap
scaled_pixmap = pixmap.scaled(
target_width,
target_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
self.imageLabel.setPixmap(scaled_pixmap)
self.captionLabel.setText(caption)
self.setWindowTitle(caption)

View File

@@ -1,8 +1,9 @@
import time
import threading
import os
import math
from typing import Protocol, cast
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
from evdev import InputDevice, InputEvent, UInput, ecodes, list_devices, ff
from enum import Enum
from pyudev import Context, Monitor, Device, Devices
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QTableWidgetItem
@@ -15,6 +16,7 @@ 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, read_gamepad_type
from portprotonqt.dialogs import AddGameDialog
from portprotonqt.virtual_keyboard import VirtualKeyboard
import select
logger = get_logger(__name__)
@@ -115,6 +117,34 @@ class InputManager(QObject):
self.last_trigger_time = 0.0
self.trigger_cooldown = 0.2
# Mouse emulation attributes
self.mouse_emulation_enabled = True # Enable by default as crutch for external apps
self.ui = None # UInput for virtual mouse
self.stick_x_raw = 0
self.stick_y_raw = 0
self.deadzone = 8000 # Deadzone for sticks
self.max_value = 32767 # Max stick value
self.sensitivity = 8.0 # Cursor sensitivity
self.scroll_accumulator = 0.0
self.scroll_sensitivity = 0.15 # Scroll sensitivity
self.scroll_threshold = 0.2 # Scroll threshold
self.last_update = time.time()
self.update_interval = 0.016 # ~60 FPS
self.emulation_active = False # Flag for external focus (updated in main thread)
self.emulation_triggered = False
self.start_held = False
self.guide_held = False
# Focus check timer for emulation flag (runs in main thread)
self.focus_check_timer = QTimer(self)
self.focus_check_timer.timeout.connect(self._update_emulation_flag)
self.focus_check_timer.start(100) # Check every 100ms
logger.info("EMUL: Mouse emulation initialized (enabled=%s)", self.mouse_emulation_enabled)
if self.mouse_emulation_enabled:
self.enable_mouse_emulation()
# FileExplorer specific attributes
self.file_explorer = None
self.original_button_handler = None
@@ -151,6 +181,13 @@ class InputManager(QObject):
# Initialize evdev + hotplug
self.init_gamepad()
def _update_emulation_flag(self):
"""Update emulation_active flag based on Qt app focus (main thread only)."""
active = QApplication.activeWindow()
self.emulation_active = (active is None) # True for external windows (e.g., winefile)
if not self.emulation_active:
self.emulation_triggered = False
def _navigate_game_cards(self, container, tab_index: int, code: int, value: int) -> None:
"""Common navigation logic for game cards in a container."""
if container is None:
@@ -637,6 +674,116 @@ class InputManager(QObject):
except Exception as e:
logger.error(f"Error in navigation repeat: {e}")
def enable_mouse_emulation(self):
"""Enable mouse emulation mode (creates virtual mouse device)."""
if self.mouse_emulation_enabled and self.ui is not None:
logger.debug("EMUL: Mouse emulation already enabled, skipping")
return
try:
logger.info("EMUL: Attempting to create UInput virtual mouse...")
if not os.path.exists('/dev/uinput'):
logger.error("EMUL: /dev/uinput does not exist")
self.mouse_emulation_enabled = False
return
if not os.access('/dev/uinput', os.W_OK):
logger.error("EMUL: No write access to /dev/uinput")
self.mouse_emulation_enabled = False
return
self.ui = UInput({
ecodes.EV_KEY: [ecodes.BTN_LEFT, ecodes.BTN_RIGHT],
ecodes.EV_REL: [ecodes.REL_X, ecodes.REL_Y, ecodes.REL_WHEEL],
}, name="Virtual DPad Mouse")
self.mouse_emulation_enabled = True
logger.info("EMUL: Virtual mouse created successfully")
except PermissionError as e:
logger.error("EMUL: Permission denied for /dev/uinput: %s", e)
self.mouse_emulation_enabled = False
except Exception as ex:
logger.error(f"EMUL: Error creating virtual mouse: {ex}", exc_info=True)
self.mouse_emulation_enabled = False
def disable_mouse_emulation(self):
"""Disable mouse emulation mode (closes virtual mouse device)."""
logger.info("EMUL: Disabling mouse emulation...")
if self.ui:
try:
self.ui.close()
logger.info("EMUL: Virtual mouse closed")
except Exception as e:
logger.error("EMUL: Error closing virtual mouse: %s", e)
self.ui = None
self.mouse_emulation_enabled = False
self.stick_x_raw = 0
self.stick_y_raw = 0
self.scroll_accumulator = 0.0
def handle_scroll(self, raw_value):
"""Обработка прокрутки с правого стика Y"""
if not self.mouse_emulation_enabled or not self.emulation_active or not self.ui:
return
if abs(raw_value) < self.deadzone:
self.scroll_accumulator = 0.0
return
normalized = raw_value / self.max_value
self.scroll_accumulator += normalized * self.scroll_sensitivity
while abs(self.scroll_accumulator) >= self.scroll_threshold:
scroll_step = 1 if self.scroll_accumulator > 0 else -1
self.scroll_wheel(-scroll_step)
self.scroll_accumulator -= scroll_step * self.scroll_threshold
def update_mouse_position(self):
"""Постоянное обновление позиции мыши на основе состояния стика"""
if not self.ui or not self.emulation_active:
return
x = self.stick_x_raw
y = self.stick_y_raw
magnitude = math.sqrt(x * x + y * y)
if magnitude < self.deadzone:
return
norm_x = x / magnitude
norm_y = y / magnitude
adjusted_magnitude = max(0.0, min(1.0, (magnitude - self.deadzone) / (self.max_value - self.deadzone)))
adjusted_magnitude = math.pow(adjusted_magnitude, 1.5)
speed = adjusted_magnitude * self.sensitivity
dx = int(norm_x * speed)
dy = int(norm_y * speed)
if dx != 0 or dy != 0:
self.move_mouse(dx, dy)
def move_mouse(self, dx, dy):
"""Сдвиг системного курсора"""
if self.ui:
self.ui.write(ecodes.EV_REL, ecodes.REL_X, dx)
self.ui.write(ecodes.EV_REL, ecodes.REL_Y, dy)
self.ui.syn()
def scroll_wheel(self, steps):
"""Прокрутка колеса мыши"""
if self.ui:
self.ui.write(ecodes.EV_REL, ecodes.REL_WHEEL, steps)
self.ui.syn()
def click_left(self):
"""Клик левой кнопкой мыши"""
if self.ui:
self.ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 1)
self.ui.syn()
self.ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 0)
self.ui.syn()
def click_right(self):
"""Клик правой кнопкой мыши"""
if self.ui:
self.ui.write(ecodes.EV_KEY, ecodes.BTN_RIGHT, 1)
self.ui.syn()
self.ui.write(ecodes.EV_KEY, ecodes.BTN_RIGHT, 0)
self.ui.syn()
@Slot(bool)
def handle_fullscreen_slot(self, enable: bool) -> None:
try:
@@ -1473,7 +1620,6 @@ class InputManager(QObject):
logger.error(f"Failed to start udev monitor: {e}")
return
import select
fd = monitor.fileno()
poller = select.poll()
poller.register(fd, select.POLLIN)
@@ -1692,60 +1838,117 @@ class InputManager(QObject):
logger.error(f"Error finding gamepad: {e}", exc_info=True)
return None
def monitor_gamepad(self) -> None:
try:
if not self.gamepad:
return
for event in self.gamepad.read_loop():
if not self.running:
break
if event.type not in (ecodes.EV_KEY, ecodes.EV_ABS):
continue
now = time.time()
while self.running:
current_time = time.time()
# Проверка фокуса: игнорируем события, если окно не в фокусе
app = QApplication.instance()
active = QApplication.activeWindow()
if not app or not active:
continue
if self.gamepad:
try:
# Non-blocking read with short timeout
events = []
r, w, x = select.select([self.gamepad.fd], [], [], 0.001)
if r:
events = list(self.gamepad.read())
if event.type == ecodes.EV_KEY:
# Emit on both press (1) and release (0)
self.button_event.emit(event.code, event.value)
# Special handling for menu on press only
if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
self.toggle_fullscreen.emit(not self._is_fullscreen)
elif event.type == ecodes.EV_ABS:
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
# Проверяем, достаточно ли времени прошло с последнего срабатывания
if now - self.last_trigger_time < self.trigger_cooldown:
continue
if event.code == ecodes.ABS_Z: # LT/L2
if event.value > 128 and not self.lt_pressed:
self.lt_pressed = True
self.button_event.emit(event.code, 1) # Emit as press
self.last_trigger_time = now
elif event.value <= 128 and self.lt_pressed:
self.lt_pressed = False
self.button_event.emit(event.code, 0) # Emit as release
elif event.code == ecodes.ABS_RZ: # RT/R2
if event.value > 128 and not self.rt_pressed:
self.rt_pressed = True
self.button_event.emit(event.code, 1) # Emit as press
self.last_trigger_time = now
elif event.value <= 128 and self.rt_pressed:
self.rt_pressed = False
self.button_event.emit(event.code, 0) # Emit as release
else:
self.dpad_moved.emit(event.code, event.value, now)
except OSError as e:
if e.errno == 19: # ENODEV: No such device
logger.info("Gamepad disconnected during event loop")
else:
logger.error(f"OSError in gamepad monitoring: {e}", exc_info=True)
# Process events
for event in events:
if not self.running:
break
# UI signal handling (always, for internal app)
if event.type == ecodes.EV_KEY:
if event.code == ecodes.BTN_START:
self.start_held = (event.value == 1)
if event.code in BUTTONS['guide']:
self.guide_held = (event.value == 1)
if event.value == 1:
if ((event.code in BUTTONS['guide'] and self.start_held) or
(event.code == ecodes.BTN_START and self.guide_held)):
self.emulation_triggered = not self.emulation_triggered
self.button_event.emit(event.code, event.value)
# Special handling for menu on press only
if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
self.toggle_fullscreen.emit(not self._is_fullscreen)
elif event.type == ecodes.EV_ABS:
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
# Trigger handling for UI
if current_time - self.last_trigger_time < self.trigger_cooldown:
continue
if event.code == ecodes.ABS_Z: # LT/L2
if event.value > 128 and not self.lt_pressed:
self.lt_pressed = True
self.button_event.emit(event.code, 1)
self.last_trigger_time = current_time
elif event.value <= 128 and self.lt_pressed:
self.lt_pressed = False
self.button_event.emit(event.code, 0)
elif event.code == ecodes.ABS_RZ: # RT/R2
if event.value > 128 and not self.rt_pressed:
self.rt_pressed = True
self.button_event.emit(event.code, 1)
self.last_trigger_time = current_time
elif event.value <= 128 and self.rt_pressed:
self.rt_pressed = False
self.button_event.emit(event.code, 0)
else:
self.dpad_moved.emit(event.code, event.value, current_time)
# Mouse emulation (only for external windows + triggered)
if self.mouse_emulation_enabled and self.emulation_active and self.emulation_triggered:
if event.type == ecodes.EV_ABS:
if event.code == ecodes.ABS_HAT0X:
if event.value == -1:
self.move_mouse(-10, 0)
elif event.value == 1:
self.move_mouse(10, 0)
elif event.code == ecodes.ABS_HAT0Y:
if event.value == -1:
self.move_mouse(0, -10)
elif event.value == 1:
self.move_mouse(0, 10)
elif event.code == ecodes.ABS_X:
self.stick_x_raw = event.value
elif event.code == ecodes.ABS_Y:
self.stick_y_raw = event.value
elif event.code == ecodes.ABS_RY:
self.handle_scroll(event.value)
elif event.type == ecodes.EV_KEY:
if event.code in (ecodes.BTN_SOUTH, ecodes.BTN_A) and event.value == 1:
self.click_left()
elif event.code in (ecodes.BTN_EAST, ecodes.BTN_B) and event.value == 1:
self.click_right()
# Periodic mouse position update
if current_time - self.last_update >= self.update_interval:
self.update_mouse_position()
self.last_update = current_time
except OSError as e:
if e.errno == 19: # ENODEV
logger.info("Gamepad disconnected during monitoring")
else:
logger.error(f"IOError in gamepad monitoring: {e}")
self.gamepad = None
self.stick_x_raw = 0
self.stick_y_raw = 0
self.scroll_accumulator = 0.0
self.start_held = False
self.guide_held = False
self.emulation_triggered = False
break
except Exception as ex:
logger.error(f"Unexpected error in gamepad monitoring: {ex}")
break
else:
time.sleep(0.1)
if not self.running:
break
except Exception as e:
logger.error(f"Error in gamepad monitoring: {e}", exc_info=True)
logger.error(f"Error in gamepad monitoring thread: {e}", exc_info=True)
finally:
if self.gamepad:
try:
@@ -1754,12 +1957,21 @@ class InputManager(QObject):
except Exception:
pass
self.gamepad = None
self.start_held = False
self.guide_held = False
self.emulation_triggered = False
def cleanup(self) -> None:
"""
Корректное завершение работы с геймпадом и udev монитором.
"""
try:
# Mouse emulation cleanup
self.disable_mouse_emulation()
# Stop focus check timer
self.focus_check_timer.stop()
# Флаг для остановки udev monitor loop
self.running = False

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-10-16 14:54+0500\n"
"POT-Creation-Date: 2025-11-11 17:00+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n"
@@ -76,10 +76,6 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -124,6 +120,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""
@@ -365,6 +365,39 @@ msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Exe Settings"
msgstr ""
msgid "Main"
msgstr ""
msgid "Advanced"
msgstr ""
msgid "Setting"
msgstr ""
msgid "Value"
msgstr ""
msgid "Description"
msgstr ""
msgid "disabled"
msgstr ""
msgid "Info"
msgstr ""
msgid "No changes to apply."
msgstr ""
msgid "Failed to apply changes. Check logs."
msgstr ""
msgid "Settings updated successfully."
msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
@@ -512,14 +545,21 @@ msgstr ""
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' cleared successfully."
msgid "Clearing prefix..."
msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr ""
#, python-brace-format
msgid ""
"Prefix '{}' cleared with errors:\n"
"{}"
msgid "Prefix clear failed with exit code {}."
msgstr ""
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr ""
msgid "Failed to start backup process."
@@ -704,6 +744,10 @@ msgstr ""
msgid "Error applying theme '{0}'"
msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH"
msgstr ""
@@ -762,6 +806,232 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid ""
"Using FPS and system load monitoring (Turns on and off by the key "
"combination - right Shift + F12)"
msgstr ""
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable vkBasalt by default to improve graphics in games running on "
"Vulkan. (The HOME hotkey disables vkbasalt)"
msgstr ""
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
msgstr ""
msgid ""
"Super + F : Toggle fullscreen\n"
"Super + N : Toggle nearest neighbour filtering\n"
"Super + U : Toggle FSR upscaling\n"
"Super + Y : Toggle NIS upscaling\n"
"Super + I : Increase FSR sharpness by 1\n"
"Super + O : Decrease FSR sharpness by 1\n"
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
"Super + G : Toggle keyboard grab\n"
"Super + C : Update clipboard"
msgstr ""
msgid "Enable in-process synchronization primitives based on eventfd."
msgstr ""
msgid "Enable futex-based in-process synchronization primitives."
msgstr ""
msgid "Enable in-process synchronization via the Linux ntsync driver."
msgstr ""
msgid "Enable vkd3d support - Ray Tracing"
msgstr ""
msgid "Enable DLSS on supported NVIDIA graphics cards"
msgstr ""
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
msgstr ""
msgid "Enable Lossless Scaling frame generation (experimental)"
msgstr ""
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
msgstr ""
msgid "Disguise all NVIDIA GPU features"
msgstr ""
msgid "Run the application in WINE virtual desktop"
msgstr ""
msgid "Run the application in a terminal"
msgstr ""
msgid "Disable startup mode and WINE version selector window"
msgstr ""
msgid "Use system GameMode for performance optimization"
msgstr ""
msgid "Enable forced use of third-party DirectX libraries"
msgstr ""
msgid "Fix pink-tinted video playback in some games"
msgstr ""
msgid "Reduce PulseAudio latency to fix intermittent sound"
msgstr ""
msgid "Force US keyboard layout"
msgstr ""
msgid "Use GStreamer for in-game clips (WMF support)"
msgstr ""
msgid "Use WINE shader caching"
msgstr ""
msgid "Force use of built-in DXGI library"
msgstr ""
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
msgstr ""
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
msgstr ""
msgid "Enable OBS Studio capture via obs-vkcapture"
msgstr ""
msgid "Disable desktop compositing for performance"
msgstr ""
msgid "Use container launch mode (recommended default)"
msgstr ""
msgid "Force DirectInput protocol instead of XInput"
msgstr ""
msgid "Enable experimental native Wayland support"
msgstr ""
msgid "Enable HDR settings under native Wayland"
msgstr ""
msgid "Use Gallium Zink (OpenGL via Vulkan)"
msgstr ""
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
msgstr ""
msgid "Use WineD3D Vulkan backend (Damavand)"
msgstr ""
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
msgstr ""
msgid "Use async dxvk-sarek (experimental)"
msgstr ""
msgid "Windows version"
msgstr ""
msgid ""
"Changing the WINDOWS emulation version may be required to run older "
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
msgstr ""
msgid "DLL Overrides"
msgstr ""
msgid ""
"Forced to use/disable the library only for the given application.\n"
"\n"
"A brief instruction:\n"
"* libraries are written WITHOUT the .dll file extension\n"
"* libraries are separated by semicolons - ;\n"
"* library=n - use the WINDOWS (third-party) library\n"
"* library=b - use WINE (built-in) library\n"
"* library=n,b - use WINDOWS library and then WINE\n"
"* library=b,n - use WINE library and then WINDOWS\n"
"* library= - disable the use of this library\n"
"\n"
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgstr ""
msgid "Launch Arguments"
msgstr ""
msgid ""
"Adding an argument after the .exe file, just like you would add an "
"argument in a shortcut on a WINDOWS system.\n"
"\n"
"Example: -dx11 -skipintro 1"
msgstr ""
msgid "CPU Cores Limit"
msgstr ""
msgid ""
"Limiting the number of CPU cores is useful for Unity games (It is "
"recommended to set the value equal to 8)"
msgstr ""
msgid "OpenGL Version"
msgstr ""
msgid ""
"You can select the required OpenGL version, some games require a forced "
"Compatibility Profile (COMP)."
msgstr ""
msgid "VKD3D Feature Level"
msgstr ""
msgid "You can set a forced feature level VKD3D for games on DirectX12"
msgstr ""
msgid "Locale"
msgstr ""
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
msgstr ""
msgid "Window Mode"
msgstr ""
msgid ""
"Window mode (for Vulkan and OpenGL):\n"
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
"immediate - Unlimited frame rate + tearing.\n"
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
" rate."
msgstr ""
msgid "AMD Vulkan Driver"
msgstr ""
msgid ""
"Select needed AMD vulkan implementation. Choosing which implementation of"
" vulkan will be used to run the game"
msgstr ""
msgid "NUMA Node"
msgstr ""
msgid ""
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
" single node reduces memory-access latency and limits costly core-to-core"
" switches."
msgstr ""
msgid "Reboot"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-10-16 14:54+0500\n"
"POT-Creation-Date: 2025-11-11 17:00+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n"
@@ -76,10 +76,6 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -124,6 +120,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""
@@ -365,6 +365,39 @@ msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Exe Settings"
msgstr ""
msgid "Main"
msgstr ""
msgid "Advanced"
msgstr ""
msgid "Setting"
msgstr ""
msgid "Value"
msgstr ""
msgid "Description"
msgstr ""
msgid "disabled"
msgstr ""
msgid "Info"
msgstr ""
msgid "No changes to apply."
msgstr ""
msgid "Failed to apply changes. Check logs."
msgstr ""
msgid "Settings updated successfully."
msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
@@ -512,14 +545,21 @@ msgstr ""
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' cleared successfully."
msgid "Clearing prefix..."
msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr ""
#, python-brace-format
msgid ""
"Prefix '{}' cleared with errors:\n"
"{}"
msgid "Prefix clear failed with exit code {}."
msgstr ""
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr ""
msgid "Failed to start backup process."
@@ -704,6 +744,10 @@ msgstr ""
msgid "Error applying theme '{0}'"
msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH"
msgstr ""
@@ -762,6 +806,232 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid ""
"Using FPS and system load monitoring (Turns on and off by the key "
"combination - right Shift + F12)"
msgstr ""
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable vkBasalt by default to improve graphics in games running on "
"Vulkan. (The HOME hotkey disables vkbasalt)"
msgstr ""
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
msgstr ""
msgid ""
"Super + F : Toggle fullscreen\n"
"Super + N : Toggle nearest neighbour filtering\n"
"Super + U : Toggle FSR upscaling\n"
"Super + Y : Toggle NIS upscaling\n"
"Super + I : Increase FSR sharpness by 1\n"
"Super + O : Decrease FSR sharpness by 1\n"
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
"Super + G : Toggle keyboard grab\n"
"Super + C : Update clipboard"
msgstr ""
msgid "Enable in-process synchronization primitives based on eventfd."
msgstr ""
msgid "Enable futex-based in-process synchronization primitives."
msgstr ""
msgid "Enable in-process synchronization via the Linux ntsync driver."
msgstr ""
msgid "Enable vkd3d support - Ray Tracing"
msgstr ""
msgid "Enable DLSS on supported NVIDIA graphics cards"
msgstr ""
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
msgstr ""
msgid "Enable Lossless Scaling frame generation (experimental)"
msgstr ""
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
msgstr ""
msgid "Disguise all NVIDIA GPU features"
msgstr ""
msgid "Run the application in WINE virtual desktop"
msgstr ""
msgid "Run the application in a terminal"
msgstr ""
msgid "Disable startup mode and WINE version selector window"
msgstr ""
msgid "Use system GameMode for performance optimization"
msgstr ""
msgid "Enable forced use of third-party DirectX libraries"
msgstr ""
msgid "Fix pink-tinted video playback in some games"
msgstr ""
msgid "Reduce PulseAudio latency to fix intermittent sound"
msgstr ""
msgid "Force US keyboard layout"
msgstr ""
msgid "Use GStreamer for in-game clips (WMF support)"
msgstr ""
msgid "Use WINE shader caching"
msgstr ""
msgid "Force use of built-in DXGI library"
msgstr ""
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
msgstr ""
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
msgstr ""
msgid "Enable OBS Studio capture via obs-vkcapture"
msgstr ""
msgid "Disable desktop compositing for performance"
msgstr ""
msgid "Use container launch mode (recommended default)"
msgstr ""
msgid "Force DirectInput protocol instead of XInput"
msgstr ""
msgid "Enable experimental native Wayland support"
msgstr ""
msgid "Enable HDR settings under native Wayland"
msgstr ""
msgid "Use Gallium Zink (OpenGL via Vulkan)"
msgstr ""
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
msgstr ""
msgid "Use WineD3D Vulkan backend (Damavand)"
msgstr ""
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
msgstr ""
msgid "Use async dxvk-sarek (experimental)"
msgstr ""
msgid "Windows version"
msgstr ""
msgid ""
"Changing the WINDOWS emulation version may be required to run older "
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
msgstr ""
msgid "DLL Overrides"
msgstr ""
msgid ""
"Forced to use/disable the library only for the given application.\n"
"\n"
"A brief instruction:\n"
"* libraries are written WITHOUT the .dll file extension\n"
"* libraries are separated by semicolons - ;\n"
"* library=n - use the WINDOWS (third-party) library\n"
"* library=b - use WINE (built-in) library\n"
"* library=n,b - use WINDOWS library and then WINE\n"
"* library=b,n - use WINE library and then WINDOWS\n"
"* library= - disable the use of this library\n"
"\n"
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgstr ""
msgid "Launch Arguments"
msgstr ""
msgid ""
"Adding an argument after the .exe file, just like you would add an "
"argument in a shortcut on a WINDOWS system.\n"
"\n"
"Example: -dx11 -skipintro 1"
msgstr ""
msgid "CPU Cores Limit"
msgstr ""
msgid ""
"Limiting the number of CPU cores is useful for Unity games (It is "
"recommended to set the value equal to 8)"
msgstr ""
msgid "OpenGL Version"
msgstr ""
msgid ""
"You can select the required OpenGL version, some games require a forced "
"Compatibility Profile (COMP)."
msgstr ""
msgid "VKD3D Feature Level"
msgstr ""
msgid "You can set a forced feature level VKD3D for games on DirectX12"
msgstr ""
msgid "Locale"
msgstr ""
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
msgstr ""
msgid "Window Mode"
msgstr ""
msgid ""
"Window mode (for Vulkan and OpenGL):\n"
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
"immediate - Unlimited frame rate + tearing.\n"
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
" rate."
msgstr ""
msgid "AMD Vulkan Driver"
msgstr ""
msgid ""
"Select needed AMD vulkan implementation. Choosing which implementation of"
" vulkan will be used to run the game"
msgstr ""
msgid "NUMA Node"
msgstr ""
msgid ""
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
" single node reduces memory-access latency and limits costly core-to-core"
" switches."
msgstr ""
msgid "Reboot"
msgstr ""

View File

@@ -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-16 14:54+0500\n"
"POT-Creation-Date: 2025-11-11 17:00+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"
@@ -74,10 +74,6 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -122,6 +118,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""
@@ -363,6 +363,39 @@ msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Exe Settings"
msgstr ""
msgid "Main"
msgstr ""
msgid "Advanced"
msgstr ""
msgid "Setting"
msgstr ""
msgid "Value"
msgstr ""
msgid "Description"
msgstr ""
msgid "disabled"
msgstr ""
msgid "Info"
msgstr ""
msgid "No changes to apply."
msgstr ""
msgid "Failed to apply changes. Check logs."
msgstr ""
msgid "Settings updated successfully."
msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
@@ -510,14 +543,21 @@ msgstr ""
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' cleared successfully."
msgid "Clearing prefix..."
msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr ""
#, python-brace-format
msgid ""
"Prefix '{}' cleared with errors:\n"
"{}"
msgid "Prefix clear failed with exit code {}."
msgstr ""
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr ""
msgid "Failed to start backup process."
@@ -702,6 +742,10 @@ msgstr ""
msgid "Error applying theme '{0}'"
msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH"
msgstr ""
@@ -760,6 +804,232 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid ""
"Using FPS and system load monitoring (Turns on and off by the key "
"combination - right Shift + F12)"
msgstr ""
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable vkBasalt by default to improve graphics in games running on "
"Vulkan. (The HOME hotkey disables vkbasalt)"
msgstr ""
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
msgstr ""
msgid ""
"Super + F : Toggle fullscreen\n"
"Super + N : Toggle nearest neighbour filtering\n"
"Super + U : Toggle FSR upscaling\n"
"Super + Y : Toggle NIS upscaling\n"
"Super + I : Increase FSR sharpness by 1\n"
"Super + O : Decrease FSR sharpness by 1\n"
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
"Super + G : Toggle keyboard grab\n"
"Super + C : Update clipboard"
msgstr ""
msgid "Enable in-process synchronization primitives based on eventfd."
msgstr ""
msgid "Enable futex-based in-process synchronization primitives."
msgstr ""
msgid "Enable in-process synchronization via the Linux ntsync driver."
msgstr ""
msgid "Enable vkd3d support - Ray Tracing"
msgstr ""
msgid "Enable DLSS on supported NVIDIA graphics cards"
msgstr ""
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
msgstr ""
msgid "Enable Lossless Scaling frame generation (experimental)"
msgstr ""
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
msgstr ""
msgid "Disguise all NVIDIA GPU features"
msgstr ""
msgid "Run the application in WINE virtual desktop"
msgstr ""
msgid "Run the application in a terminal"
msgstr ""
msgid "Disable startup mode and WINE version selector window"
msgstr ""
msgid "Use system GameMode for performance optimization"
msgstr ""
msgid "Enable forced use of third-party DirectX libraries"
msgstr ""
msgid "Fix pink-tinted video playback in some games"
msgstr ""
msgid "Reduce PulseAudio latency to fix intermittent sound"
msgstr ""
msgid "Force US keyboard layout"
msgstr ""
msgid "Use GStreamer for in-game clips (WMF support)"
msgstr ""
msgid "Use WINE shader caching"
msgstr ""
msgid "Force use of built-in DXGI library"
msgstr ""
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
msgstr ""
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
msgstr ""
msgid "Enable OBS Studio capture via obs-vkcapture"
msgstr ""
msgid "Disable desktop compositing for performance"
msgstr ""
msgid "Use container launch mode (recommended default)"
msgstr ""
msgid "Force DirectInput protocol instead of XInput"
msgstr ""
msgid "Enable experimental native Wayland support"
msgstr ""
msgid "Enable HDR settings under native Wayland"
msgstr ""
msgid "Use Gallium Zink (OpenGL via Vulkan)"
msgstr ""
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
msgstr ""
msgid "Use WineD3D Vulkan backend (Damavand)"
msgstr ""
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
msgstr ""
msgid "Use async dxvk-sarek (experimental)"
msgstr ""
msgid "Windows version"
msgstr ""
msgid ""
"Changing the WINDOWS emulation version may be required to run older "
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
msgstr ""
msgid "DLL Overrides"
msgstr ""
msgid ""
"Forced to use/disable the library only for the given application.\n"
"\n"
"A brief instruction:\n"
"* libraries are written WITHOUT the .dll file extension\n"
"* libraries are separated by semicolons - ;\n"
"* library=n - use the WINDOWS (third-party) library\n"
"* library=b - use WINE (built-in) library\n"
"* library=n,b - use WINDOWS library and then WINE\n"
"* library=b,n - use WINE library and then WINDOWS\n"
"* library= - disable the use of this library\n"
"\n"
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgstr ""
msgid "Launch Arguments"
msgstr ""
msgid ""
"Adding an argument after the .exe file, just like you would add an "
"argument in a shortcut on a WINDOWS system.\n"
"\n"
"Example: -dx11 -skipintro 1"
msgstr ""
msgid "CPU Cores Limit"
msgstr ""
msgid ""
"Limiting the number of CPU cores is useful for Unity games (It is "
"recommended to set the value equal to 8)"
msgstr ""
msgid "OpenGL Version"
msgstr ""
msgid ""
"You can select the required OpenGL version, some games require a forced "
"Compatibility Profile (COMP)."
msgstr ""
msgid "VKD3D Feature Level"
msgstr ""
msgid "You can set a forced feature level VKD3D for games on DirectX12"
msgstr ""
msgid "Locale"
msgstr ""
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
msgstr ""
msgid "Window Mode"
msgstr ""
msgid ""
"Window mode (for Vulkan and OpenGL):\n"
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
"immediate - Unlimited frame rate + tearing.\n"
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
" rate."
msgstr ""
msgid "AMD Vulkan Driver"
msgstr ""
msgid ""
"Select needed AMD vulkan implementation. Choosing which implementation of"
" vulkan will be used to run the game"
msgstr ""
msgid "NUMA Node"
msgstr ""
msgid ""
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
" single node reduces memory-access latency and limits costly core-to-core"
" switches."
msgstr ""
msgid "Reboot"
msgstr ""

View File

@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-10-16 14:54+0500\n"
"PO-Revision-Date: 2025-10-16 14:54+0500\n"
"POT-Creation-Date: 2025-11-11 17:00+0500\n"
"PO-Revision-Date: 2025-11-11 17:00+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
@@ -77,10 +77,6 @@ msgstr "Остановлен(а) '{game_name}'"
msgid "Legendary executable not found at {path}"
msgstr "Legendary не найден по пути {path}"
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr "start.sh не найден по адресу {path}"
msgid "Success"
msgstr "Успешно"
@@ -127,6 +123,10 @@ msgstr "'{game_name}' был(а) добавлен(а) в избранное"
msgid "Removed '{game_name}' from favorites"
msgstr "'{game_name}' был(а) удалён(а) из избранного"
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr "start.sh не найден по адресу {path}"
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr "Запустить игру \"{name}\" с помощью PortProton"
@@ -372,6 +372,39 @@ msgstr "Установка не удалась. Проверьте журнал
msgid "Components installed successfully."
msgstr "Компоненты успешно установлены."
msgid "Exe Settings"
msgstr "Настройки EXE"
msgid "Main"
msgstr "Основные"
msgid "Advanced"
msgstr "Расширенные"
msgid "Setting"
msgstr "Параметр"
msgid "Value"
msgstr "Значение"
msgid "Description"
msgstr "Описание"
msgid "disabled"
msgstr "отключено"
msgid "Info"
msgstr "Информация"
msgid "No changes to apply."
msgstr "Изменений для применения нет."
msgid "Failed to apply changes. Check logs."
msgstr "Не удалось применить изменения. Проверьте логи."
msgid "Settings updated successfully."
msgstr "Настройки успешно обновлены."
msgid "Loading Epic Games Store games..."
msgstr "Загрузка игр из Epic Games Store..."
@@ -519,17 +552,22 @@ msgstr "Подтвердите очистку"
msgid "Are you sure you want to clear prefix '{}'?"
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
#, python-brace-format
msgid "Prefix '{}' cleared successfully."
msgstr "Префикс '{}' успешно удален."
msgid "Clearing prefix..."
msgstr "Очистка префикса..."
msgid "Failed to start prefix clear process."
msgstr "Не удалось запустить процесс очистки префикса."
msgid "Prefix cleared successfully."
msgstr "Префикс удален успешно."
#, python-brace-format
msgid ""
"Prefix '{}' cleared with errors:\n"
"{}"
msgstr ""
"Префикс '{}' очищен с ошибками:\n"
"{}"
msgid "Prefix clear failed with exit code {}."
msgstr "Очистка префикса завершилась с кодом завершения {}."
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr "Не удалось выполнить команду очистки префикса: {}"
msgid "Failed to start backup process."
msgstr "Не удалось запустить процесс резервного копирования."
@@ -715,6 +753,10 @@ msgstr "Тема '{0}' применена успешно"
msgid "Error applying theme '{0}'"
msgstr "Ошибка при применение темы '{0}'"
#, python-brace-format
msgid "Executable not found: {0}"
msgstr "Исполняемый файл не найден: {0}"
msgid "LAST LAUNCH"
msgstr "Последний запуск"
@@ -773,6 +815,288 @@ msgstr "Неправильный формат команды (flatpak)"
msgid "File not found: {0}"
msgstr "Файл не найден: {0}"
msgid ""
"Using FPS and system load monitoring (Turns on and off by the key "
"combination - right Shift + F12)"
msgstr ""
"Использование мониторинга FPS и нагрузки системы (включается и "
"выключается комбинацией клавиш - правая Shift + F12)"
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
msgstr "Принудительное использование системных настроек MANGOHUD (GOverlay и т.д.)"
msgid ""
"Enable vkBasalt by default to improve graphics in games running on "
"Vulkan. (The HOME hotkey disables vkbasalt)"
msgstr ""
"Включить vkBasalt по умолчанию для улучшения графики в играх на Vulkan. "
"(Горячая клавиша HOME отключает vkbasalt)"
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
msgstr "Принудительное использование системных настроек VKBASALT (GOverlay и т.д.)"
msgid ""
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
msgstr ""
"Включить dgVoodoo2. Принудительное использование всех библиотек dgVoodoo2"
" (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) на всех 3D API."
msgid ""
"Super + F : Toggle fullscreen\n"
"Super + N : Toggle nearest neighbour filtering\n"
"Super + U : Toggle FSR upscaling\n"
"Super + Y : Toggle NIS upscaling\n"
"Super + I : Increase FSR sharpness by 1\n"
"Super + O : Decrease FSR sharpness by 1\n"
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
"Super + G : Toggle keyboard grab\n"
"Super + C : Update clipboard"
msgstr ""
"Super + F: Переключить полноэкранный режим\n"
"Super + N: Переключить фильтрацию ближайшего соседа\n"
"Super + U: Переключить апскейлинг FSR\n"
"Super + Y: Переключить апскейлинг NIS\n"
"Super + I: Увеличить резкость FSR на 1\n"
"Super + O: Уменьшить резкость FSR на 1\n"
"Super + S: Сделать скриншот (сейчас сохраняется в "
"/tmp/gamescope_DATE.png)\n"
"Super + G: Переключить захват клавиатуры\n"
"Super + C: Обновить буфер обмена"
msgid "Enable in-process synchronization primitives based on eventfd."
msgstr "Включить примитивы синхронизации в процессе на основе eventfd."
msgid "Enable futex-based in-process synchronization primitives."
msgstr "Включить примитивы синхронизации в процессе на основе futex."
msgid "Enable in-process synchronization via the Linux ntsync driver."
msgstr "Включить синхронизацию в процессе через драйвер ntsync в Linux."
msgid "Enable vkd3d support - Ray Tracing"
msgstr "Включить поддержку vkd3d — трассировка лучей"
msgid "Enable DLSS on supported NVIDIA graphics cards"
msgstr "Включить DLSS на поддерживаемых видеокартах NVIDIA"
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
msgstr "Включить OptiScaler (замена апскейлера / генератора кадров)"
msgid "Enable Lossless Scaling frame generation (experimental)"
msgstr "Включить генерацию кадров Lossless Scaling (экспериментально)"
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
msgstr "Апскейлинг FSR в полноэкранном режиме с ProtonGE ниже родного разрешения"
msgid "Disguise all NVIDIA GPU features"
msgstr "Маскировать все функции GPU NVIDIA"
msgid "Run the application in WINE virtual desktop"
msgstr "Запускать приложение в виртуальном рабочем столе WINE"
msgid "Run the application in a terminal"
msgstr "Запускать приложение в терминале"
msgid "Disable startup mode and WINE version selector window"
msgstr "Отключить окно выбора режима запуска и версии WINE"
msgid "Use system GameMode for performance optimization"
msgstr "Использовать системный GameMode для оптимизации производительности"
msgid "Enable forced use of third-party DirectX libraries"
msgstr "Включить принудительное использование сторонних библиотек DirectX"
msgid "Fix pink-tinted video playback in some games"
msgstr "Исправить розовый оттенок видео в некоторых играх"
msgid "Reduce PulseAudio latency to fix intermittent sound"
msgstr "Уменьшить задержку PulseAudio для исправления прерывистого звука"
msgid "Force US keyboard layout"
msgstr "Принудительно использовать раскладку клавиатуры US"
msgid "Use GStreamer for in-game clips (WMF support)"
msgstr "Использовать GStreamer для внутриигровых клипов (поддержка WMF)"
msgid "Use WINE shader caching"
msgstr "Использовать кэширование шейдеров WINE"
msgid "Force use of built-in DXGI library"
msgstr "Принудительно использовать встроенную библиотеку DXGI"
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
msgstr "Включить среды выполнения Easy Anti-Cheat и BattlEye"
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
msgstr "Использовать системные слои Vulkan (MangoHud, vkBasalt, OBS и т.д.)"
msgid "Enable OBS Studio capture via obs-vkcapture"
msgstr "Включить захват OBS Studio через obs-vkcapture"
msgid "Disable desktop compositing for performance"
msgstr "Отключить композицию рабочего стола для производительности"
msgid "Use container launch mode (recommended default)"
msgstr "Использовать режим запуска в контейнере (рекомендуемый по умолчанию)"
msgid "Force DirectInput protocol instead of XInput"
msgstr "Принудительно использовать протокол DirectInput вместо XInput"
msgid "Enable experimental native Wayland support"
msgstr "Включить экспериментальную нативную поддержку Wayland"
msgid "Enable HDR settings under native Wayland"
msgstr "Включить настройки HDR под нативным Wayland"
msgid "Use Gallium Zink (OpenGL via Vulkan)"
msgstr "Использовать Gallium Zink (OpenGL через Vulkan)"
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
msgstr "Использовать Gallium Nine (нативный DirectX 9 для Mesa)"
msgid "Use WineD3D Vulkan backend (Damavand)"
msgstr "Использовать бэкенд Vulkan WineD3D (Damavand)"
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
msgstr "Использовать встроенные dxvk/vkd3d из Wine/Proton"
msgid "Use async dxvk-sarek (experimental)"
msgstr "Использовать асинхронный dxvk-sarek (экспериментально)"
msgid "Windows version"
msgstr "Версия Windows"
msgid ""
"Changing the WINDOWS emulation version may be required to run older "
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
msgstr ""
"Изменение версии эмуляции WINDOWS может потребоваться для запуска старых "
"игр. Версии WINDOWS ниже 10 не поддерживают новые игры с DirectX 12"
msgid "DLL Overrides"
msgstr "Переопределения DLL"
msgid ""
"Forced to use/disable the library only for the given application.\n"
"\n"
"A brief instruction:\n"
"* libraries are written WITHOUT the .dll file extension\n"
"* libraries are separated by semicolons - ;\n"
"* library=n - use the WINDOWS (third-party) library\n"
"* library=b - use WINE (built-in) library\n"
"* library=n,b - use WINDOWS library and then WINE\n"
"* library=b,n - use WINE library and then WINDOWS\n"
"* library= - disable the use of this library\n"
"\n"
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgstr ""
"Принудительное использование/отключение библиотеки только для данного "
"приложения.\n"
"\n"
"Краткая инструкция:\n"
"* библиотеки пишутся БЕЗ расширения .dll\n"
"* библиотеки разделяются точкой с запятой - ;\n"
"* library=n — использовать библиотеку WINDOWS (стороннюю)\n"
"* library=b — использовать библиотеку WINE (встроенную)\n"
"* library=n,b — использовать библиотеку WINDOWS, затем WINE\n"
"* library=b,n — использовать библиотеку WINE, затем WINDOWS\n"
"* library= — отключить использование этой библиотеки\n"
"\n"
"Пример: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgid "Launch Arguments"
msgstr "Аргументы запуска"
msgid ""
"Adding an argument after the .exe file, just like you would add an "
"argument in a shortcut on a WINDOWS system.\n"
"\n"
"Example: -dx11 -skipintro 1"
msgstr ""
"Добавление аргумента после файла .exe, как вы бы добавили аргумент в "
"ярлыке на системе WINDOWS.\n"
"\n"
"Пример: -dx11 -skipintro 1"
msgid "CPU Cores Limit"
msgstr "Ограничение ядер CPU"
msgid ""
"Limiting the number of CPU cores is useful for Unity games (It is "
"recommended to set the value equal to 8)"
msgstr ""
"Ограничение количества ядер CPU полезно для игр на Unity (рекомендуется "
"установить значение равным 8)"
msgid "OpenGL Version"
msgstr "Версия OpenGL"
msgid ""
"You can select the required OpenGL version, some games require a forced "
"Compatibility Profile (COMP)."
msgstr ""
"Вы можете выбрать требуемую версию OpenGL, некоторые игры требуют "
"принудительного профиля совместимости (COMP)."
msgid "VKD3D Feature Level"
msgstr "Уровень возможностей VKD3D"
msgid "You can set a forced feature level VKD3D for games on DirectX12"
msgstr ""
"Вы можете установить принудительный уровень возможностей VKD3D для игр на"
" DirectX12"
msgid "Locale"
msgstr "Локаль"
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
msgstr ""
"Принудительно установить определённую локаль для приложения. Исправляет "
"проблемы с кодировкой в устаревшем ПО"
msgid "Window Mode"
msgstr "Режим окна"
msgid ""
"Window mode (for Vulkan and OpenGL):\n"
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
"immediate - Unlimited frame rate + tearing.\n"
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
" rate."
msgstr ""
"Режим окна (для Vulkan и OpenGL):\n"
"fifo — Первый вошёл, первый вышел. Ограничивает частоту кадров + без "
"разрывов. (VSync)\n"
"immediate — Неограниченная частота кадров + разрывы.\n"
"mailbox — Трёхбуферная. Неограниченная частота кадров + без разрывов.\n"
"relaxed — То же, что fifo, но позволяет разрывы при частоте ниже частоты "
"обновления монитора."
msgid "AMD Vulkan Driver"
msgstr "Драйвер Vulkan AMD"
msgid ""
"Select needed AMD vulkan implementation. Choosing which implementation of"
" vulkan will be used to run the game"
msgstr ""
"Выберите нужную реализацию Vulkan AMD. Выбор, какая реализация Vulkan "
"будет использоваться для запуска игры"
msgid "NUMA Node"
msgstr "Узел NUMA"
msgid ""
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
" single node reduces memory-access latency and limits costly core-to-core"
" switches."
msgstr ""
"Узел NUMA для аффинности CPU. В многоядерных системах CPU разделены на "
"узлы NUMA, каждый со своей локальной памятью и ядрами. Привязка игры к "
"одному узлу уменьшает задержку доступа к памяти и ограничивает "
"дорогостоящие переключения между ядрами."
msgid "Reboot"
msgstr "Перезагрузить"

View File

@@ -8,7 +8,7 @@ import psutil
import re
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.animations import DetailPageAnimations
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_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,
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.howlongtobeat_api import HowLongToBeat
@@ -74,6 +74,7 @@ class MainWindow(QMainWindow):
self.target_exe = None
self.current_running_button = None
self.portproton_location = get_portproton_location()
self.start_sh = get_portproton_start_command()
self.game_library_manager = GameLibraryManager(self, self.theme, None)
@@ -458,11 +459,11 @@ class MainWindow(QMainWindow):
self.current_install_script = script_name
self.seen_progress = False
self.current_percent = 0.0
start_sh = os.path.join(self.portproton_location or "", "data", "scripts", "start.sh") if self.portproton_location else ""
if not os.path.exists(start_sh):
start_sh = self.start_sh
if not start_sh:
self.installing = False
return
cmd = [start_sh, "cli", "--autoinstall", script_name]
cmd = start_sh + ["cli", "--autoinstall", script_name]
self.install_process = QProcess(self)
self.install_process.finished.connect(self.on_install_finished)
self.install_process.errorOccurred.connect(self.on_install_error)
@@ -564,6 +565,16 @@ class MainWindow(QMainWindow):
self.game_library_manager.set_games(games)
self.progress_bar.setVisible(False)
# Clear the refresh in progress flag
if hasattr(self, '_refresh_in_progress'):
self._refresh_in_progress = False
# Re-enable the refresh button if it exists
if hasattr(self, 'refreshButton'):
self.refreshButton.setEnabled(True)
self.refreshButton.setText(_("Refresh Grid"))
self.update_status_message.emit(_("Game library refreshed"), 3000)
def open_portproton_forum_topic(self, topic_name: str):
"""Open the PortProton forum topic or search page for this game."""
result = self.portproton_api.get_forum_topic_slug(topic_name)
@@ -880,7 +891,16 @@ class MainWindow(QMainWindow):
self.addGameButton.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE)
self.addGameButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.addGameButton.clicked.connect(self.openAddGameDialog)
layout.addWidget(self.addGameButton, alignment=Qt.AlignmentFlag.AlignRight)
layout.addWidget(self.addGameButton)
# Refresh button
self.refreshButton = AutoSizeButton(_("Refresh Grid"), icon=self.theme_manager.get_icon("update"))
self.refreshButton.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE)
self.refreshButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.refreshButton.clicked.connect(self.refreshGames)
layout.addWidget(self.refreshButton)
layout.addStretch() # Add stretch to push search to the right
self.searchEdit = CustomLineEdit(self, theme=self.theme)
icon: QIcon = cast(QIcon, self.theme_manager.get_icon("search"))
@@ -900,6 +920,32 @@ class MainWindow(QMainWindow):
layout.addWidget(self.searchEdit)
return self.container, self.searchEdit
def refreshGames(self):
"""Refresh the game grid by reloading all games without restarting the application."""
# Prevent multiple refreshes at once
if hasattr(self, '_refresh_in_progress') and self._refresh_in_progress:
# If refresh is already in progress, just update the status
self.update_status_message.emit(_("A refresh is already in progress..."), 3000)
return
# Mark that a refresh is in progress
self._refresh_in_progress = True
# Clear the search field to ensure all games are shown after refresh
self.searchEdit.clear()
# Disable the refresh button during refresh to prevent multiple clicks
self.refreshButton.setEnabled(False)
self.refreshButton.setText(_("Refreshing..."))
# Show progress bar
self.progress_bar.setVisible(True)
self.progress_bar.setRange(0, 0) # Indeterminate
self.update_status_message.emit(_("Refreshing game library..."), 0)
# Reload games using the existing loadGames functionality
QTimer.singleShot(0, self.loadGames)
def on_search_text_changed(self, text: str):
"""Search text change handler with debounce."""
self.searchDebounceTimer.stop()
@@ -1424,12 +1470,10 @@ class MainWindow(QMainWindow):
prefix = self.prefixCombo.currentText()
if not wine or not prefix:
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
cmd = [start_sh, "cli", cli_arg, wine, prefix]
start_sh = self.start_sh
cmd = start_sh + ["cli", cli_arg, wine, prefix]
# Показываем прогресс-бар перед запуском
self.wine_progress_bar.setVisible(True)
@@ -1508,12 +1552,13 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
def clear_prefix(self):
"""Очистка префикса (позже удалить)."""
"""Очищает префикс"""
selected_prefix = self.prefixCombo.currentText()
selected_wine = self.wineCombo.currentText()
if not selected_prefix or not selected_wine:
return
if not self.portproton_location:
if not self.portproton_location or not self.start_sh:
return
reply = QMessageBox.question(
@@ -1526,98 +1571,35 @@ class MainWindow(QMainWindow):
if reply != QMessageBox.StandardButton.Yes:
return
prefix_dir = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
if not os.path.exists(prefix_dir):
start_sh = self.start_sh
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
success = True
errors = []
# Удаление файлов
files_to_remove = [
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))
def _on_clear_prefix_finished(self, exitCode):
self.wine_progress_bar.setVisible(False)
self.update_status_message.emit("", 0)
if exitCode == 0:
QMessageBox.information(self, _("Success"), _("Prefix cleared successfully."))
else:
error_msg = _("Prefix '{}' cleared with errors:\n{}").format(selected_prefix, "\n".join(errors[:5]))
QMessageBox.warning(self, _("Warning"), error_msg)
QMessageBox.warning(self, _("Error"), _("Prefix clear failed with exit code {}.").format(exitCode))
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):
selected_prefix = self.prefixCombo.currentText()
@@ -1629,14 +1611,12 @@ class MainWindow(QMainWindow):
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):
if not self.portproton_location or not self.start_sh:
return
start_sh = self.start_sh
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]
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."))
@@ -1649,14 +1629,12 @@ class MainWindow(QMainWindow):
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):
if not self.portproton_location or not self.start_sh:
return
start_sh = self.start_sh
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]
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."))
@@ -2326,6 +2304,14 @@ class MainWindow(QMainWindow):
def darkenColor(self, color, factor=200):
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=""):
detailPage = QWidget()
self._animations = {}
@@ -2628,8 +2614,6 @@ class MainWindow(QMainWindow):
clear_layout(hltbLayout)
has_data = False
if main_story_time is not None:
@@ -2703,6 +2687,7 @@ class MainWindow(QMainWindow):
file_to_check = entry_exec_split[0]
current_exe = os.path.basename(file_to_check) if file_to_check else None
buttons_layout = QHBoxLayout()
if self.target_exe is not None and current_exe == self.target_exe:
playButton = AutoSizeButton(_("Stop"), icon=self.theme_manager.get_icon("stop"))
else:
@@ -2711,7 +2696,17 @@ class MainWindow(QMainWindow):
playButton.setFixedSize(120, 40)
playButton.setStyleSheet(self.theme.PLAY_BUTTON_STYLE)
playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton))
detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft)
buttons_layout.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))
buttons_layout.addWidget(settings_button, alignment=Qt.AlignmentFlag.AlignLeft)
buttons_layout.addStretch()
detailsLayout.addLayout(buttons_layout)
contentFrameLayout.addWidget(detailsWidget)
mainLayout.addStretch()
@@ -2935,10 +2930,7 @@ class MainWindow(QMainWindow):
env_vars = os.environ.copy()
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
wrapper = "flatpak run ru.linux_gaming.PortProton"
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
wrapper = self.start_sh or ""
cmd = [wrapper, game_exe]
@@ -3032,13 +3024,6 @@ class MainWindow(QMainWindow):
exe_name = os.path.splitext(current_exe)[0]
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:
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)

View File

@@ -0,0 +1,208 @@
def get_toggle_settings():
"""Get predefined toggle settings with descriptions."""
from portprotonqt.localization import _
return {
'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 get_advanced_settings(disabled_text, logical_core_options, locale_options,
amd_vulkan_drivers, is_amd, numa_nodes, dist_options=None, prefix_options=None):
"""Get advanced settings configuration."""
from portprotonqt.localization import _
advanced_settings = []
if dist_options is None:
dist_options = []
if prefix_options is None:
prefix_options = []
# 1. Wine Version
advanced_settings.append({
'key': 'PW_WINE_USE',
'name': _("Wine Version"),
'description': _("Select the Wine or Proton version to use for this executable."),
'type': 'combo',
'options': dist_options,
'default': ''
})
# 2. Prefix Name
advanced_settings.append({
'key': 'PW_PREFIX_NAME',
'name': _("Prefix Name"),
'description': _("Select the Wine prefix to use."),
'type': 'combo',
'options': prefix_options,
'default': 'DEFAULT'
})
# 3. Vulkan Backend
advanced_settings.append({
'key': 'PW_VULKAN_USE',
'name': _("Vulkan Backend"),
'description': _("Select Vulkan rendering backend:\n0 - WINED3D OpenGL\n1 - DXVK-Sarek and VKD3D\n2 - Stable DXVK and VKD3D\n6 - Newest DXVK and VKD3D"),
'type': 'combo',
'options': [ '0', '1', '2', '6'],
'default': '6'
})
# 4. Windows version
advanced_settings.append({
'key': 'PW_WINDOWS_VER',
'name': _("Windows version"),
'description': _("Changing the WINDOWS emulation version may be required to run older games. WINDOWS versions below 10 do not support new games with DirectX 12"),
'type': 'combo',
'options': ['11', '10', '7', 'XP'],
'default': '10'
})
# 5. DLL Overrides
advanced_settings.append({
'key': 'WINEDLLOVERRIDES',
'name': _("DLL Overrides"),
'description': _("Forced to use/disable the library only for the given application.\n\nA brief instruction:\n* libraries are written WITHOUT the .dll file extension\n* libraries are separated by semicolons - ;\n* library=n - use the WINDOWS (third-party) library\n* library=b - use WINE (built-in) library\n* library=n,b - use WINDOWS library and then WINE\n* library=b,n - use WINE library and then WINDOWS\n* library= - disable the use of this library\n\nExample: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"),
'type': 'text',
'default': ''
})
# 6. Launch arguments
advanced_settings.append({
'key': 'LAUNCH_PARAMETERS',
'name': _("Launch Arguments"),
'description': _("Adding an argument after the .exe file, just like you would add an argument in a shortcut on a WINDOWS system.\n\nExample: -dx11 -skipintro 1"),
'type': 'text',
'default': ''
})
# 7. CPU cores limit
advanced_settings.append({
'key': 'PW_WINE_CPU_TOPOLOGY',
'name': _("CPU Cores Limit"),
'description': _("Limiting the number of CPU cores is useful for Unity games (It is recommended to set the value equal to 8)"),
'type': 'combo',
'options': [disabled_text] + logical_core_options,
'default': disabled_text
})
# 8. OpenGL version
advanced_settings.append({
'key': 'PW_MESA_GL_VERSION_OVERRIDE',
'name': _("OpenGL Version"),
'description': _("You can select the required OpenGL version, some games require a forced Compatibility Profile (COMP)."),
'type': 'combo',
'options': [disabled_text, '4.6COMPAT', '4.5COMPAT', '4.3COMPAT', '4.1COMPAT', '3.3COMPAT', '3.2COMPAT'],
'default': disabled_text
})
# 9. VKD3D feature level
advanced_settings.append({
'key': 'PW_VKD3D_FEATURE_LEVEL',
'name': _("VKD3D Feature Level"),
'description': _("You can set a forced feature level VKD3D for games on DirectX12"),
'type': 'combo',
'options': [disabled_text, '12_2', '12_1', '12_0', '11_1', '11_0'],
'default': disabled_text
})
# 10. Locale
advanced_settings.append({
'key': 'PW_LOCALE_SELECT',
'name': _("Locale"),
'description': _("Force certain locale for an app. Fixes encoding issues in legacy software"),
'type': 'combo',
'options': [disabled_text] + locale_options,
'default': disabled_text
})
# 11. Present mode
advanced_settings.append({
'key': 'PW_MESA_VK_WSI_PRESENT_MODE',
'name': _("Window Mode"),
'description': _("Window mode (for Vulkan and OpenGL):\nfifo - First in, first out. Limits the frame rate + no tearing. (VSync)\nimmediate - Unlimited frame rate + tearing.\nmailbox - Triple buffering. Unlimited frame rate + no tearing.\nrelaxed - Same as fifo but allows tearing when below the monitors refresh rate."),
'type': 'combo',
'options': [disabled_text, 'fifo', 'immediate', 'mailbox', 'relaxed'],
'default': disabled_text
})
# 12. AMD Vulkan driver
amd_options = [disabled_text] + amd_vulkan_drivers if is_amd and amd_vulkan_drivers else [disabled_text]
advanced_settings.append({
'key': 'PW_AMD_VULKAN_USE',
'name': _("AMD Vulkan Driver"),
'description': _("Select needed AMD vulkan implementation. Choosing which implementation of vulkan will be used to run the game"),
'type': 'combo',
'options': amd_options,
'default': disabled_text
})
# 13. NUMA node
numa_ids = sorted(numa_nodes.keys())
numa_options = [disabled_text] + numa_ids if len(numa_ids) > 1 else [disabled_text]
advanced_settings.append({
'key': 'PW_CPU_NUMA_NODE_INDEX',
'name': _("NUMA Node"),
'description': _("NUMA node for CPU affinity. In multi-core systems, CPUs are split into NUMA nodes, each with its own local memory and cores. Binding a game to a single node reduces memory-access latency and limits costly core-to-core switches."),
'type': 'combo',
'options': numa_options,
'default': disabled_text
})
return advanced_settings
# Keys that should be recognized as advanced settings
ADVANCED_SETTING_KEYS = [
'PW_WINE_USE',
'PW_PREFIX_NAME',
'PW_VULKAN_USE',
'PW_WINDOWS_VER',
'WINEDLLOVERRIDES',
'LAUNCH_PARAMETERS',
'PW_WINE_CPU_TOPOLOGY',
'PW_MESA_GL_VERSION_OVERRIDE',
'PW_VKD3D_FEATURE_LEVEL',
'PW_LOCALE_SELECT',
'PW_MESA_VK_WSI_PRESENT_MODE',
'PW_AMD_VULKAN_USE',
'PW_CPU_NUMA_NODE_INDEX',
]

View File

@@ -13,7 +13,7 @@ from portprotonqt.logger import get_logger
from portprotonqt.localization import get_steam_language
from portprotonqt.downloader import Downloader
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
import re
import shutil
@@ -23,6 +23,7 @@ import requests
import random
import base64
import glob
import urllib.parse
downloader = Downloader()
logger = get_logger(__name__)
@@ -411,6 +412,39 @@ def save_app_details(app_id, data):
with open(cache_file, "wb") as f:
f.write(orjson.dumps(data))
def fetch_sgdb_cover(game_name: str) -> str:
"""
Fetch a cover image URL from steamgrid.usebottles.com for the given game.
The API returns a single string (quoted URL).
"""
try:
encoded = urllib.parse.quote(game_name)
url = f"https://steamgrid.usebottles.com/api/search/{encoded}"
resp = requests.get(url, timeout=5)
if resp.status_code != 200:
logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code)
return ""
text = resp.text.strip()
# Убираем возможные кавычки вокруг строки
if text.startswith('"') and text.endswith('"'):
text = text[1:-1]
if text:
logger.info("Fetched SGDB cover for %s: %s", game_name, text)
return text
except Exception as e:
logger.warning("Failed to fetch SGDB cover for %s: %s", game_name, e)
return ""
def check_url_exists(url: str) -> bool:
"""Check whether a URL returns HTTP 200."""
try:
r = requests.head(url, timeout=3)
return r.status_code == 200
except Exception:
return False
def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]):
"""
Asynchronously fetches detailed app info from Steam API.
@@ -629,6 +663,11 @@ def get_full_steam_game_info_async(appid: int, callback: Callable[[dict], None])
title = decode_text(app_info.get("name", ""))
description = decode_text(app_info.get("short_description", ""))
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
if not check_url_exists(cover):
logger.info("Steam cover not found for %s, trying SGDB", title)
alt_cover = fetch_sgdb_cover(title)
if alt_cover:
cover = alt_cover
def on_protondb_tier(tier: str):
def on_anticheat_status(anticheat_status: str):
@@ -722,12 +761,15 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
game_name = desktop_name or exe_name.capitalize()
if not matching_app:
cover = fetch_sgdb_cover(game_name) or ""
logger.info("Using SGDB cover for non-Steam game '%s': %s", game_name, cover)
def on_anticheat_status(anticheat_status: str):
callback({
"appid": "",
"name": decode_text(game_name),
"description": "",
"cover": "",
"cover": cover,
"controller_support": "",
"protondb_tier": "",
"steam_game": "false",
@@ -758,6 +800,11 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
title = decode_text(app_info.get("name", game_name))
description = decode_text(app_info.get("short_description", ""))
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
if not check_url_exists(cover):
logger.info("Steam cover not found for %s, trying SGDB", title)
alt_cover = fetch_sgdb_cover(title)
if alt_cover:
cover = alt_cover
controller_support = app_info.get("controller_support", "")
def on_protondb_tier(tier: str):
@@ -957,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}")
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")
return (False, "PortProton directory not found")
@@ -966,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())
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):
script_content = f"""#!/usr/bin/env bash
export LD_PRELOAD=
export START_FROM_STEAM=1
"{start_sh_path}" "{exe_path}" "$@"
"{start_sh}" "{exe_path}" "$@"
"""
try:
with open(script_path, "w", encoding="utf-8") as f:

View File

@@ -5,6 +5,9 @@ from portprotonqt.logger import get_logger
from PySide6.QtGui import QIcon, QFontDatabase, QPixmap
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
# Icon caching for performance optimization
_icon_cache = {}
logger = get_logger(__name__)
# Папка, где располагаются все дополнительные темы
@@ -232,6 +235,14 @@ class ThemeManager:
а если файл не найден, то из стандартной темы.
Если as_path=True, возвращает путь к иконке вместо QIcon.
"""
# Create cache key
cache_key = f"{icon_name}_{theme_name or self.current_theme_name}_{as_path}"
# Check if we already have this icon cached
if cache_key in _icon_cache:
logger.debug(f"Using cached icon for {icon_name}")
return _icon_cache[cache_key]
icon_path = None
theme_name = theme_name or self.current_theme_name
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
@@ -279,12 +290,20 @@ class ThemeManager:
# Если иконка всё равно не найдена
if not icon_path or not os.path.exists(icon_path):
logger.error(f"Warning: icon '{icon_name}' not found")
return QIcon() if not as_path else None
result = QIcon() if not as_path else None
# Cache the result even if it's None
_icon_cache[cache_key] = result
return result
if as_path:
# Cache the path
_icon_cache[cache_key] = icon_path
return icon_path
return QIcon(icon_path)
# Create QIcon and cache it
icon = QIcon(icon_path)
_icon_cache[cache_key] = icon
return icon
def get_theme_image(self, image_name, theme_name=None):
"""

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

View File

@@ -968,9 +968,8 @@ SETTINGS_CHECKBOX_STYLE = f"""
WINETRICKS_TAB_STYLE = f"""
QTabWidget::pane {{
border: 1px solid {color_d};
background: {color_b};
border-radius: {border_radius_a};
border-top: 1px solid {color_c};
background: {color_h};
}}
QTabBar::tab {{
background: {color_c};
@@ -985,15 +984,113 @@ QTabBar::tab:selected {{
color: {color_f};
}}
QTabBar::tab:hover {{
background: {color_e};
background: {color_a};
}}
"""
WINETRICKS_TABBLE_STYLE = f"""
QTableWidget {{
QComboBox {{
background: {color_c};
border: {border_c} {color_g};
border-radius: {border_radius_a};
padding-left: 12px;
color: {color_f};
gridline-color: {color_d};
font-family: '{font_family}';
font-size: {font_size_a};
min-width: 120px;
combobox-popup: 0;
}}
QComboBox:on {{
background: {color_b};
border: {border_c} {color_a};
border-bottom-style: none;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}}
QComboBox:hover {{
border: {border_c} {color_a};
background: {color_a};
}}
/* Состояние фокуса */
QComboBox:focus {{
border: {border_c} {color_a};
background-color: {color_a};
}}
QComboBox::drop-down {{
subcontrol-origin: padding;
subcontrol-position: center right;
border-left: {border_b} rgba(255, 255, 255, 0.05);
padding: 12px;
height: 12px;
width: 12px;
}}
QComboBox::down-arrow {{
image: url({theme_manager.get_icon("down", current_theme_name, as_path=True)});
padding: 12px;
height: 12px;
width: 12px;
}}
QComboBox::down-arrow:on {{
image: url({theme_manager.get_icon("up", current_theme_name, as_path=True)});
padding: 12px;
height: 12px;
width: 12px;
}}
/* Список при открытом комбобоксе */
QComboBox QAbstractItemView {{
outline: none;
background: {color_c};
border: {border_c} {color_a};
border-top-style: none;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}}
QListView {{
background: {color_c};
}}
QListView::item {{
padding: 7px 7px 7px 12px;
margin: 3px;
border-radius: {border_radius_a};
color: {color_f};
}}
QListView::item:hover {{
background: {color_b};
}}
QListView::item:selected {{
background: {color_b};
}}
/* Выделение в списке при фокусе на элементе */
QListView::item:focus {{
background: {color_a};
color: {color_f};
}}
QLineEdit {{
background: {color_c};
border: {border_c} rgba(255, 255, 255, 0.01);
border-radius: {border_radius_a};
height: 34px;
padding-left: 12px;
color: {color_f};
font-family: '{font_family}';
font-size: {font_size_a};
}}
QLineEdit:hover {{
background: {color_c};
border: {border_c} {color_a};
}}
QLineEdit:focus {{
border: {border_c} {color_a};
background-color: {color_e};
}}
QTableWidget {{
background: {color_h};
color: {color_f};
gridline-color: {color_h};
alternate-background-color: {color_d};
border: {border_a};
border-radius: {border_radius_a};
@@ -1009,39 +1106,68 @@ QHeaderView::section {{
}}
QTableWidget::item {{
padding: 8px;
border-bottom: 1px solid {color_d};
border-bottom: {border_a } {color_c};
height: 36px;
}}
QTableWidget::item:selected {{
QTableWidget::item:selected,
QTableWidget::item:focus,
QTableWidget::item:selected:focus {{
background: {color_a};
color: {color_f};
selection-background-color: {color_a};
}}
QTableWidget::item:hover {{
background: {color_e};
background: {color_h};
}}
QTableWidget::indicator {{
width: 24px;
height: 24px;
border: {border_b} {color_a};
border: {border_c} {color_h};
border-radius: {border_radius_a};
background: rgba(255, 255, 255, 0.1);
background: {color_b};
}}
QTableWidget::indicator:unchecked {{
background: rgba(255, 255, 255, 0.1);
image: none;
}}
QTableWidget::indicator:checked {{
background: {color_a};
background: {color_b};
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
border: {border_b} {color_f};
border: {border_c} {color_a};
}}
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}
QTableWidget::indicator:focus {{
background: rgba(255, 255, 255, 0.2);
border: {border_c} {color_a};
}}
QScrollBar:vertical {{
width: 10px;
border: {border_a};
border-radius: 5px;
background: rgba(20, 20, 20, 0.30);
}}
QScrollBar::handle:vertical {{
background: #bebebe;
border: {border_a};
border-radius: 5px;
}}
QScrollBar::add-line:vertical {{
border: {border_a};
background: none;
}}
QScrollBar::sub-line:vertical {{
border: {border_a};
background: none;
}}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {{
border: {border_a};
width: 3px;
height: 3px;
background: none;
}}
"""
WINETRICKS_LOG_STYLE = f"""

View File

@@ -32,7 +32,7 @@ dependencies = [
"icoextract>=0.2.0",
"numpy>=2.2.4",
"orjson>=3.11.3",
"pillow>=11.3.0",
"pillow>=12.0.0",
"psutil>=7.1.0",
"pyside6==6.9.1",
"pyudev>=0.24.3",

View File

@@ -8,7 +8,7 @@
"enabled": true
},
"pre-commit": {
"enabled": true
"enabled": false
},
"packageRules": [
{

976
uv.lock generated

File diff suppressed because it is too large Load Diff