Compare commits
40 Commits
v0.1.8
...
80a2c06b5a
| Author | SHA1 | Date | |
|---|---|---|---|
|
80a2c06b5a
|
|||
|
f0a4ace735
|
|||
|
7dfaee6831
|
|||
|
5481cd80d7
|
|||
|
a016cfa810
|
|||
|
8fc097ccaf
|
|||
|
ad3eeb6e06
|
|||
|
92631cd2c6
|
|||
|
4477679f2d
|
|||
|
|
b6644eeee5
|
||
|
|
2e921226c4
|
||
|
|
4fc1ea73d3
|
||
|
|
3c15cbe495
|
||
|
fed6aafed5
|
|||
|
2e8be13437
|
|||
|
ea272c29b6
|
|||
|
17262f6c9f
|
|||
|
e07f3f06bc
|
|||
|
16a3f4e09a
|
|||
|
a448ba29b0
|
|||
|
06e55db54d
|
|||
|
5fce23f261
|
|||
|
|
96ad40d625 | ||
|
|
a30f6f2e74 | ||
|
0231073b19
|
|||
|
dec24429f5
|
|||
|
4a758f3b3c
|
|||
|
0853dd1579
|
|||
|
bbb87c0455
|
|||
|
b32a71a125
|
|||
|
|
bddf9f850a | ||
|
|
a9c3cfa167 | ||
|
7675bc4cdc
|
|||
|
ffa203f019
|
|||
|
3eed25ecee
|
|||
|
3736bb279e
|
|||
|
|
b59ee5ae8e | ||
|
33176590fd
|
|||
|
8046065929
|
|||
|
|
fbad5add6c |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
1
build-aux/lib/udev/rules.d/60-portprotonqt.rules
Normal file
1
build-aux/lib/udev/rules.d/60-portprotonqt.rules
Normal file
@@ -0,0 +1 @@
|
|||||||
|
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"
|
||||||
@@ -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.
12972
data/games_appid.json
12972
data/games_appid.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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.
@@ -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:
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -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 ""
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -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 "Перезагрузить"
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
208
portprotonqt/settings_manager.py
Normal file
208
portprotonqt/settings_manager.py
Normal 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',
|
||||||
|
]
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
1
portprotonqt/themes/standart/images/icons/settings.svg
Normal file
1
portprotonqt/themes/standart/images/icons/settings.svg
Normal 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 |
@@ -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"""
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"pre-commit": {
|
"pre-commit": {
|
||||||
"enabled": true
|
"enabled": false
|
||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user