40 Commits

Author SHA1 Message Date
80a2c06b5a feat: added refresh button
All checks were successful
Code check / Check code (push) Successful in 1m20s
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
All checks were successful
Code check / Check code (push) Successful in 1m17s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 13:35:35 +05:00
5481cd80d7 chore: added null pixmaps check
All checks were successful
Code check / Check code (push) Successful in 1m14s
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
All checks were successful
Code check / Check code (push) Successful in 1m10s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 12:03:18 +05:00
ad3eeb6e06 chore(localization): update
All checks were successful
Code check / Check code (push) Successful in 1m7s
renovate / renovate (push) Successful in 29s
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
All checks were successful
Code check / Check code (pull_request) Successful in 1m19s
2025-11-15 16:22:59 +07:00
ea272c29b6 WINETRICKS_TABBLE_STYLE reworked
All checks were successful
Code check / Check code (pull_request) Successful in 1m14s
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
All checks were successful
Code check / Check code (push) Successful in 1m29s
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
All checks were successful
Code check / Check code (push) Successful in 1m28s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-04 11:14:17 +05:00
a448ba29b0 feat(input_manager): added mouse emulation
All checks were successful
Code check / Check code (push) Successful in 1m19s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-03 12:34:27 +05:00
06e55db54d feat(settings): update styles
All checks were successful
Code check / Check code (push) Successful in 1m13s
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
All checks were successful
Code check / Check code (pull_request) Successful in 1m35s
Code check / Check code (push) Successful in 1m38s
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
All checks were successful
Code check / Check code (push) Successful in 1m6s
Fetch Data / build (push) Successful in 1m13s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-30 16:27:45 +05:00
dec24429f5 chore: separate start.sh to new function
All checks were successful
Code check / Check code (push) Successful in 1m11s
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
Some checks failed
Code check / Check code (push) Failing after 1m35s
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
All checks were successful
Code check / Check code (push) Successful in 1m43s
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
All checks were successful
Code check / Check code (push) Successful in 1m12s
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
Some checks failed
Code check / Check code (push) Has been cancelled
Code check / Check code (pull_request) Successful in 1m17s
2025-10-26 00:01:19 +00:00
7675bc4cdc feat: added initial exe settings
All checks were successful
Code check / Check code (push) Successful in 1m31s
renovate / renovate (push) Successful in 39s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-26 00:12:00 +05:00
ffa203f019 feat: restore instance from tray
All checks were successful
Code check / Check code (push) Successful in 1m9s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-22 15:46:57 +05:00
3eed25ecee feat: update grid on update_favorite_icon
All checks were successful
Code check / Check code (push) Successful in 1m17s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-21 20:41:21 +05:00
3736bb279e feat: use SGDB for cover too
All checks were successful
Code check / Check code (push) Successful in 1m33s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-20 13:07:09 +05:00
Renovate Bot
b59ee5ae8e chore(deps): update archlinux:base-devel docker digest to 87a967f
All checks were successful
Code check / Check code (push) Successful in 1m8s
Build AppImage, Arch and Fedora Packages / Build AppImage (push) Successful in 2m28s
Build AppImage, Arch and Fedora Packages / Build Arch Package (push) Successful in 1m13s
Build AppImage, Arch and Fedora Packages / Build Fedora RPM (41) (push) Successful in 54s
Build AppImage, Arch and Fedora Packages / Build Fedora RPM (42) (push) Successful in 1m0s
Build AppImage, Arch and Fedora Packages / Build Fedora RPM (43) (push) Successful in 1m9s
Build AppImage, Arch and Fedora Packages / Build Fedora RPM (rawhide) (push) Successful in 59s
Build AppImage, Arch and Fedora Packages / Create and Publish Release (push) Successful in 22s
2025-10-19 12:07:20 +00:00
33176590fd feat: Make autoinstall games loading asynchronous with caching
All checks were successful
Code check / Check code (push) Successful in 1m7s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-19 17:03:26 +05:00
8046065929 refactor(gamepad): replace busy-wait with threading.Event for monitor readiness
All checks were successful
Code check / Check code (push) Successful in 1m12s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-19 11:00:22 +05:00
Renovate Bot
fbad5add6c chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.1
All checks were successful
Code check / Check code (pull_request) Successful in 1m5s
Code check / Check code (push) Successful in 1m1s
2025-10-19 00:01:23 +00:00
49 changed files with 15324 additions and 2240 deletions

View File

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

View File

@@ -119,7 +119,7 @@ jobs:
- name: Install build dependencies - name: Install build dependencies
run: | run: |
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \ 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 redhat-rpm-config nodejs npm
- name: Setup rpmbuild environment - name: Setup rpmbuild environment

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,11 +6,12 @@ script:
- uv pip install --no-cache-dir ../ - uv pip install --no-cache-dir ../
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages - cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
- cp -r share AppDir/usr - cp -r share AppDir/usr
- cp -r lib AppDir/usr
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/ - 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/{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 - 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: AppDir:
path: ./AppDir path: ./AppDir
after_bundle: after_bundle:

View File

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

View File

@@ -25,4 +25,5 @@ package() {
cd "$srcdir/PortProtonQt" cd "$srcdir/PortProtonQt"
python -m installer --destdir="$pkgdir" dist/*.whl python -m installer --destdir="$pkgdir" dist/*.whl
cp -r build-aux/share "$pkgdir/usr/" 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: pyproject-rpm-macros
BuildRequires: python3dist(setuptools) BuildRequires: python3dist(setuptools)
BuildRequires: git BuildRequires: git
BuildRequires: systemd-rpm-macros
%description %description
%{summary} %{summary}
@@ -69,11 +70,13 @@ cd %{oname}
%pyproject_install %pyproject_install
%pyproject_save_files %{pypi_name} %pyproject_save_files %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/ cp -r build-aux/share %{buildroot}/usr/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name}-git -f %{pyproject_files} %files -n python3-%{pypi_name}-git -f %{pyproject_files}
%{_bindir}/%{pypi_name} %{_bindir}/%{pypi_name}
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg %{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml %{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
%{_udevrulesdir}/60-portprotonqt.rules
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop %{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%{bash_completions_dir}/portprotonqt %{bash_completions_dir}/portprotonqt

View File

@@ -19,6 +19,7 @@ BuildRequires: python3-build
BuildRequires: pyproject-rpm-macros BuildRequires: pyproject-rpm-macros
BuildRequires: python3dist(setuptools) BuildRequires: python3dist(setuptools)
BuildRequires: git BuildRequires: git
BuildRequires: systemd-rpm-macros
%description %description
%{summary} %{summary}
@@ -68,11 +69,13 @@ cd %{oname}
%pyproject_install %pyproject_install
%pyproject_save_files %{pypi_name} %pyproject_save_files %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/ cp -r build-aux/share %{buildroot}/usr/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name} -f %{pyproject_files} %files -n python3-%{pypi_name} -f %{pyproject_files}
%{_bindir}/%{pypi_name} %{_bindir}/%{pypi_name}
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg %{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml %{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
%{_udevrulesdir}/60-portprotonqt.rules
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop %{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%{bash_completions_dir}/portprotonqt %{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", "normalized_name": "farlight 84",
"status": "Supported" "status": "Denied"
}, },
{ {
"normalized_name": "riders republic", "normalized_name": "riders republic",
@@ -1436,8 +1436,8 @@
"status": "Broken" "status": "Broken"
}, },
{ {
"normalized_name": "blue protocol", "normalized_name": "blue protocol star resonance",
"status": "Broken" "status": "Running"
}, },
{ {
"normalized_name": "dark and darker", "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", "normalized_title": "dirt rally 2.0 game of the year",
"slug": "dirt-rally-2-0-game-of-the-year-edition" "slug": "dirt-rally-2-0-game-of-the-year-edition"
@@ -271,10 +375,6 @@
"normalized_title": "steins;gate the distant valhalla", "normalized_title": "steins;gate the distant valhalla",
"slug": "steins-gate-the-distant-valhalla" "slug": "steins-gate-the-distant-valhalla"
}, },
{
"normalized_title": "hogwarts legacy",
"slug": "hogwarts-legacy"
},
{ {
"normalized_title": "osu!", "normalized_title": "osu!",
"slug": "osu" "slug": "osu"

Binary file not shown.

View File

@@ -17,17 +17,31 @@ import json
class PySide6DependencyAnalyzer: class PySide6DependencyAnalyzer:
def __init__(self): def __init__(self, project_root: Path = None):
# Системные библиотеки, которые нужно всегда оставлять # Системные библиотеки, которые нужно всегда оставлять
self.system_libs = { self.system_libs = {
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl', 'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus' 'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
'libQt6Svg'
}
self.critical_modules = {
'QtSvg',
} }
self.real_dependencies = {} self.real_dependencies = {}
self.used_modules_code = set() self.used_modules_code = set()
self.used_modules_ldd = set() self.used_modules_ldd = set()
self.all_required_modules = 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]: def find_python_files(self, directory: Path) -> List[Path]:
"""Находит все Python файлы в директории""" """Находит все Python файлы в директории"""
@@ -44,24 +58,61 @@ class PySide6DependencyAnalyzer:
"""Находит все PySide6 библиотеки (.so файлы)""" """Находит все PySide6 библиотеки (.so файлы)"""
libs = {} libs = {}
# Поиск в единственной локации # Ищем venv в корне проекта
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6") venv_candidates = [
print(f"Поиск PySide6 библиотек в: {search_path}") self.venv_path, # .venv
self.project_root / "venv",
self.project_root / ".virtualenv",
]
if search_path.exists(): pyside6_path = None
# Ищем .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 в venv
for subdir in search_path.iterdir(): for venv in venv_candidates:
if subdir.is_dir() and subdir.name.startswith('Qt'): if venv.exists():
for so_file in subdir.glob("*.so*"): # Ищем Python версию
if 'Qt' in so_file.name: lib_path = venv / "lib"
libs[subdir.name] = so_file if lib_path.exists():
break 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 return libs
@@ -257,8 +308,10 @@ class PySide6DependencyAnalyzer:
# Модули для удаления # Модули для удаления
if removable_modules: if removable_modules:
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)]) removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}") 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() 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})" f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
]) ])
# Заменяем блок очистки в рецепте
import re import re
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir: # Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)' # Паттерн: после " - 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 return updated_recipe
def main(): def main():
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd') 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('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)') parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод') parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')
args = parser.parse_args() args = parser.parse_args()
project_path = Path(args.project_path) project_path = Path(args.project_path).resolve()
if not project_path.exists(): if not project_path.exists():
print(f"Ошибка: путь {project_path} не существует") print(f"Ошибка: путь {project_path} не существует")
sys.exit(1) 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(): if appdir_path and not appdir_path.exists():
print(f"Предупреждение: AppDir путь {appdir_path} не существует") print(f"Предупреждение: AppDir путь {appdir_path} не существует")
appdir_path = None 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) results = analyzer.analyze_project(project_path, appdir_path)
# Сохраняем в анализатор для генерации команд # Сохраняем в анализатор для генерации команд
@@ -347,13 +443,13 @@ def main():
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}") print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
if args.verbose and results['real_dependencies']: if args.verbose and results['real_dependencies']:
Devlin(f"\nРеальные зависимости (ldd):") print(f"\nРеальные зависимости (ldd):")
for module, deps in results['real_dependencies'].items(): for module, deps in results['real_dependencies'].items():
if deps: if deps:
print(f" {module}{', '.join(deps)}") print(f" {module}{', '.join(deps)}")
# Обновляем AppImage рецепт # Обновляем AppImage рецепт
recipe_path = Path("../build-aux/AppImageBuilder.yml") recipe_path = analyzer.build_path / "AppImageBuilder.yml"
if recipe_path.exists(): if recipe_path.exists():
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path) updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
if updated_recipe: if updated_recipe:

View File

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

View File

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

View File

@@ -1,11 +1,17 @@
import os import os
import configparser import configparser
import shutil import shutil
import subprocess
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
_portproton_location = None _portproton_location = None
_portproton_start_sh = None
# Configuration cache for performance optimization
_config_cache = {}
_config_last_modified = {}
# Paths to configuration files # Paths to configuration files
CONFIG_FILE = os.path.join( CONFIG_FILE = os.path.join(
@@ -26,13 +32,35 @@ THEMES_DIRS = [
] ]
def read_config_safely(config_file: str) -> configparser.ConfigParser | None: 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.""" """Safely reads a configuration file and returns a ConfigParser object or None if reading fails.
cp = configparser.ConfigParser() Uses caching to avoid repeated file reads for better performance.
"""
# Check if file exists
if not os.path.exists(config_file): if not os.path.exists(config_file):
logger.debug(f"Configuration file {config_file} not found") logger.debug(f"Configuration file {config_file} not found")
return None 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: try:
cp.read(config_file, encoding="utf-8") 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 return cp
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e: except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
logger.warning(f"Invalid configuration file format: {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}") logger.warning(f"Failed to read configuration file: {e}")
return None 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(): def read_config():
"""Reads the configuration file and returns a dictionary of parameters. """Reads the configuration file and returns a dictionary of parameters.
Example line in config (no sections): Example line in config (no sections):
@@ -75,6 +111,8 @@ def save_theme_to_config(theme_name):
cp["Appearance"]["theme"] = theme_name cp["Appearance"]["theme"] = theme_name
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_time_config(): def read_time_config():
"""Reads time settings from the [Time] section of the configuration file. """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 cp["Time"]["detail_level"] = detail_level
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_file_content(file_path): def read_file_content(file_path):
"""Reads the content of a file and returns it as a string.""" """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() return f.read().strip()
def get_portproton_location(): def get_portproton_location():
"""Returns the path to the PortProton directory. """Возвращает путь к PortProton каталогу (строку) или None."""
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
If the path is invalid, uses the default directory.
"""
global _portproton_location global _portproton_location
if _portproton_location is not None: if _portproton_location is not None:
return _portproton_location return _portproton_location
location = None
if os.path.isfile(PORTPROTON_CONFIG_FILE): if os.path.isfile(PORTPROTON_CONFIG_FILE):
try: try:
location = read_file_content(PORTPROTON_CONFIG_FILE).strip() location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
@@ -116,19 +156,46 @@ def get_portproton_location():
_portproton_location = location _portproton_location = location
logger.info(f"PortProton path from configuration: {location}") logger.info(f"PortProton path from configuration: {location}")
return _portproton_location return _portproton_location
logger.warning(f"Invalid PortProton path in configuration: {location}, using default path") logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
except (OSError, PermissionError) as e: except (OSError, PermissionError) as e:
logger.warning(f"Failed to read PortProton configuration file: {e}, using default path") logger.warning(f"Failed to read PortProton configuration file: {e}")
default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton") default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
if os.path.isdir(default_dir): if os.path.isdir(default_flatpak_dir):
_portproton_location = default_dir _portproton_location = default_flatpak_dir
logger.info(f"Using flatpak PortProton directory: {default_dir}") logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}")
return _portproton_location return _portproton_location
logger.warning("PortProton configuration and flatpak directory not found") logger.warning("PortProton configuration and Flatpak directory not found")
return None return None
def get_portproton_start_command():
"""Возвращает список команд для запуска PortProton (start.sh или flatpak run)."""
portproton_path = get_portproton_location()
if not portproton_path:
return None
try:
result = subprocess.run(
["flatpak", "list"],
capture_output=True,
text=True,
check=False
)
if "ru.linux_gaming.PortProton" in result.stdout:
logger.info("Detected Flatpak installation")
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
except Exception:
pass
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
if os.path.exists(start_sh_path):
return [start_sh_path]
logger.warning("Neither flatpak nor start.sh found for PortProton")
return None
def parse_desktop_entry(file_path): def parse_desktop_entry(file_path):
"""Reads and parses a .desktop file using configparser. """Reads and parses a .desktop file using configparser.
Returns None if the [Desktop Entry] section is missing. Returns None if the [Desktop Entry] section is missing.
@@ -176,6 +243,8 @@ def save_card_size(card_width):
cp["Cards"]["card_width"] = str(card_width) cp["Cards"]["card_width"] = str(card_width)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_auto_card_size(): def read_auto_card_size():
"""Reads the card size (width) for Auto Install from the [Cards] section. """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) cp["Cards"]["auto_card_width"] = str(card_width)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_sort_method(): def read_sort_method():
@@ -215,6 +286,8 @@ def save_sort_method(sort_method):
cp["Games"]["sort_method"] = sort_method cp["Games"]["sort_method"] = sort_method
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_display_filter(): def read_display_filter():
"""Reads the display_filter parameter from the [Games] section. """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 cp["Games"]["display_filter"] = filter_value
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_favorites(): def read_favorites():
"""Reads the list of favorite games from the [Favorites] section. """Reads the list of favorite games from the [Favorites] section.
@@ -259,6 +334,8 @@ def save_favorites(favorites):
cp["Favorites"]["games"] = f'"{fav_str}"' cp["Favorites"]["games"] = f'"{fav_str}"'
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_rumble_config(): def read_rumble_config():
"""Reads the gamepad rumble setting from the [Gamepad] section. """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) cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_gamepad_type(): def read_gamepad_type():
"""Reads the gamepad type from the [Gamepad] section. """Reads the gamepad type from the [Gamepad] section.
@@ -297,6 +376,8 @@ def save_gamepad_type(gpad_type):
cp["Gamepad"]["type"] = gpad_type cp["Gamepad"]["type"] = gpad_type
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def ensure_default_proxy_config(): def ensure_default_proxy_config():
"""Ensures the [Proxy] section exists in the configuration file. """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 cp["Proxy"]["proxy_password"] = proxy_password
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_fullscreen_config(): def read_fullscreen_config():
"""Reads the fullscreen mode setting from the [Display] section. """Reads the fullscreen mode setting from the [Display] section.
@@ -360,6 +443,8 @@ def save_fullscreen_config(fullscreen):
cp["Display"]["fullscreen"] = str(fullscreen) cp["Display"]["fullscreen"] = str(fullscreen)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_window_geometry() -> tuple[int, int]: def read_window_geometry() -> tuple[int, int]:
"""Reads the window width and height from the [MainWindow] section. """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) cp["MainWindow"]["height"] = str(height)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def reset_config(): def reset_config():
"""Resets the configuration file by deleting it. """Resets the configuration file by deleting it.
@@ -390,6 +477,8 @@ def reset_config():
try: try:
os.remove(CONFIG_FILE) os.remove(CONFIG_FILE)
logger.info("Configuration file %s deleted", CONFIG_FILE) logger.info("Configuration file %s deleted", CONFIG_FILE)
# Invalidate cache after deletion
invalidate_config_cache()
except Exception as e: except Exception as e:
logger.warning(f"Failed to delete configuration file: {e}") logger.warning(f"Failed to delete configuration file: {e}")
@@ -404,6 +493,9 @@ def clear_cache():
except Exception as e: except Exception as e:
logger.warning(f"Failed to delete cache: {e}") logger.warning(f"Failed to delete cache: {e}")
# Also clear our internal config cache
invalidate_config_cache()
def read_auto_fullscreen_gamepad(): def read_auto_fullscreen_gamepad():
"""Reads the auto-fullscreen setting for gamepad from the [Display] section. """Reads the auto-fullscreen setting for gamepad from the [Display] section.
Returns False if the parameter is missing. 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) cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_favorite_folders(): def read_favorite_folders():
"""Reads the list of favorite folders from the [FavoritesFolders] section. """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}"' cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_minimize_to_tray(): def read_minimize_to_tray():
"""Reads the minimize-to-tray setting from the [Display] section. """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) cp["Display"]["minimize_to_tray"] = str(minimize_to_tray)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
# 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.QtCore import QUrl, QPoint, QObject, Signal, Qt
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
from portprotonqt.localization import _ from portprotonqt.localization import _
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders, get_portproton_start_command
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
@@ -406,16 +406,7 @@ class ContextMenuManager:
) )
return return
# Construct EGS launch command # Construct EGS launch command
wrapper = "flatpak run ru.linux_gaming.PortProton" wrapper = get_portproton_start_command()
start_sh_path = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
if self.portproton_location and ".var" not in self.portproton_location:
wrapper = start_sh_path
if not os.path.exists(start_sh_path):
self.signals.show_warning_dialog.emit(
_("Error"),
_("start.sh not found at {path}").format(path=start_sh_path)
)
return
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"' exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
else: else:
exec_line = self._get_exec_line(game_card.name, game_card.exec_line) exec_line = self._get_exec_line(game_card.name, game_card.exec_line)

View File

@@ -2,12 +2,11 @@ import os
import tempfile import tempfile
import re import re
from typing import cast, TYPE_CHECKING from typing import cast, TYPE_CHECKING
from PySide6.QtGui import QPixmap, QIcon, QTextCursor from PySide6.QtGui import QPixmap, QIcon, QTextCursor, QColor
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller, QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget, QComboBox, QLineEdit
) )
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
from icoextract import IconExtractor, IconExtractorError from icoextract import IconExtractor, IconExtractorError
from PIL import Image from PIL import Image
@@ -19,6 +18,7 @@ from portprotonqt.custom_widgets import AutoSizeButton
from portprotonqt.downloader import Downloader from portprotonqt.downloader import Downloader
from portprotonqt.virtual_keyboard import VirtualKeyboard from portprotonqt.virtual_keyboard import VirtualKeyboard
from portprotonqt.preloader import Preloader from portprotonqt.preloader import Preloader
from portprotonqt.settings_manager import get_toggle_settings, get_advanced_settings, ADVANCED_SETTING_KEYS
import psutil import psutil
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -1326,7 +1326,9 @@ class WinetricksDialog(QDialog):
# DLLs tab # DLLs tab
self.dll_table = QTableWidget() self.dll_table = QTableWidget()
self.dll_table.setAlternatingRowColors(True)
self.dll_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.dll_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.dll_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.dll_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.dll_table.setColumnCount(3) self.dll_table.setColumnCount(3)
@@ -1357,7 +1359,9 @@ class WinetricksDialog(QDialog):
# Fonts tab # Fonts tab
self.fonts_table = QTableWidget() self.fonts_table = QTableWidget()
self.fonts_table.setAlternatingRowColors(True)
self.fonts_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.fonts_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.fonts_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.fonts_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.fonts_table.setColumnCount(3) self.fonts_table.setColumnCount(3)
@@ -1388,7 +1392,9 @@ class WinetricksDialog(QDialog):
# Settings tab # Settings tab
self.settings_table = QTableWidget() self.settings_table = QTableWidget()
self.settings_table.setAlternatingRowColors(True)
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.settings_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.settings_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.settings_table.setColumnCount(3) self.settings_table.setColumnCount(3)
@@ -1674,3 +1680,417 @@ class WinetricksDialog(QDialog):
if self.input_manager: if self.input_manager:
self.input_manager.disable_winetricks_mode() self.input_manager.disable_winetricks_mode()
super().reject() super().reject()
class ExeSettingsDialog(QDialog):
def __init__(self, parent=None, theme=None, exe_path=None):
super().__init__(parent)
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
self.exe_path = exe_path
if not self.exe_path:
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.logger import get_logger
from portprotonqt.image_utils import load_pixmap_async from portprotonqt.image_utils import load_pixmap_async
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
from portprotonqt.config_utils import get_portproton_location from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
from portprotonqt.steam_api import ( from portprotonqt.steam_api import (
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async, get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
@@ -254,14 +254,7 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba
return return
# Determine wrapper # Determine wrapper
wrapper = "flatpak run ru.linux_gaming.PortProton" wrapper = get_portproton_start_command()
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
if portproton_dir is not None and ".var" not in portproton_dir:
wrapper = start_sh_path
if not os.path.exists(start_sh_path):
logger.error(f"start.sh not found at {start_sh_path}")
callback((False, f"start.sh not found at {start_sh_path}"))
return
# Create launch script # Create launch script
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts") steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")

View File

@@ -1,5 +1,5 @@
from PySide6.QtGui import QPainter, QColor, QDesktopServices from PySide6.QtGui import QPainter, QColor, QDesktopServices
from PySide6.QtCore import Signal, Property, Qt, QUrl from PySide6.QtCore import Signal, Property, Qt, QUrl, QTimer
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
from collections.abc import Callable from collections.abc import Callable
from portprotonqt.image_utils import load_pixmap_async, round_corners from portprotonqt.image_utils import load_pixmap_async, round_corners
@@ -102,7 +102,7 @@ class GameCard(QFrame):
self.favoriteLabel = ClickableLabel(self.coverWidget) self.favoriteLabel = ClickableLabel(self.coverWidget)
self.favoriteLabel.clicked.connect(self.toggle_favorite) 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.update_favorite_icon()
self.favoriteLabel.raise_() self.favoriteLabel.raise_()
@@ -203,7 +203,7 @@ class GameCard(QFrame):
self.update_cover_pixmap() self.update_cover_pixmap()
def update_cover_pixmap(self): 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_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) 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)) rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
@@ -404,14 +404,22 @@ class GameCard(QFrame):
self.favoriteLabel.setText("") self.favoriteLabel.setText("")
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE) self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
parent = self.parent()
while parent:
if hasattr(parent, 'game_library_manager'):
QTimer.singleShot(0, parent.game_library_manager.update_game_grid) # type: ignore[attr-defined]
break
parent = parent.parent()
def toggle_favorite(self): def toggle_favorite(self):
favorites = read_favorites() favorites = read_favorites()
favorites_set = set(favorites)
if self.is_favorite: if self.is_favorite:
if self.name in favorites: if self.name in favorites_set:
favorites.remove(self.name) favorites.remove(self.name)
self.is_favorite = False self.is_favorite = False
else: else:
if self.name not in favorites: if self.name not in favorites_set:
favorites.append(self.name) favorites.append(self.name)
self.is_favorite = True self.is_favorite = True
save_favorites(favorites) 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) 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 # Quick partition: Sort favorites and non-favorites separately, then merge
fav_games = [g for g in games_list if g[0] in favorites] favorites_set = set(favorites) # Convert to set for O(1) lookup
non_fav_games = [g for g in games_list if g[0] not in favorites] 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_fav = sorted(fav_games, key=partition_sort_key)
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key) sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
sorted_games = sorted_fav + sorted_non_fav 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() current_theme_name = read_theme_from_config()
def finish_with(pixmap: QPixmap): def finish_with(pixmap: QPixmap):
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation) # Check if pixmap is valid before attempting to scale it
x = (scaled.width() - width) // 2 if pixmap.isNull():
y = (scaled.height() - height) // 2 # Create a default placeholder pixmap instead of trying to scale a null pixmap
cropped = scaled.copy(x, y, width, height) placeholder_pixmap = QPixmap(width, height)
callback(cropped) 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")) 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") 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") local_path = os.path.join(image_folder, f"{appid}.jpg")
if os.path.exists(local_path): if os.path.exists(local_path):
pixmap = QPixmap(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) finish_with(pixmap)
return 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) placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder_path and QFile.exists(placeholder_path): if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path) pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else: else:
pixmap = QPixmap(width, height) pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333")) 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: except Exception as e:
logger.error(f"Ошибка обработки URL {cover}: {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: 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): if os.path.exists(local_path):
pixmap = QPixmap(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) finish_with(pixmap)
return 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) placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder_path and QFile.exists(placeholder_path): if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(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: else:
pixmap = QPixmap(width, height) pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333")) 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): if cover and QFile.exists(cover):
pixmap = QPixmap(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) finish_with(pixmap)
return return
@@ -122,6 +188,8 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
pixmap = QPixmap() pixmap = QPixmap()
if placeholder_path and QFile.exists(placeholder_path): if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path) pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else: else:
pixmap = QPixmap(width, height) pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333")) pixmap.fill(QColor("#333333"))
@@ -141,7 +209,15 @@ def round_corners(pixmap, radius):
""" """
if pixmap.isNull(): if pixmap.isNull():
return pixmap return pixmap
# Check if radius is valid to prevent issues
if radius <= 0:
return pixmap
size = pixmap.size() size = pixmap.size()
if size.width() <= 0 or size.height() <= 0:
return pixmap
rounded = QPixmap(size) rounded = QPixmap(size)
rounded.fill(QColor(0, 0, 0, 0)) rounded.fill(QColor(0, 0, 0, 0))
painter = QPainter(rounded) painter = QPainter(rounded)
@@ -244,20 +320,31 @@ class FullscreenDialog(QDialog):
QApplication.processEvents() QApplication.processEvents()
pixmap, caption = self.images[self.current_index] pixmap, caption = self.images[self.current_index]
# Учитываем devicePixelRatio для масштабирования высокого качества # Check if pixmap is valid before attempting to scale it
device_pixel_ratio = get_device_pixel_ratio() if pixmap.isNull():
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio) # Create a default placeholder pixmap instead of trying to scale a null pixmap
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio) 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 # Масштабируем изображение из оригинального pixmap
scaled_pixmap = pixmap.scaled( scaled_pixmap = pixmap.scaled(
target_width, target_width,
target_height, target_height,
Qt.AspectRatioMode.KeepAspectRatio, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation Qt.TransformationMode.SmoothTransformation
) )
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio) scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
self.imageLabel.setPixmap(scaled_pixmap) self.imageLabel.setPixmap(scaled_pixmap)
self.captionLabel.setText(caption) self.captionLabel.setText(caption)
self.setWindowTitle(caption) self.setWindowTitle(caption)

View File

@@ -1,8 +1,9 @@
import time import time
import threading import threading
import os import os
import math
from typing import Protocol, cast 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 enum import Enum
from pyudev import Context, Monitor, Device, Devices 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 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.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.dialogs import AddGameDialog
from portprotonqt.virtual_keyboard import VirtualKeyboard from portprotonqt.virtual_keyboard import VirtualKeyboard
import select
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -115,6 +117,34 @@ class InputManager(QObject):
self.last_trigger_time = 0.0 self.last_trigger_time = 0.0
self.trigger_cooldown = 0.2 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 # FileExplorer specific attributes
self.file_explorer = None self.file_explorer = None
self.original_button_handler = None self.original_button_handler = None
@@ -151,6 +181,13 @@ class InputManager(QObject):
# Initialize evdev + hotplug # Initialize evdev + hotplug
self.init_gamepad() 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: def _navigate_game_cards(self, container, tab_index: int, code: int, value: int) -> None:
"""Common navigation logic for game cards in a container.""" """Common navigation logic for game cards in a container."""
if container is None: if container is None:
@@ -637,6 +674,116 @@ class InputManager(QObject):
except Exception as e: except Exception as e:
logger.error(f"Error in navigation repeat: {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) @Slot(bool)
def handle_fullscreen_slot(self, enable: bool) -> None: def handle_fullscreen_slot(self, enable: bool) -> None:
try: try:
@@ -1440,6 +1587,7 @@ class InputManager(QObject):
self.udev_context = Context() self.udev_context = Context()
self.Devices = Devices self.Devices = Devices
self.monitor_ready = False self.monitor_ready = False
self.monitor_event = threading.Event()
# Подключаем сигнал hotplug к обработчику в главном потоке # Подключаем сигнал hotplug к обработчику в главном потоке
self.gamepad_hotplug.connect(self._on_gamepad_hotplug) self.gamepad_hotplug.connect(self._on_gamepad_hotplug)
@@ -1472,7 +1620,6 @@ class InputManager(QObject):
logger.error(f"Failed to start udev monitor: {e}") logger.error(f"Failed to start udev monitor: {e}")
return return
import select
fd = monitor.fileno() fd = monitor.fileno()
poller = select.poll() poller = select.poll()
poller.register(fd, select.POLLIN) poller.register(fd, select.POLLIN)
@@ -1491,6 +1638,7 @@ class InputManager(QObject):
break break
self.monitor_ready = True self.monitor_ready = True
self.monitor_event.set()
logger.info(f"Drained {drained_count} initial events, now monitoring hotplug...") logger.info(f"Drained {drained_count} initial events, now monitoring hotplug...")
# Основной цикл # Основной цикл
@@ -1592,7 +1740,6 @@ class InputManager(QObject):
except Exception as e: except Exception as e:
logger.error(f"Error in hotplug handler: {e}", exc_info=True) logger.error(f"Error in hotplug handler: {e}", exc_info=True)
def check_gamepad(self) -> None: def check_gamepad(self) -> None:
""" """
Проверка и подключение геймпада. Проверка и подключение геймпада.
@@ -1601,18 +1748,23 @@ class InputManager(QObject):
try: try:
new_gamepad = self.find_gamepad() new_gamepad = self.find_gamepad()
# Проверяем, действительно ли это новый геймпад
if new_gamepad: if new_gamepad:
if not self.gamepad or new_gamepad.path != self.gamepad.path: if not self.gamepad or new_gamepad.path != self.gamepad.path:
logger.info(f"Gamepad connected: {new_gamepad.name} at {new_gamepad.path}") logger.info(f"Gamepad connected: {new_gamepad.name} at {new_gamepad.path}")
self.stop_rumble() self.stop_rumble()
self.gamepad = new_gamepad self.gamepad = new_gamepad
if self.gamepad_thread: if self.gamepad_thread and self.gamepad_thread.is_alive():
self.gamepad_thread.join(timeout=2.0) self.gamepad_thread.join(timeout=2.0)
def start_monitoring():
# Ожидание готовности udev monitor без busy-wait
if not self.monitor_event.wait(timeout=2.0):
logger.warning("Timeout waiting for udev monitor readiness")
self.monitor_gamepad()
self.gamepad_thread = threading.Thread( self.gamepad_thread = threading.Thread(
target=self.monitor_gamepad, target=start_monitoring,
daemon=True daemon=True
) )
self.gamepad_thread.start() self.gamepad_thread.start()
@@ -1622,12 +1774,11 @@ class InputManager(QObject):
self.toggle_fullscreen.emit(True) self.toggle_fullscreen.emit(True)
elif self.gamepad and not any(self.gamepad.path == path for path in list_devices()): elif self.gamepad and not any(self.gamepad.path == path for path in list_devices()):
# Геймпад был подключён, но теперь его нет в системе
logger.info("Gamepad no longer detected") logger.info("Gamepad no longer detected")
self.stop_rumble() self.stop_rumble()
self.gamepad = None self.gamepad = None
if self.gamepad_thread: if self.gamepad_thread and self.gamepad_thread.is_alive():
self.gamepad_thread.join(timeout=2.0) self.gamepad_thread.join(timeout=2.0)
if read_auto_fullscreen_gamepad() and not read_fullscreen_config(): if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
@@ -1636,7 +1787,6 @@ class InputManager(QObject):
except Exception as e: except Exception as e:
logger.error(f"Error checking gamepad: {e}", exc_info=True) logger.error(f"Error checking gamepad: {e}", exc_info=True)
def find_gamepad(self) -> InputDevice | None: def find_gamepad(self) -> InputDevice | None:
""" """
Находит первый доступный геймпад. Находит первый доступный геймпад.
@@ -1688,60 +1838,117 @@ class InputManager(QObject):
logger.error(f"Error finding gamepad: {e}", exc_info=True) logger.error(f"Error finding gamepad: {e}", exc_info=True)
return None return None
def monitor_gamepad(self) -> None: def monitor_gamepad(self) -> None:
try: try:
if not self.gamepad: while self.running:
return current_time = time.time()
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()
# Проверка фокуса: игнорируем события, если окно не в фокусе if self.gamepad:
app = QApplication.instance() try:
active = QApplication.activeWindow() # Non-blocking read with short timeout
if not app or not active: events = []
continue r, w, x = select.select([self.gamepad.fd], [], [], 0.001)
if r:
events = list(self.gamepad.read())
if event.type == ecodes.EV_KEY: # Process events
# Emit on both press (1) and release (0) for event in events:
self.button_event.emit(event.code, event.value) if not self.running:
# Special handling for menu on press only break
if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
self.toggle_fullscreen.emit(not self._is_fullscreen) # UI signal handling (always, for internal app)
elif event.type == ecodes.EV_ABS: if event.type == ecodes.EV_KEY:
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}: if event.code == ecodes.BTN_START:
# Проверяем, достаточно ли времени прошло с последнего срабатывания self.start_held = (event.value == 1)
if now - self.last_trigger_time < self.trigger_cooldown:
continue if event.code in BUTTONS['guide']:
if event.code == ecodes.ABS_Z: # LT/L2 self.guide_held = (event.value == 1)
if event.value > 128 and not self.lt_pressed:
self.lt_pressed = True if event.value == 1:
self.button_event.emit(event.code, 1) # Emit as press if ((event.code in BUTTONS['guide'] and self.start_held) or
self.last_trigger_time = now (event.code == ecodes.BTN_START and self.guide_held)):
elif event.value <= 128 and self.lt_pressed: self.emulation_triggered = not self.emulation_triggered
self.lt_pressed = False
self.button_event.emit(event.code, 0) # Emit as release self.button_event.emit(event.code, event.value)
elif event.code == ecodes.ABS_RZ: # RT/R2 # Special handling for menu on press only
if event.value > 128 and not self.rt_pressed: if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
self.rt_pressed = True self.toggle_fullscreen.emit(not self._is_fullscreen)
self.button_event.emit(event.code, 1) # Emit as press elif event.type == ecodes.EV_ABS:
self.last_trigger_time = now if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
elif event.value <= 128 and self.rt_pressed: # Trigger handling for UI
self.rt_pressed = False if current_time - self.last_trigger_time < self.trigger_cooldown:
self.button_event.emit(event.code, 0) # Emit as release continue
else: if event.code == ecodes.ABS_Z: # LT/L2
self.dpad_moved.emit(event.code, event.value, now) if event.value > 128 and not self.lt_pressed:
except OSError as e: self.lt_pressed = True
if e.errno == 19: # ENODEV: No such device self.button_event.emit(event.code, 1)
logger.info("Gamepad disconnected during event loop") self.last_trigger_time = current_time
else: elif event.value <= 128 and self.lt_pressed:
logger.error(f"OSError in gamepad monitoring: {e}", exc_info=True) 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: 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: finally:
if self.gamepad: if self.gamepad:
try: try:
@@ -1750,12 +1957,21 @@ class InputManager(QObject):
except Exception: except Exception:
pass pass
self.gamepad = None self.gamepad = None
self.start_held = False
self.guide_held = False
self.emulation_triggered = False
def cleanup(self) -> None: def cleanup(self) -> None:
""" """
Корректное завершение работы с геймпадом и udev монитором. Корректное завершение работы с геймпадом и udev монитором.
""" """
try: try:
# Mouse emulation cleanup
self.disable_mouse_emulation()
# Stop focus check timer
self.focus_check_timer.stop()
# Флаг для остановки udev monitor loop # Флаг для остановки udev monitor loop
self.running = False self.running = False

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n" "Language: de_DE\n"
@@ -76,10 +76,6 @@ msgstr ""
msgid "Legendary executable not found at {path}" msgid "Legendary executable not found at {path}"
msgstr "" msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success" msgid "Success"
msgstr "" msgstr ""
@@ -124,6 +120,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites" msgid "Removed '{game_name}' from favorites"
msgstr "" msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format #, python-brace-format
msgid "Launch game \"{name}\" with PortProton" msgid "Launch game \"{name}\" with PortProton"
msgstr "" msgstr ""
@@ -365,6 +365,39 @@ msgstr ""
msgid "Components installed successfully." msgid "Components installed successfully."
msgstr "" 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..." msgid "Loading Epic Games Store games..."
msgstr "" msgstr ""
@@ -512,14 +545,21 @@ msgstr ""
msgid "Are you sure you want to clear prefix '{}'?" msgid "Are you sure you want to clear prefix '{}'?"
msgstr "" msgstr ""
#, python-brace-format msgid "Clearing prefix..."
msgid "Prefix '{}' cleared successfully." msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr "" msgstr ""
#, python-brace-format #, python-brace-format
msgid "" msgid "Prefix clear failed with exit code {}."
"Prefix '{}' cleared with errors:\n" msgstr ""
"{}"
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr "" msgstr ""
msgid "Failed to start backup process." msgid "Failed to start backup process."
@@ -704,6 +744,10 @@ msgstr ""
msgid "Error applying theme '{0}'" msgid "Error applying theme '{0}'"
msgstr "" msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH" msgid "LAST LAUNCH"
msgstr "" msgstr ""
@@ -762,6 +806,232 @@ msgstr ""
msgid "File not found: {0}" msgid "File not found: {0}"
msgstr "" 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" msgid "Reboot"
msgstr "" msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n" "Language: es_ES\n"
@@ -76,10 +76,6 @@ msgstr ""
msgid "Legendary executable not found at {path}" msgid "Legendary executable not found at {path}"
msgstr "" msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success" msgid "Success"
msgstr "" msgstr ""
@@ -124,6 +120,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites" msgid "Removed '{game_name}' from favorites"
msgstr "" msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format #, python-brace-format
msgid "Launch game \"{name}\" with PortProton" msgid "Launch game \"{name}\" with PortProton"
msgstr "" msgstr ""
@@ -365,6 +365,39 @@ msgstr ""
msgid "Components installed successfully." msgid "Components installed successfully."
msgstr "" 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..." msgid "Loading Epic Games Store games..."
msgstr "" msgstr ""
@@ -512,14 +545,21 @@ msgstr ""
msgid "Are you sure you want to clear prefix '{}'?" msgid "Are you sure you want to clear prefix '{}'?"
msgstr "" msgstr ""
#, python-brace-format msgid "Clearing prefix..."
msgid "Prefix '{}' cleared successfully." msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr "" msgstr ""
#, python-brace-format #, python-brace-format
msgid "" msgid "Prefix clear failed with exit code {}."
"Prefix '{}' cleared with errors:\n" msgstr ""
"{}"
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr "" msgstr ""
msgid "Failed to start backup process." msgid "Failed to start backup process."
@@ -704,6 +744,10 @@ msgstr ""
msgid "Error applying theme '{0}'" msgid "Error applying theme '{0}'"
msgstr "" msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH" msgid "LAST LAUNCH"
msgstr "" msgstr ""
@@ -762,6 +806,232 @@ msgstr ""
msgid "File not found: {0}" msgid "File not found: {0}"
msgstr "" 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" msgid "Reboot"
msgstr "" msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n" "Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -74,10 +74,6 @@ msgstr ""
msgid "Legendary executable not found at {path}" msgid "Legendary executable not found at {path}"
msgstr "" msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success" msgid "Success"
msgstr "" msgstr ""
@@ -122,6 +118,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites" msgid "Removed '{game_name}' from favorites"
msgstr "" msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format #, python-brace-format
msgid "Launch game \"{name}\" with PortProton" msgid "Launch game \"{name}\" with PortProton"
msgstr "" msgstr ""
@@ -363,6 +363,39 @@ msgstr ""
msgid "Components installed successfully." msgid "Components installed successfully."
msgstr "" 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..." msgid "Loading Epic Games Store games..."
msgstr "" msgstr ""
@@ -510,14 +543,21 @@ msgstr ""
msgid "Are you sure you want to clear prefix '{}'?" msgid "Are you sure you want to clear prefix '{}'?"
msgstr "" msgstr ""
#, python-brace-format msgid "Clearing prefix..."
msgid "Prefix '{}' cleared successfully." msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr "" msgstr ""
#, python-brace-format #, python-brace-format
msgid "" msgid "Prefix clear failed with exit code {}."
"Prefix '{}' cleared with errors:\n" msgstr ""
"{}"
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr "" msgstr ""
msgid "Failed to start backup process." msgid "Failed to start backup process."
@@ -702,6 +742,10 @@ msgstr ""
msgid "Error applying theme '{0}'" msgid "Error applying theme '{0}'"
msgstr "" msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH" msgid "LAST LAUNCH"
msgstr "" msgstr ""
@@ -760,6 +804,232 @@ msgstr ""
msgid "File not found: {0}" msgid "File not found: {0}"
msgstr "" 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" msgid "Reboot"
msgstr "" msgstr ""

View File

@@ -9,8 +9,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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: 2025-10-16 14:54+0500\n" "PO-Revision-Date: 2025-11-11 17:00+0500\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language: ru_RU\n" "Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n" "Language-Team: ru_RU <LL@li.org>\n"
@@ -77,10 +77,6 @@ msgstr "Остановлен(а) '{game_name}'"
msgid "Legendary executable not found at {path}" msgid "Legendary executable not found at {path}"
msgstr "Legendary не найден по пути {path}" msgstr "Legendary не найден по пути {path}"
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr "start.sh не найден по адресу {path}"
msgid "Success" msgid "Success"
msgstr "Успешно" msgstr "Успешно"
@@ -127,6 +123,10 @@ msgstr "'{game_name}' был(а) добавлен(а) в избранное"
msgid "Removed '{game_name}' from favorites" msgid "Removed '{game_name}' from favorites"
msgstr "'{game_name}' был(а) удалён(а) из избранного" msgstr "'{game_name}' был(а) удалён(а) из избранного"
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr "start.sh не найден по адресу {path}"
#, python-brace-format #, python-brace-format
msgid "Launch game \"{name}\" with PortProton" msgid "Launch game \"{name}\" with PortProton"
msgstr "Запустить игру \"{name}\" с помощью PortProton" msgstr "Запустить игру \"{name}\" с помощью PortProton"
@@ -372,6 +372,39 @@ msgstr "Установка не удалась. Проверьте журнал
msgid "Components installed successfully." msgid "Components installed successfully."
msgstr "Компоненты успешно установлены." 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..." msgid "Loading Epic Games Store games..."
msgstr "Загрузка игр из Epic Games Store..." msgstr "Загрузка игр из Epic Games Store..."
@@ -519,17 +552,22 @@ msgstr "Подтвердите очистку"
msgid "Are you sure you want to clear prefix '{}'?" msgid "Are you sure you want to clear prefix '{}'?"
msgstr "Вы уверены, что хотите очистить префикс «{}»?" msgstr "Вы уверены, что хотите очистить префикс «{}»?"
#, python-brace-format msgid "Clearing prefix..."
msgid "Prefix '{}' cleared successfully." msgstr "Очистка префикса..."
msgstr "Префикс '{}' успешно удален."
msgid "Failed to start prefix clear process."
msgstr "Не удалось запустить процесс очистки префикса."
msgid "Prefix cleared successfully."
msgstr "Префикс удален успешно."
#, python-brace-format #, python-brace-format
msgid "" msgid "Prefix clear failed with exit code {}."
"Prefix '{}' cleared with errors:\n" msgstr "Очистка префикса завершилась с кодом завершения {}."
"{}"
msgstr "" #, python-brace-format
"Префикс '{}' очищен с ошибками:\n" msgid "Failed to run clear prefix command: {}"
"{}" msgstr "Не удалось выполнить команду очистки префикса: {}"
msgid "Failed to start backup process." msgid "Failed to start backup process."
msgstr "Не удалось запустить процесс резервного копирования." msgstr "Не удалось запустить процесс резервного копирования."
@@ -715,6 +753,10 @@ msgstr "Тема '{0}' применена успешно"
msgid "Error applying theme '{0}'" msgid "Error applying theme '{0}'"
msgstr "Ошибка при применение темы '{0}'" msgstr "Ошибка при применение темы '{0}'"
#, python-brace-format
msgid "Executable not found: {0}"
msgstr "Исполняемый файл не найден: {0}"
msgid "LAST LAUNCH" msgid "LAST LAUNCH"
msgstr "Последний запуск" msgstr "Последний запуск"
@@ -773,6 +815,288 @@ msgstr "Неправильный формат команды (flatpak)"
msgid "File not found: {0}" msgid "File not found: {0}"
msgstr "Файл не найден: {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" msgid "Reboot"
msgstr "Перезагрузить" msgstr "Перезагрузить"

View File

@@ -8,7 +8,7 @@ import psutil
import re import re
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog
from portprotonqt.game_card import GameCard from portprotonqt.game_card import GameCard
from portprotonqt.animations import DetailPageAnimations from portprotonqt.animations import DetailPageAnimations
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
@@ -30,7 +30,7 @@ from portprotonqt.config_utils import (
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config, save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config, save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray, clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray,
read_auto_card_size, save_auto_card_size read_auto_card_size, save_auto_card_size, get_portproton_start_command
) )
from portprotonqt.localization import _, get_egs_language, read_metadata_translations from portprotonqt.localization import _, get_egs_language, read_metadata_translations
from portprotonqt.howlongtobeat_api import HowLongToBeat from portprotonqt.howlongtobeat_api import HowLongToBeat
@@ -74,6 +74,7 @@ class MainWindow(QMainWindow):
self.target_exe = None self.target_exe = None
self.current_running_button = None self.current_running_button = None
self.portproton_location = get_portproton_location() self.portproton_location = get_portproton_location()
self.start_sh = get_portproton_start_command()
self.game_library_manager = GameLibraryManager(self, self.theme, None) self.game_library_manager = GameLibraryManager(self, self.theme, None)
@@ -458,11 +459,11 @@ class MainWindow(QMainWindow):
self.current_install_script = script_name self.current_install_script = script_name
self.seen_progress = False self.seen_progress = False
self.current_percent = 0.0 self.current_percent = 0.0
start_sh = os.path.join(self.portproton_location or "", "data", "scripts", "start.sh") if self.portproton_location else "" start_sh = self.start_sh
if not os.path.exists(start_sh): if not start_sh:
self.installing = False self.installing = False
return return
cmd = [start_sh, "cli", "--autoinstall", script_name] cmd = start_sh + ["cli", "--autoinstall", script_name]
self.install_process = QProcess(self) self.install_process = QProcess(self)
self.install_process.finished.connect(self.on_install_finished) self.install_process.finished.connect(self.on_install_finished)
self.install_process.errorOccurred.connect(self.on_install_error) self.install_process.errorOccurred.connect(self.on_install_error)
@@ -564,6 +565,16 @@ class MainWindow(QMainWindow):
self.game_library_manager.set_games(games) self.game_library_manager.set_games(games)
self.progress_bar.setVisible(False) 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): def open_portproton_forum_topic(self, topic_name: str):
"""Open the PortProton forum topic or search page for this game.""" """Open the PortProton forum topic or search page for this game."""
result = self.portproton_api.get_forum_topic_slug(topic_name) 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.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE)
self.addGameButton.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.addGameButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.addGameButton.clicked.connect(self.openAddGameDialog) 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) self.searchEdit = CustomLineEdit(self, theme=self.theme)
icon: QIcon = cast(QIcon, self.theme_manager.get_icon("search")) icon: QIcon = cast(QIcon, self.theme_manager.get_icon("search"))
@@ -900,6 +920,32 @@ class MainWindow(QMainWindow):
layout.addWidget(self.searchEdit) layout.addWidget(self.searchEdit)
return self.container, 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): def on_search_text_changed(self, text: str):
"""Search text change handler with debounce.""" """Search text change handler with debounce."""
self.searchDebounceTimer.stop() self.searchDebounceTimer.stop()
@@ -1253,7 +1299,15 @@ class MainWindow(QMainWindow):
# Показываем прогресс # Показываем прогресс
self.autoInstallProgress.setVisible(True) self.autoInstallProgress.setVisible(True)
self.autoInstallProgress.setRange(0, 0) self.autoInstallProgress.setRange(0, 0)
self.portproton_api.get_autoinstall_games_async(on_autoinstall_games_loaded)
# Store the thread to prevent premature destruction
self.autoInstallLoadThread = self.portproton_api.start_autoinstall_games_load(on_autoinstall_games_loaded)
# Optional: Clean up thread when finished (prevents leak)
if self.autoInstallLoadThread:
def on_thread_finished():
self.autoInstallLoadThread = None # Release reference
self.autoInstallLoadThread.finished.connect(on_thread_finished)
self.stackedWidget.addWidget(autoInstallPage) self.stackedWidget.addWidget(autoInstallPage)
@@ -1416,12 +1470,10 @@ class MainWindow(QMainWindow):
prefix = self.prefixCombo.currentText() prefix = self.prefixCombo.currentText()
if not wine or not prefix: if not wine or not prefix:
return return
if not self.portproton_location: if not self.portproton_location or not self.start_sh:
return return
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh") start_sh = self.start_sh
if not os.path.exists(start_sh): cmd = start_sh + ["cli", cli_arg, wine, prefix]
return
cmd = [start_sh, "cli", cli_arg, wine, prefix]
# Показываем прогресс-бар перед запуском # Показываем прогресс-бар перед запуском
self.wine_progress_bar.setVisible(True) self.wine_progress_bar.setVisible(True)
@@ -1500,12 +1552,13 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}") QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
def clear_prefix(self): def clear_prefix(self):
"""Очистка префикса (позже удалить).""" """Очищает префикс"""
selected_prefix = self.prefixCombo.currentText() selected_prefix = self.prefixCombo.currentText()
selected_wine = self.wineCombo.currentText() selected_wine = self.wineCombo.currentText()
if not selected_prefix or not selected_wine: if not selected_prefix or not selected_wine:
return return
if not self.portproton_location: if not self.portproton_location or not self.start_sh:
return return
reply = QMessageBox.question( reply = QMessageBox.question(
@@ -1518,98 +1571,35 @@ class MainWindow(QMainWindow):
if reply != QMessageBox.StandardButton.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
prefix_dir = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix) start_sh = self.start_sh
if not os.path.exists(prefix_dir):
self.wine_progress_bar.setVisible(True)
self.update_status_message.emit(_("Clearing prefix..."), 0)
self.clear_process = QProcess(self)
self.clear_process.finished.connect(lambda exitCode, exitStatus: self._on_clear_prefix_finished(exitCode))
self.clear_process.errorOccurred.connect(lambda error: self._on_clear_prefix_error(error))
cmd = start_sh + ["cli", "--clear_pfx", selected_wine, selected_prefix]
self.clear_process.start(cmd[0], cmd[1:])
if not self.clear_process.waitForStarted(5000):
self.wine_progress_bar.setVisible(False)
self.update_status_message.emit("", 0)
QMessageBox.warning(self, _("Error"), _("Failed to start prefix clear process."))
return return
success = True def _on_clear_prefix_finished(self, exitCode):
errors = [] self.wine_progress_bar.setVisible(False)
self.update_status_message.emit("", 0)
# Удаление файлов if exitCode == 0:
files_to_remove = [ QMessageBox.information(self, _("Success"), _("Prefix cleared successfully."))
os.path.join(prefix_dir, "*.dot*"),
os.path.join(prefix_dir, "*.prog*"),
os.path.join(prefix_dir, ".wine_ver"),
os.path.join(prefix_dir, "system.reg"),
os.path.join(prefix_dir, "user.reg"),
os.path.join(prefix_dir, "userdef.reg"),
os.path.join(prefix_dir, "winetricks.log"),
os.path.join(prefix_dir, ".update-timestamp"),
os.path.join(prefix_dir, "drive_c", ".windows-serial"),
]
import glob
for pattern in files_to_remove:
if "*" in pattern: # Глобальный паттерн
matches = glob.glob(pattern)
for file_path in matches:
try:
if os.path.exists(file_path):
os.remove(file_path)
except Exception as e:
success = False
errors.append(str(e))
else: # Конкретный файл
try:
if os.path.exists(pattern):
os.remove(pattern)
except Exception as e:
success = False
errors.append(str(e))
# Удаление директорий
dirs_to_remove = [
os.path.join(prefix_dir, "drive_c", "windows"),
os.path.join(prefix_dir, "drive_c", "ProgramData", "Setup"),
os.path.join(prefix_dir, "drive_c", "ProgramData", "Windows"),
os.path.join(prefix_dir, "drive_c", "ProgramData", "WindowsTask"),
os.path.join(prefix_dir, "drive_c", "ProgramData", "Package Cache"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Microsoft"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Temp"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Temporary Internet Files"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "Microsoft"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "wine_gecko"),
os.path.join(prefix_dir, "drive_c", "users", "Public", "Temp"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Microsoft"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Temp"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Temporary Internet Files"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "Microsoft"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "wine_gecko"),
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Temp"),
os.path.join(prefix_dir, "drive_c", "Program Files", "Internet Explorer"),
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows Media Player"),
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows NT"),
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Internet Explorer"),
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows Media Player"),
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows NT"),
]
import shutil
for dir_path in dirs_to_remove:
try:
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
except Exception as e:
success = False
errors.append(str(e))
tmp_path = os.path.join(self.portproton_location, "data", "tmp")
if os.path.exists(tmp_path):
import glob
bin_files = glob.glob(os.path.join(tmp_path, "*.bin"))
foz_files = glob.glob(os.path.join(tmp_path, "*.foz"))
for file_path in bin_files + foz_files:
try:
os.remove(file_path)
except Exception as e:
success = False
errors.append(str(e))
if success:
QMessageBox.information(self, _("Success"), _("Prefix '{}' cleared successfully.").format(selected_prefix))
else: else:
error_msg = _("Prefix '{}' cleared with errors:\n{}").format(selected_prefix, "\n".join(errors[:5])) QMessageBox.warning(self, _("Error"), _("Prefix clear failed with exit code {}.").format(exitCode))
QMessageBox.warning(self, _("Warning"), error_msg)
def _on_clear_prefix_error(self, error):
self.wine_progress_bar.setVisible(False)
self.update_status_message.emit("", 0)
QMessageBox.warning(self, _("Error"), _("Failed to run clear prefix command: {}").format(error))
def create_prefix_backup(self): def create_prefix_backup(self):
selected_prefix = self.prefixCombo.currentText() selected_prefix = self.prefixCombo.currentText()
@@ -1621,14 +1611,12 @@ class MainWindow(QMainWindow):
def _perform_backup(self, backup_dir, prefix_name): def _perform_backup(self, backup_dir, prefix_name):
os.makedirs(backup_dir, exist_ok=True) os.makedirs(backup_dir, exist_ok=True)
if not self.portproton_location: if not self.portproton_location or not self.start_sh:
return
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
if not os.path.exists(start_sh):
return return
start_sh = self.start_sh
self.backup_process = QProcess(self) self.backup_process = QProcess(self)
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode)) self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
cmd = [start_sh, "--backup-prefix", prefix_name, backup_dir] cmd = start_sh + ["--backup-prefix", prefix_name, backup_dir]
self.backup_process.start(cmd[0], cmd[1:]) self.backup_process.start(cmd[0], cmd[1:])
if not self.backup_process.waitForStarted(): if not self.backup_process.waitForStarted():
QMessageBox.warning(self, _("Error"), _("Failed to start backup process.")) QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
@@ -1641,14 +1629,12 @@ class MainWindow(QMainWindow):
def _perform_restore(self, file_path): def _perform_restore(self, file_path):
if not file_path or not os.path.exists(file_path): if not file_path or not os.path.exists(file_path):
return return
if not self.portproton_location: if not self.portproton_location or not self.start_sh:
return
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
if not os.path.exists(start_sh):
return return
start_sh = self.start_sh
self.restore_process = QProcess(self) self.restore_process = QProcess(self)
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode)) self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
cmd = [start_sh, "--restore-prefix", file_path] cmd = start_sh + ["--restore-prefix", file_path]
self.restore_process.start(cmd[0], cmd[1:]) self.restore_process.start(cmd[0], cmd[1:])
if not self.restore_process.waitForStarted(): if not self.restore_process.waitForStarted():
QMessageBox.warning(self, _("Error"), _("Failed to start restore process.")) QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
@@ -2318,6 +2304,14 @@ class MainWindow(QMainWindow):
def darkenColor(self, color, factor=200): def darkenColor(self, color, factor=200):
return color.darker(factor) return color.darker(factor)
def open_exe_settings(self, exe_path):
"""Open the ExeSettingsDialog for the given executable."""
if not os.path.exists(exe_path):
QMessageBox.warning(self, _("Error"), _("Executable not found: {0}").format(exe_path))
return
dialog = ExeSettingsDialog(self, self.theme, exe_path)
dialog.exec()
def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""): def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""):
detailPage = QWidget() detailPage = QWidget()
self._animations = {} self._animations = {}
@@ -2620,8 +2614,6 @@ class MainWindow(QMainWindow):
clear_layout(hltbLayout) clear_layout(hltbLayout)
has_data = False has_data = False
if main_story_time is not None: if main_story_time is not None:
@@ -2695,6 +2687,7 @@ class MainWindow(QMainWindow):
file_to_check = entry_exec_split[0] file_to_check = entry_exec_split[0]
current_exe = os.path.basename(file_to_check) if file_to_check else None 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: if self.target_exe is not None and current_exe == self.target_exe:
playButton = AutoSizeButton(_("Stop"), icon=self.theme_manager.get_icon("stop")) playButton = AutoSizeButton(_("Stop"), icon=self.theme_manager.get_icon("stop"))
else: else:
@@ -2703,7 +2696,17 @@ class MainWindow(QMainWindow):
playButton.setFixedSize(120, 40) playButton.setFixedSize(120, 40)
playButton.setStyleSheet(self.theme.PLAY_BUTTON_STYLE) playButton.setStyleSheet(self.theme.PLAY_BUTTON_STYLE)
playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton)) playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton))
detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft) 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) contentFrameLayout.addWidget(detailsWidget)
mainLayout.addStretch() mainLayout.addStretch()
@@ -2927,10 +2930,7 @@ class MainWindow(QMainWindow):
env_vars = os.environ.copy() env_vars = os.environ.copy()
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
wrapper = "flatpak run ru.linux_gaming.PortProton" wrapper = self.start_sh or ""
if self.portproton_location is not None and ".var" not in self.portproton_location:
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
wrapper = start_sh
cmd = [wrapper, game_exe] cmd = [wrapper, game_exe]
@@ -3024,13 +3024,6 @@ class MainWindow(QMainWindow):
exe_name = os.path.splitext(current_exe)[0] exe_name = os.path.splitext(current_exe)[0]
env_vars = os.environ.copy() env_vars = os.environ.copy()
if entry_exec_split[0] == "env" and len(entry_exec_split) > 1 and 'data/scripts/start.sh' in entry_exec_split[1]:
env_vars['START_FROM_STEAM'] = '1'
env_vars['PROCESS_LOG'] = '1'
elif entry_exec_split[0] == "flatpak":
env_vars['START_FROM_STEAM'] = '1'
env_vars['PROCESS_LOG'] = '1'
# Запускаем игру # Запускаем игру
try: try:
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid) process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)

View File

@@ -6,13 +6,16 @@ import urllib.parse
import time import time
import glob import glob
import re import re
import hashlib
from collections.abc import Callable from collections.abc import Callable
from PySide6.QtCore import QThread, Signal
from portprotonqt.downloader import Downloader from portprotonqt.downloader import Downloader
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.config_utils import get_portproton_location from portprotonqt.config_utils import get_portproton_location
logger = get_logger(__name__) logger = get_logger(__name__)
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
AUTOINSTALL_CACHE_DURATION = 3600 # 1 hour for autoinstall cache
def normalize_name(s): def normalize_name(s):
""" """
@@ -59,6 +62,7 @@ class PortProtonAPI:
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data") self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
self._topics_data = None self._topics_data = None
self._autoinstall_cache = None # New: In-memory cache
def _get_game_dir(self, exe_name: str) -> str: def _get_game_dir(self, exe_name: str) -> str:
game_dir = os.path.join(self.custom_data_dir, exe_name) game_dir = os.path.join(self.custom_data_dir, exe_name)
@@ -231,67 +235,139 @@ class PortProtonAPI:
logger.error(f"Failed to parse {file_path}: {e}") logger.error(f"Failed to parse {file_path}: {e}")
return None, None return None, None
def get_autoinstall_games_async(self, callback: Callable[[list[tuple]], None]) -> None: def _compute_scripts_signature(self, auto_dir: str) -> str:
"""Load auto-install games with user/builtin covers (no async download here).""" """Compute a hash-based signature of the autoinstall scripts to detect changes."""
games = []
auto_dir = os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall") if self.portproton_location else ""
if not os.path.exists(auto_dir): if not os.path.exists(auto_dir):
callback(games) return ""
return
scripts = sorted(glob.glob(os.path.join(auto_dir, "*"))) scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
if not scripts: # Simple hash: concatenate sorted filenames and hash
callback(games) filenames_str = "".join(sorted([os.path.basename(s) for s in scripts]))
return return hashlib.md5(filenames_str.encode()).hexdigest()
xdg_data_home = os.getenv("XDG_DATA_HOME", def _load_autoinstall_cache(self):
os.path.join(os.path.expanduser("~"), ".local", "share")) """Load cached autoinstall games if fresh and scripts unchanged."""
base_autoinstall_dir = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall") if self._autoinstall_cache is not None:
os.makedirs(base_autoinstall_dir, exist_ok=True) return self._autoinstall_cache
cache_dir = get_cache_dir()
cache_file = os.path.join(cache_dir, "autoinstall_games_cache.json")
if os.path.exists(cache_file):
try:
mod_time = os.path.getmtime(cache_file)
if time.time() - mod_time < AUTOINSTALL_CACHE_DURATION:
with open(cache_file, "rb") as f:
data = orjson.loads(f.read())
# Check signature
cached_signature = data.get("scripts_signature", "")
current_signature = self._compute_scripts_signature(
os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
)
if cached_signature != current_signature:
logger.info("Scripts signature mismatch; invalidating cache")
return None
self._autoinstall_cache = data["games"]
logger.info(f"Loaded {len(self._autoinstall_cache)} cached autoinstall games")
return self._autoinstall_cache
except Exception as e:
logger.error(f"Failed to load autoinstall cache: {e}")
return None
for script_path in scripts: def _save_autoinstall_cache(self, games):
display_name, exe_name = self.parse_autoinstall_script(script_path) """Save parsed autoinstall games to cache with scripts signature."""
script_name = os.path.splitext(os.path.basename(script_path))[0] try:
cache_dir = get_cache_dir()
cache_file = os.path.join(cache_dir, "autoinstall_games_cache.json")
auto_dir = os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
scripts_signature = self._compute_scripts_signature(auto_dir)
data = {"games": games, "scripts_signature": scripts_signature, "timestamp": time.time()}
with open(cache_file, "wb") as f:
f.write(orjson.dumps(data))
logger.debug(f"Saved {len(games)} autoinstall games to cache with signature {scripts_signature}")
except Exception as e:
logger.error(f"Failed to save autoinstall cache: {e}")
if not (display_name and exe_name): def start_autoinstall_games_load(self, callback: Callable[[list[tuple]], None]) -> QThread | None:
continue """Start loading auto-install games in a background thread. Returns the thread for management."""
# Check cache first (sync, fast)
cached_games = self._load_autoinstall_cache()
if cached_games is not None:
# Emit via callback immediately if cached
QThread.msleep(0) # Yield to Qt event loop
callback(cached_games)
return None # No thread needed
exe_name = os.path.splitext(exe_name)[0] # Без .exe # No cache: Start background thread
user_game_folder = os.path.join(base_autoinstall_dir, exe_name) class AutoinstallWorker(QThread):
os.makedirs(user_game_folder, exist_ok=True) finished = Signal(list)
api: "PortProtonAPI"
portproton_location: str | None
# Поиск обложки def run(self):
cover_path = "" games = []
user_files = set(os.listdir(user_game_folder)) if os.path.exists(user_game_folder) else set() auto_dir = os.path.join(
for ext in [".jpg", ".png", ".jpeg", ".bmp"]: self.portproton_location or "", "data", "scripts", "pw_autoinstall"
candidate = f"cover{ext}" ) if self.portproton_location else ""
if candidate in user_files: if not os.path.exists(auto_dir):
cover_path = os.path.join(user_game_folder, candidate) self.finished.emit(games)
break return
if not cover_path: scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
logger.debug(f"No local cover found for autoinstall {exe_name}") if not scripts:
self.finished.emit(games)
return
# Формируем кортеж игры (добавлен exe_name в конец) xdg_data_home = os.getenv(
game_tuple = ( "XDG_DATA_HOME",
display_name, # name os.path.join(os.path.expanduser("~"), ".local", "share"),
"", # description )
cover_path, # cover base_autoinstall_dir = os.path.join(
"", # appid xdg_data_home, "PortProtonQt", "custom_data", "autoinstall"
f"autoinstall:{script_name}", # exec_line )
"", # controller_support os.makedirs(base_autoinstall_dir, exist_ok=True)
"Never", # last_launch
"0h 0m", # formatted_playtime
"", # protondb_tier
"", # anticheat_status
0, # last_played
0, # playtime_seconds
"autoinstall", # game_source
exe_name # exe_name
)
games.append(game_tuple)
callback(games) for script_path in scripts:
display_name, exe_name = self.api.parse_autoinstall_script(script_path)
script_name = os.path.splitext(os.path.basename(script_path))[0]
if not (display_name and exe_name):
continue
exe_name = os.path.splitext(exe_name)[0]
user_game_folder = os.path.join(base_autoinstall_dir, exe_name)
os.makedirs(user_game_folder, exist_ok=True)
# Find cover
cover_path = ""
user_files = (
set(os.listdir(user_game_folder))
if os.path.exists(user_game_folder)
else set()
)
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
candidate = f"cover{ext}"
if candidate in user_files:
cover_path = os.path.join(user_game_folder, candidate)
break
if not cover_path:
logger.debug(f"No local cover found for autoinstall {exe_name}")
game_tuple = (
display_name, "", cover_path, "", f"autoinstall:{script_name}",
"", "Never", "0h 0m", "", "", 0, 0, "autoinstall", exe_name
)
games.append(game_tuple)
self.api._save_autoinstall_cache(games)
self.api._autoinstall_cache = games
self.finished.emit(games)
worker = AutoinstallWorker()
worker.api = self
worker.portproton_location = self.portproton_location
worker.finished.connect(lambda games: callback(games))
worker.start()
logger.info("Started background load of autoinstall games")
return worker
def _load_topics_data(self): def _load_topics_data(self):
"""Load and cache linux_gaming_topics_min.json from the archive.""" """Load and cache linux_gaming_topics_min.json from the archive."""

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.localization import get_steam_language
from portprotonqt.downloader import Downloader from portprotonqt.downloader import Downloader
from portprotonqt.dialogs import generate_thumbnail from portprotonqt.dialogs import generate_thumbnail
from portprotonqt.config_utils import get_portproton_location from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
from collections.abc import Callable from collections.abc import Callable
import re import re
import shutil import shutil
@@ -23,6 +23,7 @@ import requests
import random import random
import base64 import base64
import glob import glob
import urllib.parse
downloader = Downloader() downloader = Downloader()
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -411,6 +412,39 @@ def save_app_details(app_id, data):
with open(cache_file, "wb") as f: with open(cache_file, "wb") as f:
f.write(orjson.dumps(data)) 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]): def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]):
""" """
Asynchronously fetches detailed app info from Steam API. 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", "")) title = decode_text(app_info.get("name", ""))
description = decode_text(app_info.get("short_description", "")) description = decode_text(app_info.get("short_description", ""))
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg" 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_protondb_tier(tier: str):
def on_anticheat_status(anticheat_status: 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() game_name = desktop_name or exe_name.capitalize()
if not matching_app: 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): def on_anticheat_status(anticheat_status: str):
callback({ callback({
"appid": "", "appid": "",
"name": decode_text(game_name), "name": decode_text(game_name),
"description": "", "description": "",
"cover": "", "cover": cover,
"controller_support": "", "controller_support": "",
"protondb_tier": "", "protondb_tier": "",
"steam_game": "false", "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)) title = decode_text(app_info.get("name", game_name))
description = decode_text(app_info.get("short_description", "")) description = decode_text(app_info.get("short_description", ""))
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg" 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", "") controller_support = app_info.get("controller_support", "")
def on_protondb_tier(tier: str): 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}") return (False, f"Executable file not found: {exe_path}")
portproton_dir = get_portproton_location() portproton_dir = get_portproton_location()
if not portproton_dir: start_sh = get_portproton_start_command()
if not portproton_dir or not start_sh:
logger.error("PortProton directory not found") logger.error("PortProton directory not found")
return (False, "PortProton directory not found") return (False, "PortProton directory not found")
@@ -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()) safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh") script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
if not os.path.exists(start_sh_path):
logger.error(f"start.sh not found at {start_sh_path}")
return (False, f"start.sh not found at {start_sh_path}")
if not os.path.exists(script_path): if not os.path.exists(script_path):
script_content = f"""#!/usr/bin/env bash script_content = f"""#!/usr/bin/env bash
export LD_PRELOAD= export LD_PRELOAD=
export START_FROM_STEAM=1 export START_FROM_STEAM=1
"{start_sh_path}" "{exe_path}" "$@" "{start_sh}" "{exe_path}" "$@"
""" """
try: try:
with open(script_path, "w", encoding="utf-8") as f: with open(script_path, "w", encoding="utf-8") as f:

View File

@@ -5,6 +5,9 @@ from portprotonqt.logger import get_logger
from PySide6.QtGui import QIcon, QFontDatabase, QPixmap from PySide6.QtGui import QIcon, QFontDatabase, QPixmap
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
# Icon caching for performance optimization
_icon_cache = {}
logger = get_logger(__name__) logger = get_logger(__name__)
# Папка, где располагаются все дополнительные темы # Папка, где располагаются все дополнительные темы
@@ -232,6 +235,14 @@ class ThemeManager:
а если файл не найден, то из стандартной темы. а если файл не найден, то из стандартной темы.
Если as_path=True, возвращает путь к иконке вместо QIcon. Если 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 icon_path = None
theme_name = theme_name or self.current_theme_name theme_name = theme_name or self.current_theme_name
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg'] supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
@@ -279,12 +290,20 @@ class ThemeManager:
# Если иконка всё равно не найдена # Если иконка всё равно не найдена
if not icon_path or not os.path.exists(icon_path): if not icon_path or not os.path.exists(icon_path):
logger.error(f"Warning: icon '{icon_name}' not found") 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: if as_path:
# Cache the path
_icon_cache[cache_key] = icon_path
return 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): 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""" WINETRICKS_TAB_STYLE = f"""
QTabWidget::pane {{ QTabWidget::pane {{
border: 1px solid {color_d}; border-top: 1px solid {color_c};
background: {color_b}; background: {color_h};
border-radius: {border_radius_a};
}} }}
QTabBar::tab {{ QTabBar::tab {{
background: {color_c}; background: {color_c};
@@ -985,15 +984,113 @@ QTabBar::tab:selected {{
color: {color_f}; color: {color_f};
}} }}
QTabBar::tab:hover {{ QTabBar::tab:hover {{
background: {color_e}; background: {color_a};
}} }}
""" """
WINETRICKS_TABBLE_STYLE = f""" WINETRICKS_TABBLE_STYLE = f"""
QTableWidget {{ QComboBox {{
background: {color_c}; background: {color_c};
border: {border_c} {color_g};
border-radius: {border_radius_a};
padding-left: 12px;
color: {color_f}; 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}; alternate-background-color: {color_d};
border: {border_a}; border: {border_a};
border-radius: {border_radius_a}; border-radius: {border_radius_a};
@@ -1009,39 +1106,68 @@ QHeaderView::section {{
}} }}
QTableWidget::item {{ QTableWidget::item {{
padding: 8px; 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}; background: {color_a};
color: {color_f}; color: {color_f};
selection-background-color: {color_a};
}} }}
QTableWidget::item:hover {{ QTableWidget::item:hover {{
background: {color_e}; background: {color_h};
}} }}
QTableWidget::indicator {{ QTableWidget::indicator {{
width: 24px; width: 24px;
height: 24px; height: 24px;
border: {border_b} {color_a}; border: {border_c} {color_h};
border-radius: {border_radius_a}; border-radius: {border_radius_a};
background: rgba(255, 255, 255, 0.1); background: {color_b};
}} }}
QTableWidget::indicator:unchecked {{ QTableWidget::indicator:unchecked {{
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
image: none; image: none;
}} }}
QTableWidget::indicator:checked {{ QTableWidget::indicator:checked {{
background: {color_a}; background: {color_b};
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)}); 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 {{ QTableWidget::indicator:hover {{
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
border: {border_b} {color_a};
}}
QTableWidget::indicator:focus {{
border: {border_c} {color_a}; 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""" WINETRICKS_LOG_STYLE = f"""

View File

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

View File

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

976
uv.lock generated

File diff suppressed because it is too large Load Diff