Compare commits
60 Commits
84d5e46a74
...
main
| 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 | ||
|
438e9737ea
|
|||
|
2d39a4c740
|
|||
|
567203b0b0
|
|||
|
502cbc5030
|
|||
|
9b61215152
|
|||
|
10d3fe8ab4
|
|||
|
a568ad9ef8
|
|||
|
f074843fc8
|
|||
|
4ab078b93e
|
|||
|
7df6ad3b80
|
|||
|
464ad0fe9c
|
|||
|
cde92885d4
|
|||
|
120c7b319c
|
|||
|
596aed0077
|
|||
|
6fc6cb1e02
|
|||
|
186e28a19b
|
|||
|
28e4d1e77c
|
|||
|
fff1f888c4
|
|||
|
fdd5a0a3d5
|
|||
|
792e52d981
|
@@ -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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
# Common version, will be used for tagging the release
|
# Common version, will be used for tagging the release
|
||||||
VERSION: 0.1.7
|
VERSION: 0.1.8
|
||||||
PKGDEST: "/tmp/portprotonqt"
|
PKGDEST: "/tmp/portprotonqt"
|
||||||
PACKAGE: "portprotonqt"
|
PACKAGE: "portprotonqt"
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
@@ -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
|
||||||
@@ -188,4 +188,4 @@ jobs:
|
|||||||
tag_name: v${{ env.VERSION }}
|
tag_name: v${{ env.VERSION }}
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: release/**/*
|
files: release/**/*
|
||||||
sha256sum: true
|
sha256sum: false
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
10
CHANGELOG.md
@@ -3,16 +3,24 @@
|
|||||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [0.1.8] - 2025-10-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
|
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
|
||||||
|
- В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет
|
||||||
|
- К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада
|
||||||
|
- Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- При завершении автоустановки приложение больше не перезапускается
|
- При завершении автоустановки приложение больше не перезапускается
|
||||||
|
- Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название
|
||||||
|
- Обновлены и дополнены скриншоты темы
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Исправлено наложение карточек при смене фильтра игр
|
- Исправлено наложение карточек при смене фильтра игр
|
||||||
|
- Исправлена невозможность запуска приложения без подключёного геймпада
|
||||||
|
- Исправлена невозможность установки компонентов Winetricks через геймпад
|
||||||
|
- Ресиверы и виртуальные устройства больше не считаются за геймпад
|
||||||
|
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -36,7 +37,7 @@ AppDir:
|
|||||||
id: ru.linux_gaming.PortProtonQt
|
id: ru.linux_gaming.PortProtonQt
|
||||||
name: PortProtonQt
|
name: PortProtonQt
|
||||||
icon: ru.linux_gaming.PortProtonQt
|
icon: ru.linux_gaming.PortProtonQt
|
||||||
version: 0.1.7
|
version: 0.1.8
|
||||||
exec: usr/bin/python3
|
exec: usr/bin/python3
|
||||||
exec_args: "-m portprotonqt.app $@"
|
exec_args: "-m portprotonqt.app $@"
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=portprotonqt
|
pkgname=portprotonqt
|
||||||
pkgver=0.1.7
|
pkgver=0.1.8
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
%global pypi_name portprotonqt
|
%global pypi_name portprotonqt
|
||||||
%global pypi_version 0.1.7
|
%global pypi_version 0.1.8
|
||||||
%global oname PortProtonQt
|
%global oname PortProtonQt
|
||||||
%global _python_no_extras_requires 1
|
%global _python_no_extras_requires 1
|
||||||
|
|
||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
12972
data/games_appid.json
@@ -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"
|
||||||
|
|||||||
@@ -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 241 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 323 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 241 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 323 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 241 of 241 |
|
| [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 из 241 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 323 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 241 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 323 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 241 из 241 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 323 из 323 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
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
|
||||||
|
|
||||||
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
||||||
__app_name__ = "PortProtonQt"
|
__app_name__ = "PortProtonQt"
|
||||||
__app_version__ = "0.1.7"
|
__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,30 @@ 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():
|
||||||
|
"""Reads the card size (width) for Auto Install from the [Cards] section.
|
||||||
|
Returns 250 if the parameter is not set.
|
||||||
|
"""
|
||||||
|
cp = read_config_safely(CONFIG_FILE)
|
||||||
|
if cp is None or not cp.has_section("Cards") or not cp.has_option("Cards", "auto_card_width"):
|
||||||
|
save_auto_card_size(250)
|
||||||
|
return 250
|
||||||
|
return cp.getint("Cards", "auto_card_width", fallback=250)
|
||||||
|
|
||||||
|
def save_auto_card_size(card_width):
|
||||||
|
"""Saves the card size (width) for Auto Install to the [Cards] section."""
|
||||||
|
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
|
||||||
|
if "Cards" not in cp:
|
||||||
|
cp["Cards"] = {}
|
||||||
|
cp["Cards"]["auto_card_width"] = str(card_width)
|
||||||
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
|
|
||||||
def read_sort_method():
|
def read_sort_method():
|
||||||
"""Reads the sort method from the [Games] section.
|
"""Reads the sort method from the [Games] section.
|
||||||
@@ -195,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.
|
||||||
@@ -214,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.
|
||||||
@@ -239,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.
|
||||||
@@ -258,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.
|
||||||
@@ -277,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.
|
||||||
@@ -321,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.
|
||||||
@@ -340,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.
|
||||||
@@ -361,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.
|
||||||
@@ -370,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}")
|
||||||
|
|
||||||
@@ -384,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.
|
||||||
@@ -402,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.
|
||||||
@@ -427,3 +541,26 @@ 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():
|
||||||
|
"""Reads the minimize-to-tray setting from the [Display] section.
|
||||||
|
Returns True if the parameter is missing (default: minimize to tray).
|
||||||
|
"""
|
||||||
|
cp = read_config_safely(CONFIG_FILE)
|
||||||
|
if cp is None or not cp.has_section("Display") or not cp.has_option("Display", "minimize_to_tray"):
|
||||||
|
save_minimize_to_tray(True)
|
||||||
|
return True
|
||||||
|
return cp.getboolean("Display", "minimize_to_tray", fallback=True)
|
||||||
|
|
||||||
|
def save_minimize_to_tray(minimize_to_tray):
|
||||||
|
"""Saves the minimize-to-tray setting to the [Display] section."""
|
||||||
|
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
|
||||||
|
if "Display" not in cp:
|
||||||
|
cp["Display"] = {}
|
||||||
|
cp["Display"]["minimize_to_tray"] = str(minimize_to_tray)
|
||||||
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -91,6 +91,130 @@ def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
|||||||
logger.error(f"Ошибка при сохранении миниатюры: {e}")
|
logger.error(f"Ошибка при сохранении миниатюры: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def create_dialog_hints_widget(theme, main_window, input_manager, context='default'):
|
||||||
|
"""
|
||||||
|
Common function to create hints widget for all dialogs.
|
||||||
|
Uses main_window for get_button_icon/get_nav_icon, input_manager for gamepad detection.
|
||||||
|
"""
|
||||||
|
theme_manager = ThemeManager()
|
||||||
|
current_theme_name = read_theme_from_config()
|
||||||
|
|
||||||
|
hintsWidget = QWidget()
|
||||||
|
hintsWidget.setStyleSheet(theme.STATUS_BAR_STYLE)
|
||||||
|
hintsLayout = QHBoxLayout(hintsWidget)
|
||||||
|
hintsLayout.setContentsMargins(10, 0, 10, 0)
|
||||||
|
hintsLayout.setSpacing(20)
|
||||||
|
|
||||||
|
dialog_actions = []
|
||||||
|
|
||||||
|
# Context-specific actions (gamepad only, no keyboard)
|
||||||
|
if context == 'file_explorer':
|
||||||
|
dialog_actions = [
|
||||||
|
("confirm", _("Open")), # A / Cross
|
||||||
|
("add_game", _("Select Dir")), # X / Triangle
|
||||||
|
("prev_dir", _("Prev Dir")), # Y / Square
|
||||||
|
("back", _("Cancel")), # B / Circle
|
||||||
|
("context_menu", _("Menu")), # Start / Options
|
||||||
|
]
|
||||||
|
elif context == 'winetricks':
|
||||||
|
dialog_actions = [
|
||||||
|
("confirm", _("Toggle")), # A / Cross
|
||||||
|
("add_game", _("Install")), # X / Triangle
|
||||||
|
("prev_dir", _("Force Install")), # Y / Square
|
||||||
|
("back", _("Cancel")), # B / Circle
|
||||||
|
("prev_tab", _("Prev Tab")), # LB / L1
|
||||||
|
("next_tab", _("Next Tab")), # RB / R1
|
||||||
|
]
|
||||||
|
|
||||||
|
hints_labels = [] # Store for updates (returned for class storage)
|
||||||
|
|
||||||
|
def make_hint(icon_name, text, action=None):
|
||||||
|
container = QWidget()
|
||||||
|
hlayout = QHBoxLayout(container)
|
||||||
|
hlayout.setContentsMargins(0, 5, 0, 0)
|
||||||
|
hlayout.setSpacing(6)
|
||||||
|
|
||||||
|
icon_label = QLabel()
|
||||||
|
icon_label.setFixedSize(26, 26)
|
||||||
|
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
pixmap = QPixmap()
|
||||||
|
icon_path = theme_manager.get_theme_image(icon_name, current_theme_name)
|
||||||
|
if icon_path:
|
||||||
|
pixmap.load(str(icon_path))
|
||||||
|
if not pixmap.isNull():
|
||||||
|
icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||||||
|
|
||||||
|
hlayout.addWidget(icon_label)
|
||||||
|
|
||||||
|
text_label = QLabel(text)
|
||||||
|
text_label.setStyleSheet(theme.LAST_LAUNCH_VALUE_STYLE)
|
||||||
|
text_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
|
||||||
|
hlayout.addWidget(text_label)
|
||||||
|
|
||||||
|
# Initially hidden; show only if gamepad connected
|
||||||
|
container.setVisible(False)
|
||||||
|
hints_labels.append((container, icon_label, action))
|
||||||
|
|
||||||
|
hintsLayout.addWidget(container)
|
||||||
|
|
||||||
|
# Add gamepad hints only
|
||||||
|
for action, text in dialog_actions:
|
||||||
|
make_hint("placeholder", text, action)
|
||||||
|
|
||||||
|
hintsLayout.addStretch()
|
||||||
|
|
||||||
|
# Return widget and labels for class storage
|
||||||
|
return hintsWidget, hints_labels
|
||||||
|
|
||||||
|
def update_dialog_hints(hints_labels, main_window, input_manager, theme_manager, current_theme_name):
|
||||||
|
"""
|
||||||
|
Common function to update hints for any dialog.
|
||||||
|
"""
|
||||||
|
if not input_manager or not main_window:
|
||||||
|
# Hide all if no input_manager or main_window
|
||||||
|
for container, _, _ in hints_labels:
|
||||||
|
container.setVisible(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
is_gamepad = input_manager.gamepad is not None
|
||||||
|
if not is_gamepad:
|
||||||
|
# Hide all hints if no gamepad
|
||||||
|
for container, _, _ in hints_labels:
|
||||||
|
container.setVisible(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
gtype = input_manager.gamepad_type
|
||||||
|
gamepad_actions = ['confirm', 'back', 'context_menu', 'add_game', 'prev_dir', 'prev_tab', 'next_tab']
|
||||||
|
|
||||||
|
for container, icon_label, action in hints_labels:
|
||||||
|
if action and action in gamepad_actions:
|
||||||
|
container.setVisible(True)
|
||||||
|
# Update icon using main_window methods
|
||||||
|
if action in ['confirm', 'back', 'context_menu', 'add_game', 'prev_dir']:
|
||||||
|
icon_name = main_window.get_button_icon(action, gtype)
|
||||||
|
else: # only prev_tab/next_tab (treat as nav)
|
||||||
|
direction = 'left' if action == 'prev_tab' else 'right'
|
||||||
|
icon_name = main_window.get_nav_icon(direction, gtype)
|
||||||
|
icon_path = theme_manager.get_theme_image(icon_name, current_theme_name)
|
||||||
|
pixmap = QPixmap()
|
||||||
|
if icon_path:
|
||||||
|
pixmap.load(str(icon_path))
|
||||||
|
if not pixmap.isNull():
|
||||||
|
icon_label.setPixmap(pixmap.scaled(
|
||||||
|
26, 26,
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
# Fallback to placeholder
|
||||||
|
placeholder = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||||
|
if placeholder:
|
||||||
|
pixmap.load(str(placeholder))
|
||||||
|
icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||||||
|
else:
|
||||||
|
container.setVisible(False)
|
||||||
|
|
||||||
class FileSelectedSignal(QObject):
|
class FileSelectedSignal(QObject):
|
||||||
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
||||||
|
|
||||||
@@ -185,6 +309,7 @@ class FileExplorer(QDialog):
|
|||||||
self.initial_path = initial_path # Store initial path if provided
|
self.initial_path = initial_path # Store initial path if provided
|
||||||
self.thumbnail_cache = {} # Cache for loaded thumbnails
|
self.thumbnail_cache = {} # Cache for loaded thumbnails
|
||||||
self.pending_thumbnails = set() # Track files pending thumbnail loading
|
self.pending_thumbnails = set() # Track files pending thumbnail loading
|
||||||
|
self.main_window = None # Add reference to MainWindow
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
# Window settings
|
# Window settings
|
||||||
@@ -198,6 +323,7 @@ class FileExplorer(QDialog):
|
|||||||
while parent:
|
while parent:
|
||||||
if hasattr(parent, 'input_manager'):
|
if hasattr(parent, 'input_manager'):
|
||||||
self.input_manager = cast("MainWindow", parent).input_manager
|
self.input_manager = cast("MainWindow", parent).input_manager
|
||||||
|
self.main_window = parent
|
||||||
if hasattr(parent, 'context_menu_manager'):
|
if hasattr(parent, 'context_menu_manager'):
|
||||||
self.context_menu_manager = cast("MainWindow", parent).context_menu_manager
|
self.context_menu_manager = cast("MainWindow", parent).context_menu_manager
|
||||||
parent = parent.parent()
|
parent = parent.parent()
|
||||||
@@ -214,6 +340,17 @@ class FileExplorer(QDialog):
|
|||||||
self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid
|
self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid
|
||||||
self.update_file_list()
|
self.update_file_list()
|
||||||
|
|
||||||
|
# Create hints widget using common function
|
||||||
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
self.hints_widget, self.hints_labels = create_dialog_hints_widget(self.theme, self.main_window, self.input_manager, context='file_explorer')
|
||||||
|
self.main_layout.addWidget(self.hints_widget)
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.button_event.connect(lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name))
|
||||||
|
self.input_manager.dpad_moved.connect(lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name))
|
||||||
|
update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
|
||||||
class ThumbnailLoader(QRunnable):
|
class ThumbnailLoader(QRunnable):
|
||||||
"""Class for asynchronous thumbnail loading in a separate thread."""
|
"""Class for asynchronous thumbnail loading in a separate thread."""
|
||||||
class Signals(QObject):
|
class Signals(QObject):
|
||||||
@@ -897,8 +1034,8 @@ class AddGameDialog(QDialog):
|
|||||||
"""Обработчик выбора файла в FileExplorer"""
|
"""Обработчик выбора файла в FileExplorer"""
|
||||||
self.exeEdit.setText(file_path)
|
self.exeEdit.setText(file_path)
|
||||||
self.last_exe_path = file_path # Update last selected exe path
|
self.last_exe_path = file_path # Update last selected exe path
|
||||||
if not self.edit_mode:
|
if not self.edit_mode and not self.nameEdit.text().strip():
|
||||||
# Автоматически заполняем имя игры, если не в режиме редактирования
|
# Автоматически заполняем имя игры, если не в режиме редактирования или если оно не введено вручную
|
||||||
game_name = os.path.splitext(os.path.basename(file_path))[0]
|
game_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
self.nameEdit.setText(game_name)
|
self.nameEdit.setText(game_name)
|
||||||
|
|
||||||
@@ -1037,8 +1174,6 @@ Icon={icon_path}
|
|||||||
return desktop_entry, desktop_path
|
return desktop_entry, desktop_path
|
||||||
|
|
||||||
class WinetricksDialog(QDialog):
|
class WinetricksDialog(QDialog):
|
||||||
"""Dialog for managing Winetricks components in a prefix."""
|
|
||||||
|
|
||||||
def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None):
|
def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
@@ -1071,6 +1206,36 @@ class WinetricksDialog(QDialog):
|
|||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.load_lists()
|
self.load_lists()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
# Enable Winetricks-specific mode
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.enable_winetricks_mode(self)
|
||||||
|
|
||||||
|
# Create hints widget using common function
|
||||||
|
self.hints_widget, self.hints_labels = create_dialog_hints_widget(self.theme, self.main_window, self.input_manager, context='winetricks')
|
||||||
|
self.main_layout.addWidget(self.hints_widget)
|
||||||
|
|
||||||
|
# Connect signals (use self.theme_manager)
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.button_event.connect(
|
||||||
|
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
)
|
||||||
|
self.input_manager.dpad_moved.connect(
|
||||||
|
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
)
|
||||||
|
update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
|
||||||
def update_winetricks(self):
|
def update_winetricks(self):
|
||||||
"""Update the winetricks script."""
|
"""Update the winetricks script."""
|
||||||
if not self.downloader.has_internet():
|
if not self.downloader.has_internet():
|
||||||
@@ -1143,15 +1308,15 @@ class WinetricksDialog(QDialog):
|
|||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Set up the user interface with tabs and tables."""
|
"""Set up the user interface with tabs and tables."""
|
||||||
main_layout = QVBoxLayout(self)
|
self.main_layout = QVBoxLayout(self)
|
||||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
main_layout.setSpacing(10)
|
self.main_layout.setSpacing(10)
|
||||||
|
|
||||||
# Log output
|
# Log output
|
||||||
self.log_output = QTextEdit()
|
self.log_output = QTextEdit()
|
||||||
self.log_output.setReadOnly(True)
|
self.log_output.setReadOnly(True)
|
||||||
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
|
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
|
||||||
main_layout.addWidget(self.log_output)
|
self.main_layout.addWidget(self.log_output)
|
||||||
|
|
||||||
# Tab widget
|
# Tab widget
|
||||||
self.tab_widget = QTabWidget()
|
self.tab_widget = QTabWidget()
|
||||||
@@ -1161,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)
|
||||||
@@ -1192,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)
|
||||||
@@ -1223,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)
|
||||||
@@ -1258,7 +1429,7 @@ class WinetricksDialog(QDialog):
|
|||||||
"settings": self.settings_container
|
"settings": self.settings_container
|
||||||
}
|
}
|
||||||
|
|
||||||
main_layout.addWidget(self.tab_widget)
|
self.main_layout.addWidget(self.tab_widget)
|
||||||
|
|
||||||
# Buttons
|
# Buttons
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
@@ -1272,7 +1443,7 @@ class WinetricksDialog(QDialog):
|
|||||||
button_layout.addWidget(self.cancel_button)
|
button_layout.addWidget(self.cancel_button)
|
||||||
button_layout.addWidget(self.force_button)
|
button_layout.addWidget(self.force_button)
|
||||||
button_layout.addWidget(self.install_button)
|
button_layout.addWidget(self.install_button)
|
||||||
main_layout.addLayout(button_layout)
|
self.main_layout.addLayout(button_layout)
|
||||||
|
|
||||||
self.cancel_button.clicked.connect(self.reject)
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
|
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
|
||||||
@@ -1497,3 +1668,429 @@ class WinetricksDialog(QDialog):
|
|||||||
"""Добавляет в лог."""
|
"""Добавляет в лог."""
|
||||||
self.log_output.append(message)
|
self.log_output.append(message)
|
||||||
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Disable mode on close."""
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.disable_winetricks_mode()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
"""Disable mode on reject."""
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.disable_winetricks_mode()
|
||||||
|
super().reject()
|
||||||
|
|
||||||
|
class ExeSettingsDialog(QDialog):
|
||||||
|
def __init__(self, parent=None, theme=None, exe_path=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
|
self.exe_path = exe_path
|
||||||
|
if not self.exe_path:
|
||||||
|
return
|
||||||
|
self.portproton_path = get_portproton_location()
|
||||||
|
if self.portproton_path is None:
|
||||||
|
logger.error("PortProton location not found")
|
||||||
|
return
|
||||||
|
base_path = os.path.join(self.portproton_path, "data")
|
||||||
|
self.start_sh = [os.path.join(base_path, "scripts", "start.sh")]
|
||||||
|
|
||||||
|
self.dist_options = []
|
||||||
|
self.prefix_options = []
|
||||||
|
if self.portproton_path:
|
||||||
|
dist_dir = os.path.join(self.portproton_path, 'dist')
|
||||||
|
if os.path.exists(dist_dir):
|
||||||
|
self.dist_options = [f for f in os.listdir(dist_dir) if os.path.isdir(os.path.join(dist_dir, f))]
|
||||||
|
|
||||||
|
prefixes_dir = os.path.join(self.portproton_path, 'prefixes')
|
||||||
|
if os.path.exists(prefixes_dir):
|
||||||
|
self.prefix_options = [f for f in os.listdir(prefixes_dir) if os.path.isdir(os.path.join(prefixes_dir, f))]
|
||||||
|
|
||||||
|
self.current_settings = {}
|
||||||
|
self.value_widgets = {}
|
||||||
|
self.original_values = {}
|
||||||
|
self.advanced_widgets = {}
|
||||||
|
self.original_display_values = {}
|
||||||
|
self.available_keys = set()
|
||||||
|
self.blocked_keys = set()
|
||||||
|
self.numa_nodes = {}
|
||||||
|
self.is_amd = False
|
||||||
|
self.locale_options = []
|
||||||
|
self.logical_core_options = []
|
||||||
|
self.amd_vulkan_drivers = []
|
||||||
|
|
||||||
|
self.setWindowTitle(_("Exe Settings"))
|
||||||
|
self.setModal(True)
|
||||||
|
self.resize(1100, 720)
|
||||||
|
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
|
||||||
|
|
||||||
|
# Load toggle settings from config module
|
||||||
|
self.toggle_settings = get_toggle_settings()
|
||||||
|
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
# Find input_manager and main_window
|
||||||
|
self.input_manager = None
|
||||||
|
self.main_window = None
|
||||||
|
parent = self.parent()
|
||||||
|
while parent:
|
||||||
|
if hasattr(parent, 'input_manager'):
|
||||||
|
self.input_manager = cast("MainWindow", parent).input_manager
|
||||||
|
self.main_window = parent
|
||||||
|
parent = parent.parent()
|
||||||
|
|
||||||
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
|
||||||
|
# Load current settings (includes list-db)
|
||||||
|
self.load_current_settings()
|
||||||
|
|
||||||
|
def _get_process_args(self, subcommand_args):
|
||||||
|
"""Get the full arguments for QProcess.start, handling flatpak separator."""
|
||||||
|
if self.start_sh[0] == "flatpak":
|
||||||
|
return self.start_sh[1:] + ["--"] + subcommand_args
|
||||||
|
else:
|
||||||
|
return self.start_sh + subcommand_args
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Set up the user interface."""
|
||||||
|
self.main_layout = QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
self.main_layout.setSpacing(10)
|
||||||
|
|
||||||
|
# Tab widget
|
||||||
|
self.tab_widget = QTabWidget()
|
||||||
|
self.tab_widget.setStyleSheet(self.theme.WINETRICKS_TAB_STYLE)
|
||||||
|
self.main_tab = QWidget()
|
||||||
|
self.main_tab_layout = QVBoxLayout(self.main_tab)
|
||||||
|
self.advanced_tab = QWidget()
|
||||||
|
self.advanced_tab_layout = QVBoxLayout(self.advanced_tab)
|
||||||
|
|
||||||
|
self.tab_widget.addTab(self.main_tab, _("Main"))
|
||||||
|
self.tab_widget.addTab(self.advanced_tab, _("Advanced"))
|
||||||
|
|
||||||
|
# Main settings table
|
||||||
|
self.settings_table = QTableWidget()
|
||||||
|
self.settings_table.setAlternatingRowColors(True)
|
||||||
|
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
# self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
|
||||||
|
self.settings_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
self.settings_table.setColumnCount(3)
|
||||||
|
self.settings_table.setHorizontalHeaderLabels([_("Setting"), _("Value"), _("Description")])
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.settings_table.horizontalHeader().resizeSection(1, 100)
|
||||||
|
self.settings_table.setWordWrap(True)
|
||||||
|
self.settings_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.settings_table.setTextElideMode(Qt.TextElideMode.ElideNone)
|
||||||
|
self.settings_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
|
||||||
|
self.main_tab_layout.addWidget(self.settings_table)
|
||||||
|
|
||||||
|
# Advanced settings table
|
||||||
|
self.advanced_table = QTableWidget()
|
||||||
|
self.advanced_table.setAlternatingRowColors(True)
|
||||||
|
self.advanced_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.advanced_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
|
||||||
|
self.advanced_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
# self.advanced_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
self.advanced_table.setColumnCount(3)
|
||||||
|
self.advanced_table.setHorizontalHeaderLabels([_("Setting"), _("Value"), _("Description")])
|
||||||
|
self.advanced_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.advanced_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.advanced_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.advanced_table.horizontalHeader().resizeSection(1, 200)
|
||||||
|
self.advanced_table.setWordWrap(True)
|
||||||
|
self.advanced_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.advanced_table.setTextElideMode(Qt.TextElideMode.ElideNone)
|
||||||
|
self.advanced_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
|
||||||
|
self.advanced_tab_layout.addWidget(self.advanced_table)
|
||||||
|
|
||||||
|
self.main_layout.addWidget(self.tab_widget)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
self.apply_button = AutoSizeButton(_("Apply"), icon=ThemeManager().get_icon("apply"))
|
||||||
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=ThemeManager().get_icon("cancel"))
|
||||||
|
self.apply_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
button_layout.addWidget(self.apply_button)
|
||||||
|
button_layout.addWidget(self.cancel_button)
|
||||||
|
self.main_layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
self.apply_button.clicked.connect(self.apply_changes)
|
||||||
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
|
|
||||||
|
def load_current_settings(self):
|
||||||
|
"""Load available toggles first, then current settings."""
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_list_db_finished)
|
||||||
|
process.start(self.start_sh[0], ["cli", "--list-db"])
|
||||||
|
|
||||||
|
def on_list_db_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --list-db output and extract available keys and system info."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
self.available_keys = set()
|
||||||
|
self.blocked_keys = set()
|
||||||
|
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
|
||||||
|
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
|
||||||
|
lines = output.splitlines()
|
||||||
|
self.numa_nodes = {}
|
||||||
|
self.is_amd = False
|
||||||
|
self.logical_core_options = []
|
||||||
|
self.locale_options = []
|
||||||
|
self.amd_vulkan_drivers = []
|
||||||
|
for line in lines:
|
||||||
|
line_stripped = line.strip()
|
||||||
|
if not line_stripped:
|
||||||
|
continue
|
||||||
|
if re.match(r'^[A-Z_0-9]+=[^=]+$', line_stripped) and not line_stripped.startswith('PW_'):
|
||||||
|
# System info
|
||||||
|
k, v = line_stripped.split('=', 1)
|
||||||
|
if k.startswith('NUMA_NODE_'):
|
||||||
|
node_id = k[10:]
|
||||||
|
self.numa_nodes[node_id] = v
|
||||||
|
elif k == 'IS_AMD':
|
||||||
|
self.is_amd = v.lower() == 'true'
|
||||||
|
elif k == 'LOGICAL_CORE_OPTIONS':
|
||||||
|
self.logical_core_options = v.split('!') if v else []
|
||||||
|
elif k == 'LOCALE_LIST':
|
||||||
|
self.locale_options = v.split('!') if v else []
|
||||||
|
elif k == 'AMD_VULKAN_DRIVER_LIST':
|
||||||
|
self.amd_vulkan_drivers = v.split('!') if v else []
|
||||||
|
continue
|
||||||
|
if line_stripped.startswith('PW_'):
|
||||||
|
parts = line_stripped.split(maxsplit=1)
|
||||||
|
key = parts[0]
|
||||||
|
self.available_keys.add(key)
|
||||||
|
if len(parts) > 1 and 'blocked' in parts[1]:
|
||||||
|
self.blocked_keys.add(key)
|
||||||
|
|
||||||
|
# Show only intersection
|
||||||
|
self.available_keys &= set(self.toggle_settings.keys())
|
||||||
|
|
||||||
|
# Load current settings
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_show_ppdb_finished)
|
||||||
|
process.start(self.start_sh[0], ["cli", "--show-ppdb", f"{self.exe_path}.ppdb"])
|
||||||
|
|
||||||
|
def on_show_ppdb_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --show-ppdb output."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||||
|
# Fallback to defaults if load fails
|
||||||
|
for key in self.toggle_settings:
|
||||||
|
self.current_settings[key] = '0'
|
||||||
|
for adv_key in ADVANCED_SETTING_KEYS:
|
||||||
|
self.current_settings[adv_key] = 'disabled' if 'TOPOLOGY' in adv_key or 'SELECT' in adv_key or 'MODE' in adv_key or 'LEVEL' in adv_key or 'GL_VERSION' in adv_key or 'NUMA' in adv_key else ''
|
||||||
|
else:
|
||||||
|
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore').strip()
|
||||||
|
self.current_settings = {}
|
||||||
|
for line in output.split('\n'):
|
||||||
|
line_stripped = line.strip()
|
||||||
|
if '=' in line_stripped:
|
||||||
|
try:
|
||||||
|
key, val = line_stripped.split('=', 1)
|
||||||
|
if key in self.toggle_settings or key in ADVANCED_SETTING_KEYS:
|
||||||
|
self.current_settings[key] = val
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Force blocked settings to '0'
|
||||||
|
for key in self.blocked_keys:
|
||||||
|
self.current_settings[key] = '0'
|
||||||
|
|
||||||
|
self.original_values = self.current_settings.copy()
|
||||||
|
for key in set(self.toggle_settings.keys()):
|
||||||
|
self.original_values.setdefault(key, '0')
|
||||||
|
|
||||||
|
self.populate_table()
|
||||||
|
self.populate_advanced()
|
||||||
|
|
||||||
|
def populate_table(self):
|
||||||
|
"""Populate the table with settings that are available in both lists."""
|
||||||
|
self.settings_table.setRowCount(0)
|
||||||
|
self.value_widgets.clear()
|
||||||
|
self.settings_table.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
|
visible_keys = sorted(self.available_keys) if self.available_keys else sorted(self.toggle_settings.keys())
|
||||||
|
|
||||||
|
for toggle in visible_keys:
|
||||||
|
description = self.toggle_settings.get(toggle)
|
||||||
|
if not description:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = self.settings_table.rowCount()
|
||||||
|
self.settings_table.insertRow(row)
|
||||||
|
|
||||||
|
name_item = QTableWidgetItem(toggle)
|
||||||
|
name_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
|
||||||
|
current_val = self.current_settings.get(toggle, '0')
|
||||||
|
is_blocked = toggle in self.blocked_keys
|
||||||
|
checkbox = QTableWidgetItem()
|
||||||
|
checkbox.setFlags(checkbox.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
check_state = Qt.CheckState.Checked if current_val == '1' and not is_blocked else Qt.CheckState.Unchecked
|
||||||
|
checkbox.setCheckState(check_state)
|
||||||
|
checkbox.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
if is_blocked:
|
||||||
|
checkbox.setFlags(checkbox.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
checkbox.setBackground(QColor(240, 240, 240))
|
||||||
|
name_item.setForeground(QColor(128, 128, 128))
|
||||||
|
self.settings_table.setItem(row, 1, checkbox)
|
||||||
|
|
||||||
|
desc_item = QTableWidgetItem(description)
|
||||||
|
desc_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
desc_item.setToolTip(description)
|
||||||
|
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
if is_blocked:
|
||||||
|
desc_item.setForeground(QColor(128, 128, 128))
|
||||||
|
self.settings_table.setItem(row, 2, desc_item)
|
||||||
|
|
||||||
|
self.settings_table.setItem(row, 0, name_item)
|
||||||
|
self.value_widgets[(row, 1)] = checkbox
|
||||||
|
|
||||||
|
self.settings_table.resizeRowsToContents()
|
||||||
|
if self.settings_table.rowCount() > 0:
|
||||||
|
self.settings_table.setCurrentCell(0, 0)
|
||||||
|
self.settings_table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
|
||||||
|
def populate_advanced(self):
|
||||||
|
"""Populate the advanced tab with table format."""
|
||||||
|
self.advanced_table.setRowCount(0)
|
||||||
|
self.advanced_widgets.clear()
|
||||||
|
self.original_display_values = {}
|
||||||
|
self.advanced_table.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
|
current = self.current_settings
|
||||||
|
disabled_text = _('disabled')
|
||||||
|
|
||||||
|
# Get advanced settings from config module
|
||||||
|
advanced_settings = get_advanced_settings(
|
||||||
|
disabled_text=disabled_text,
|
||||||
|
logical_core_options=self.logical_core_options,
|
||||||
|
locale_options=self.locale_options,
|
||||||
|
amd_vulkan_drivers=self.amd_vulkan_drivers,
|
||||||
|
is_amd=self.is_amd,
|
||||||
|
numa_nodes=self.numa_nodes,
|
||||||
|
dist_options=self.dist_options,
|
||||||
|
prefix_options=self.prefix_options
|
||||||
|
)
|
||||||
|
|
||||||
|
# Populate table
|
||||||
|
for setting in advanced_settings:
|
||||||
|
row = self.advanced_table.rowCount()
|
||||||
|
self.advanced_table.insertRow(row)
|
||||||
|
|
||||||
|
# Name column
|
||||||
|
name_item = QTableWidgetItem(setting['name'])
|
||||||
|
name_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
self.advanced_table.setItem(row, 0, name_item)
|
||||||
|
|
||||||
|
# Value column (widget)
|
||||||
|
if setting['type'] == 'combo':
|
||||||
|
combo = QComboBox()
|
||||||
|
combo.addItems(setting['options'])
|
||||||
|
|
||||||
|
# Get current value
|
||||||
|
current_raw = current.get(setting['key'], setting['default'])
|
||||||
|
if setting['key'] == 'PW_WINE_CPU_TOPOLOGY':
|
||||||
|
current_val = disabled_text if current_raw == 'disabled' else (current_raw.split(':')[0] if isinstance(current_raw, str) and ':' in current_raw else current_raw)
|
||||||
|
elif setting['key'] == 'PW_AMD_VULKAN_USE':
|
||||||
|
current_val = disabled_text if not current_raw or current_raw == '' else current_raw
|
||||||
|
else:
|
||||||
|
current_val = disabled_text if current_raw == 'disabled' else current_raw
|
||||||
|
|
||||||
|
if current_val not in setting['options']:
|
||||||
|
combo.addItem(current_val)
|
||||||
|
combo.setCurrentText(current_val)
|
||||||
|
|
||||||
|
# Block if only disabled option
|
||||||
|
if len(setting['options']) == 1:
|
||||||
|
combo.setEnabled(False)
|
||||||
|
|
||||||
|
self.advanced_table.setCellWidget(row, 1, combo)
|
||||||
|
self.advanced_widgets[setting['key']] = combo
|
||||||
|
self.original_display_values[setting['key']] = current_val
|
||||||
|
|
||||||
|
elif setting['type'] == 'text':
|
||||||
|
line_edit = QLineEdit()
|
||||||
|
current_val = current.get(setting['key'], setting['default'])
|
||||||
|
line_edit.setText(current_val)
|
||||||
|
|
||||||
|
self.advanced_table.setCellWidget(row, 1, line_edit)
|
||||||
|
self.advanced_widgets[setting['key']] = line_edit
|
||||||
|
self.original_display_values[setting['key']] = current_val
|
||||||
|
|
||||||
|
# Description column
|
||||||
|
desc_item = QTableWidgetItem(setting['description'])
|
||||||
|
desc_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
desc_item.setToolTip(setting['description'])
|
||||||
|
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
self.advanced_table.setItem(row, 2, desc_item)
|
||||||
|
|
||||||
|
def apply_changes(self):
|
||||||
|
"""Apply changes by collecting diffs from both main and advanced tabs."""
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
for key, orig_val in self.original_values.items():
|
||||||
|
if key in self.blocked_keys:
|
||||||
|
continue # Skip blocked keys
|
||||||
|
row = -1
|
||||||
|
for r in range(self.settings_table.rowCount()):
|
||||||
|
item0 = self.settings_table.item(r, 0)
|
||||||
|
if item0 and item0.text() == key:
|
||||||
|
row = r
|
||||||
|
break
|
||||||
|
if row == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item = self.settings_table.item(row, 1)
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_val = '1' if item.checkState() == Qt.CheckState.Checked else '0'
|
||||||
|
if new_val != orig_val:
|
||||||
|
changes.append(f"{key}={new_val}")
|
||||||
|
|
||||||
|
for key, widget in self.advanced_widgets.items():
|
||||||
|
orig_val = self.original_display_values.get(key, '')
|
||||||
|
if isinstance(widget, QComboBox):
|
||||||
|
new_val = widget.currentText()
|
||||||
|
if new_val.lower() == _('disabled').lower():
|
||||||
|
new_val = 'disabled'
|
||||||
|
elif isinstance(widget, QLineEdit):
|
||||||
|
new_val = widget.text().strip()
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if new_val != orig_val:
|
||||||
|
changes.append(f"{key}={new_val}")
|
||||||
|
|
||||||
|
if not changes:
|
||||||
|
QMessageBox.information(self, _("Info"), _("No changes to apply."))
|
||||||
|
return
|
||||||
|
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_edit_db_finished)
|
||||||
|
args = ["cli", "--edit-db", self.exe_path] + changes
|
||||||
|
process.start(self.start_sh[0], args)
|
||||||
|
self.apply_button.setEnabled(False)
|
||||||
|
|
||||||
|
def on_edit_db_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --edit-db output."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
self.apply_button.setEnabled(True)
|
||||||
|
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||||
|
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to apply changes. Check logs."))
|
||||||
|
logger.error(f"Failed to apply changes: {error_output}")
|
||||||
|
else:
|
||||||
|
self.load_current_settings()
|
||||||
|
QMessageBox.information(self, _("Success"), _("Settings updated successfully."))
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
super().reject()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class MainWindowProtocol(Protocol):
|
|||||||
# Required attributes
|
# Required attributes
|
||||||
searchEdit: CustomLineEdit
|
searchEdit: CustomLineEdit
|
||||||
_last_card_width: int
|
_last_card_width: int
|
||||||
|
card_width: int
|
||||||
current_hovered_card: GameCard | None
|
current_hovered_card: GameCard | None
|
||||||
current_focused_card: GameCard | None
|
current_focused_card: GameCard | None
|
||||||
gamesListWidget: QWidget | None
|
gamesListWidget: QWidget | None
|
||||||
@@ -128,6 +129,8 @@ class GameLibraryManager:
|
|||||||
self.card_width = self.sizeSlider.value()
|
self.card_width = self.sizeSlider.value()
|
||||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
||||||
save_card_size(self.card_width)
|
save_card_size(self.card_width)
|
||||||
|
self.main_window.card_width = self.card_width
|
||||||
|
self.main_window._last_card_width = self.card_width
|
||||||
for card in self.game_card_cache.values():
|
for card in self.game_card_cache.values():
|
||||||
card.update_card_size(self.card_width)
|
card.update_card_size(self.card_width)
|
||||||
self.update_game_grid()
|
self.update_game_grid()
|
||||||
@@ -264,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,11 +1,12 @@
|
|||||||
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, MonitorObserver, Device
|
from pyudev import Context, Monitor, Device, Devices
|
||||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView
|
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QTableWidgetItem
|
||||||
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
|
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
|
||||||
from PySide6.QtGui import QKeyEvent, QMouseEvent
|
from PySide6.QtGui import QKeyEvent, QMouseEvent
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
@@ -13,8 +14,9 @@ from portprotonqt.image_utils import FullscreenDialog
|
|||||||
from portprotonqt.custom_widgets import NavLabel, AutoSizeButton
|
from portprotonqt.custom_widgets import NavLabel, AutoSizeButton
|
||||||
from portprotonqt.game_card import GameCard
|
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, WinetricksDialog
|
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__)
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ class InputManager(QObject):
|
|||||||
button_event = Signal(int, int) # Signal for button events: (code, value) where value=1 (press), 0 (release)
|
button_event = Signal(int, int) # Signal for button events: (code, value) where value=1 (press), 0 (release)
|
||||||
dpad_moved = Signal(int, int, float) # Signal for D-pad movements
|
dpad_moved = Signal(int, int, float) # Signal for D-pad movements
|
||||||
toggle_fullscreen = Signal(bool) # Signal for toggling fullscreen mode (True for fullscreen, False for normal)
|
toggle_fullscreen = Signal(bool) # Signal for toggling fullscreen mode (True for fullscreen, False for normal)
|
||||||
|
gamepad_hotplug = Signal(str) # 'add' or 'remove'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -114,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
|
||||||
@@ -150,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:
|
||||||
@@ -455,6 +493,171 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error in FileExplorer dpad handler: %s", e)
|
logger.error("Error in FileExplorer dpad handler: %s", e)
|
||||||
|
|
||||||
|
def enable_winetricks_mode(self, winetricks_dialog):
|
||||||
|
"""Setup gamepad handling for WinetricksDialog"""
|
||||||
|
try:
|
||||||
|
self.winetricks_dialog = winetricks_dialog
|
||||||
|
self.original_button_handler = self.handle_button_slot
|
||||||
|
self.original_dpad_handler = self.handle_dpad_slot
|
||||||
|
self.original_gamepad_state = self._gamepad_handling_enabled
|
||||||
|
self.handle_button_slot = self.handle_winetricks_button
|
||||||
|
self.handle_dpad_slot = self.handle_winetricks_dpad
|
||||||
|
self._gamepad_handling_enabled = True
|
||||||
|
# Reset dpad timer for table nav
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.current_dpad_code = None
|
||||||
|
self.current_dpad_value = 0
|
||||||
|
logger.debug("Gamepad handling successfully connected for WinetricksDialog")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error connecting gamepad handlers for Winetricks: {e}")
|
||||||
|
|
||||||
|
def disable_winetricks_mode(self):
|
||||||
|
"""Restore original main window handlers"""
|
||||||
|
try:
|
||||||
|
if self.winetricks_dialog:
|
||||||
|
self.handle_button_slot = self.original_button_handler
|
||||||
|
self.handle_dpad_slot = self.original_dpad_handler
|
||||||
|
self._gamepad_handling_enabled = self.original_gamepad_state
|
||||||
|
self.winetricks_dialog = None
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.current_dpad_code = None
|
||||||
|
self.current_dpad_value = 0
|
||||||
|
logger.debug("Gamepad handling successfully restored from Winetricks")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error restoring gamepad handlers from Winetricks: {e}")
|
||||||
|
|
||||||
|
def handle_winetricks_button(self, button_code, value):
|
||||||
|
if self.winetricks_dialog is None:
|
||||||
|
return
|
||||||
|
if value == 0: # Ignore releases
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
# Always check for popups first, including QMessageBox
|
||||||
|
popup = QApplication.activePopupWidget()
|
||||||
|
if popup:
|
||||||
|
if isinstance(popup, QMessageBox):
|
||||||
|
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['back']:
|
||||||
|
popup.accept() # Close QMessageBox with A or B
|
||||||
|
return
|
||||||
|
elif isinstance(popup, QMenu):
|
||||||
|
if button_code in BUTTONS['confirm']: # A: Select menu item
|
||||||
|
focused = popup.activeAction()
|
||||||
|
if focused:
|
||||||
|
focused.trigger()
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['back']: # B: Close menu
|
||||||
|
popup.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Additional check for top-level QMessageBox (in case not active popup yet)
|
||||||
|
for widget in QApplication.topLevelWidgets():
|
||||||
|
if isinstance(widget, QMessageBox) and widget.isVisible():
|
||||||
|
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['back']:
|
||||||
|
widget.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
focused = QApplication.focusWidget()
|
||||||
|
if button_code in BUTTONS['confirm']: # A: Toggle checkbox
|
||||||
|
if isinstance(focused, QTableWidget):
|
||||||
|
current_row = focused.currentRow()
|
||||||
|
if current_row >= 0:
|
||||||
|
checkbox_item = focused.item(current_row, 0)
|
||||||
|
if checkbox_item and isinstance(checkbox_item, QTableWidgetItem):
|
||||||
|
new_state = Qt.CheckState.Checked if checkbox_item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked
|
||||||
|
checkbox_item.setCheckState(new_state)
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['add_game']: # X: Install (no force)
|
||||||
|
self.winetricks_dialog.install_selected(force=False)
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['prev_dir']: # Y: Force Install
|
||||||
|
self.winetricks_dialog.install_selected(force=True)
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['back']: # B: Cancel
|
||||||
|
self.winetricks_dialog.reject()
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['prev_tab']: # LB: Prev Tab
|
||||||
|
current_index = self.winetricks_dialog.tab_widget.currentIndex()
|
||||||
|
new_index = max(0, current_index - 1)
|
||||||
|
self.winetricks_dialog.tab_widget.setCurrentIndex(new_index)
|
||||||
|
self._focus_first_row_in_current_table()
|
||||||
|
return
|
||||||
|
elif button_code in BUTTONS['next_tab']: # RB: Next Tab
|
||||||
|
current_index = self.winetricks_dialog.tab_widget.currentIndex()
|
||||||
|
new_index = min(self.winetricks_dialog.tab_widget.count() - 1, current_index + 1)
|
||||||
|
self.winetricks_dialog.tab_widget.setCurrentIndex(new_index)
|
||||||
|
self._focus_first_row_in_current_table()
|
||||||
|
return
|
||||||
|
# Fallback: Activate focused widget (e.g., buttons)
|
||||||
|
self._parent.activateFocusedWidget()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in handle_winetricks_button: {e}")
|
||||||
|
|
||||||
|
def handle_winetricks_dpad(self, code, value, now):
|
||||||
|
if self.winetricks_dialog is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if value == 0: # Release: Stop repeat
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.current_dpad_code = None
|
||||||
|
self.current_dpad_value = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
# Start/update repeat timer for hold navigation
|
||||||
|
if self.current_dpad_code != code or self.current_dpad_value != value:
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.dpad_timer.setInterval(150 if self.dpad_timer.isActive() else 300) # Initial slower, then faster repeat
|
||||||
|
self.dpad_timer.start()
|
||||||
|
self.current_dpad_code = code
|
||||||
|
self.current_dpad_value = value
|
||||||
|
|
||||||
|
table = self._get_current_table()
|
||||||
|
if not table or table.rowCount() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_row = table.currentRow()
|
||||||
|
if code == ecodes.ABS_HAT0Y: # Up/Down: Navigate rows
|
||||||
|
if value < 0: # Up
|
||||||
|
new_row = max(0, current_row - 1)
|
||||||
|
elif value > 0: # Down
|
||||||
|
new_row = min(table.rowCount() - 1, current_row + 1)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if new_row != current_row:
|
||||||
|
table.setCurrentCell(new_row, 0) # Focus checkbox column
|
||||||
|
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
elif code == ecodes.ABS_HAT0X: # Left/Right: Switch tabs
|
||||||
|
if value < 0: # Left: Prev tab
|
||||||
|
current_index = self.winetricks_dialog.tab_widget.currentIndex()
|
||||||
|
new_index = max(0, current_index - 1)
|
||||||
|
self.winetricks_dialog.tab_widget.setCurrentIndex(new_index)
|
||||||
|
elif value > 0: # Right: Next tab
|
||||||
|
current_index = self.winetricks_dialog.tab_widget.currentIndex()
|
||||||
|
new_index = min(self.winetricks_dialog.tab_widget.count() - 1, current_index + 1)
|
||||||
|
self.winetricks_dialog.tab_widget.setCurrentIndex(new_index)
|
||||||
|
self._focus_first_row_in_current_table()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in handle_winetricks_dpad: {e}")
|
||||||
|
|
||||||
|
def _get_current_table(self):
|
||||||
|
"""Get the current visible table from the tab widget's stacked container."""
|
||||||
|
if self.winetricks_dialog is None:
|
||||||
|
return None
|
||||||
|
current_container = self.winetricks_dialog.tab_widget.currentWidget()
|
||||||
|
if current_container and isinstance(current_container, QStackedWidget):
|
||||||
|
current_table = current_container.widget(1) # Table is at index 1 (after preloader)
|
||||||
|
if isinstance(current_table, QTableWidget):
|
||||||
|
return current_table
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _focus_first_row_in_current_table(self):
|
||||||
|
"""Focus the first row in the current table after tab switch."""
|
||||||
|
if self.winetricks_dialog is None:
|
||||||
|
return
|
||||||
|
table = self._get_current_table()
|
||||||
|
if table and table.rowCount() > 0:
|
||||||
|
table.setCurrentCell(0, 0)
|
||||||
|
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
|
||||||
def handle_navigation_repeat(self):
|
def handle_navigation_repeat(self):
|
||||||
"""Плавное повторение движения с переменной скоростью для FileExplorer"""
|
"""Плавное повторение движения с переменной скоростью для FileExplorer"""
|
||||||
try:
|
try:
|
||||||
@@ -471,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:
|
||||||
@@ -705,39 +1018,6 @@ class InputManager(QObject):
|
|||||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
if isinstance(active, WinetricksDialog):
|
|
||||||
if button_code in BUTTONS['confirm']: # A button - toggle checkbox
|
|
||||||
current_table = active.tab_widget.currentWidget()
|
|
||||||
if isinstance(current_table, QTableWidget):
|
|
||||||
current_row = current_table.currentRow()
|
|
||||||
if current_row >= 0:
|
|
||||||
checkbox = current_table.item(current_row, 0)
|
|
||||||
if checkbox:
|
|
||||||
checkbox.setCheckState(
|
|
||||||
Qt.CheckState.Unchecked if checkbox.checkState() == Qt.CheckState.Checked else Qt.CheckState.Checked
|
|
||||||
)
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['add_game']: # X button - install
|
|
||||||
active.install_selected(force=False)
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['prev_dir']: # Y button - force install
|
|
||||||
active.install_selected(force=True)
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['back']: # B button - close dialog
|
|
||||||
active.reject()
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['prev_tab']: # LB - previous tab
|
|
||||||
current_idx = active.tab_widget.currentIndex()
|
|
||||||
new_idx = (current_idx - 1) % active.tab_widget.count()
|
|
||||||
active.tab_widget.setCurrentIndex(new_idx)
|
|
||||||
return
|
|
||||||
elif button_code in BUTTONS['next_tab']: # RB - next tab
|
|
||||||
current_idx = active.tab_widget.currentIndex()
|
|
||||||
new_idx = (current_idx + 1) % active.tab_widget.count()
|
|
||||||
active.tab_widget.setCurrentIndex(new_idx)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Standard navigation
|
# Standard navigation
|
||||||
if button_code in BUTTONS['confirm']:
|
if button_code in BUTTONS['confirm']:
|
||||||
self._parent.activateFocusedWidget()
|
self._parent.activateFocusedWidget()
|
||||||
@@ -1304,127 +1584,371 @@ class InputManager(QObject):
|
|||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
def init_gamepad(self) -> None:
|
def init_gamepad(self) -> None:
|
||||||
|
self.udev_context = Context()
|
||||||
|
self.Devices = Devices
|
||||||
|
self.monitor_ready = False
|
||||||
|
self.monitor_event = threading.Event()
|
||||||
|
|
||||||
|
# Подключаем сигнал hotplug к обработчику в главном потоке
|
||||||
|
self.gamepad_hotplug.connect(self._on_gamepad_hotplug)
|
||||||
|
|
||||||
|
# Debounce timer для отложенной проверки геймпада (в главном потоке Qt)
|
||||||
|
self.gamepad_check_timer = QTimer()
|
||||||
|
self.gamepad_check_timer.setSingleShot(True)
|
||||||
|
self.gamepad_check_timer.timeout.connect(self.check_gamepad)
|
||||||
|
|
||||||
|
# Первоначальная проверка
|
||||||
self.check_gamepad()
|
self.check_gamepad()
|
||||||
|
|
||||||
|
# Запускаем udev monitor в отдельном потоке
|
||||||
threading.Thread(target=self.run_udev_monitor, daemon=True).start()
|
threading.Thread(target=self.run_udev_monitor, daemon=True).start()
|
||||||
logger.info("Gamepad support initialized with hotplug (evdev + pyudev)")
|
logger.info("Gamepad support initialized with hotplug (evdev + pyudev)")
|
||||||
|
|
||||||
def run_udev_monitor(self) -> None:
|
def run_udev_monitor(self) -> None:
|
||||||
|
"""
|
||||||
|
Безопасный неблокирующий udev monitor для геймпадов.
|
||||||
|
Использует select.poll() вместо блокирующего monitor.poll().
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
context = Context()
|
logger.info("Starting udev monitor...")
|
||||||
monitor = Monitor.from_netlink(context)
|
monitor = Monitor.from_netlink(self.udev_context)
|
||||||
monitor.filter_by(subsystem='input')
|
monitor.filter_by(subsystem='input')
|
||||||
observer = MonitorObserver(monitor, self.handle_udev_event)
|
|
||||||
observer.start()
|
try:
|
||||||
|
monitor.start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start udev monitor: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
fd = monitor.fileno()
|
||||||
|
poller = select.poll()
|
||||||
|
poller.register(fd, select.POLLIN)
|
||||||
|
|
||||||
|
# Короткий дренаж событий при запуске (0.5 сек)
|
||||||
|
drain_start = time.time()
|
||||||
|
drained_count = 0
|
||||||
|
while time.time() - drain_start < 0.5:
|
||||||
|
events = poller.poll(100)
|
||||||
|
if not events:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
_ = monitor.poll(timeout=0) # просто читаем, не обрабатываем
|
||||||
|
drained_count += 1
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.monitor_ready = True
|
||||||
|
self.monitor_event.set()
|
||||||
|
logger.info(f"Drained {drained_count} initial events, now monitoring hotplug...")
|
||||||
|
|
||||||
|
# Основной цикл
|
||||||
while self.running:
|
while self.running:
|
||||||
time.sleep(1)
|
events = poller.poll(1000) # 1 сек таймаут
|
||||||
|
if not events:
|
||||||
|
continue # просто ждём, не блокируем
|
||||||
|
|
||||||
|
try:
|
||||||
|
device = monitor.poll(timeout=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Monitor poll failed: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
continue
|
||||||
|
|
||||||
|
action = device.action
|
||||||
|
if action and self._is_joystick_device(device):
|
||||||
|
logger.info(f"Joystick hotplug event: {action} for {device.sys_name}")
|
||||||
|
# отправляем сигнал в Qt-поток
|
||||||
|
self.handle_udev_event(action, device)
|
||||||
|
|
||||||
|
logger.info("udev monitor stopped gracefully")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in udev monitor: {e}", exc_info=True)
|
logger.error(f"Error in udev monitor: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def _is_joystick_device(self, device: Device) -> bool:
|
||||||
|
"""
|
||||||
|
Быстрая проверка: является ли устройство джойстиком.
|
||||||
|
Проверяет ID_INPUT_JOYSTICK из udev базы данных.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Проверяем свойство ID_INPUT_JOYSTICK
|
||||||
|
if device.get('ID_INPUT_JOYSTICK') == '1':
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Дополнительно: проверяем родительские устройства
|
||||||
|
# (некоторые контроллеры имеют свойство только у родителя)
|
||||||
|
parent = device.parent
|
||||||
|
if parent and parent.get('ID_INPUT_JOYSTICK') == '1':
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error checking joystick device: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def handle_udev_event(self, action: str, device: Device) -> None:
|
def handle_udev_event(self, action: str, device: Device) -> None:
|
||||||
|
"""
|
||||||
|
Обработчик udev событий для джойстиков.
|
||||||
|
Отправляет сигнал в главный поток Qt вместо прямого вызова QTimer.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if action == 'add':
|
if action == 'add':
|
||||||
time.sleep(0.1)
|
# Отправляем сигнал в главный поток Qt
|
||||||
self.check_gamepad()
|
# QTimer будет запущен там безопасно
|
||||||
|
logger.debug("Emitting gamepad add signal")
|
||||||
|
self.gamepad_hotplug.emit('add')
|
||||||
|
|
||||||
elif action == 'remove' and self.gamepad:
|
elif action == 'remove' and self.gamepad:
|
||||||
if not any(self.gamepad.path == path for path in list_devices()):
|
# Проверяем конкретно наш геймпад по пути устройства
|
||||||
logger.info("Gamepad disconnected")
|
device_node = device.device_node # например, /dev/input/event3
|
||||||
self.stop_rumble()
|
|
||||||
self.gamepad = None
|
if device_node and self.gamepad.path == device_node:
|
||||||
if self.gamepad_thread:
|
logger.info(f"Connected gamepad disconnected: {device_node}")
|
||||||
self.gamepad_thread.join()
|
# Отправляем сигнал в главный поток
|
||||||
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
self.gamepad_hotplug.emit('remove')
|
||||||
self.toggle_fullscreen.emit(False)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling udev event: {e}", exc_info=True)
|
logger.error(f"Error handling udev event: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _on_gamepad_hotplug(self, action: str) -> None:
|
||||||
|
"""
|
||||||
|
Обработчик сигнала hotplug, выполняется в главном потоке Qt.
|
||||||
|
Безопасно работает с QTimer.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if action == 'add':
|
||||||
|
# Debounce: откладываем проверку на 200ms
|
||||||
|
# Множественные события за короткое время объединяются в один вызов
|
||||||
|
logger.debug("Scheduling gamepad check (debounced)")
|
||||||
|
self.gamepad_check_timer.start(200)
|
||||||
|
|
||||||
|
elif action == 'remove':
|
||||||
|
# Немедленная обработка отключения
|
||||||
|
self.stop_rumble()
|
||||||
|
self.gamepad = None
|
||||||
|
|
||||||
|
if self.gamepad_thread:
|
||||||
|
self.gamepad_thread.join(timeout=2.0)
|
||||||
|
|
||||||
|
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
||||||
|
self.toggle_fullscreen.emit(False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in hotplug handler: {e}", exc_info=True)
|
||||||
|
|
||||||
def check_gamepad(self) -> None:
|
def check_gamepad(self) -> None:
|
||||||
|
"""
|
||||||
|
Проверка и подключение геймпада.
|
||||||
|
Вызывается из главного потока Qt через QTimer (debounced).
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
new_gamepad = self.find_gamepad()
|
new_gamepad = self.find_gamepad()
|
||||||
if new_gamepad and new_gamepad != self.gamepad:
|
|
||||||
logger.info(f"Gamepad connected: {new_gamepad.name}")
|
if new_gamepad:
|
||||||
|
if not self.gamepad or new_gamepad.path != self.gamepad.path:
|
||||||
|
logger.info(f"Gamepad connected: {new_gamepad.name} at {new_gamepad.path}")
|
||||||
|
self.stop_rumble()
|
||||||
|
self.gamepad = new_gamepad
|
||||||
|
|
||||||
|
if self.gamepad_thread and self.gamepad_thread.is_alive():
|
||||||
|
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(
|
||||||
|
target=start_monitoring,
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
self.gamepad_thread.start()
|
||||||
|
|
||||||
|
# Автоматический фуллскрин при подключении геймпада
|
||||||
|
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
||||||
|
self.toggle_fullscreen.emit(True)
|
||||||
|
|
||||||
|
elif self.gamepad and not any(self.gamepad.path == path for path in list_devices()):
|
||||||
|
logger.info("Gamepad no longer detected")
|
||||||
self.stop_rumble()
|
self.stop_rumble()
|
||||||
self.gamepad = new_gamepad
|
self.gamepad = None
|
||||||
if self.gamepad_thread:
|
|
||||||
self.gamepad_thread.join()
|
if self.gamepad_thread and self.gamepad_thread.is_alive():
|
||||||
self.gamepad_thread = threading.Thread(target=self.monitor_gamepad, daemon=True)
|
self.gamepad_thread.join(timeout=2.0)
|
||||||
self.gamepad_thread.start()
|
|
||||||
# Send signal for fullscreen mode only if:
|
|
||||||
# 1. auto_fullscreen_gamepad is enabled
|
|
||||||
# 2. fullscreen is not already enabled (to avoid conflict)
|
|
||||||
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
||||||
self.toggle_fullscreen.emit(True)
|
self.toggle_fullscreen.emit(False)
|
||||||
|
|
||||||
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:
|
||||||
|
"""
|
||||||
|
Находит первый доступный геймпад.
|
||||||
|
Оптимизирован: предварительная фильтрация по capabilities перед udev-запросами.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
devices = [InputDevice(path) for path in list_devices()]
|
devices = [InputDevice(path) for path in list_devices()]
|
||||||
|
|
||||||
|
if not devices:
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.debug(f"Checking {len(devices)} devices for gamepad...")
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
# Skip ASRock LED controller (vendor ID: 26ce, product ID: 01a2)
|
# Skip ASRock LED controller (известная проблема)
|
||||||
if device.info.vendor == 0x26ce and device.info.product == 0x01a2:
|
if device.info.vendor == 0x26ce and device.info.product == 0x01a2:
|
||||||
logger.debug(f"Skipping ASRock LED controller: {device.name}")
|
|
||||||
continue
|
continue
|
||||||
caps = device.capabilities()
|
|
||||||
if ecodes.EV_KEY in caps or ecodes.EV_ABS in caps:
|
# Предварительная фильтрация: проверяем capabilities
|
||||||
return device
|
# Джойстик должен иметь хотя бы оси (ABS) или кнопки (KEY)
|
||||||
|
# Это избегает udev-запросов для явно не-джойстиков
|
||||||
|
caps = device.capabilities(verbose=False)
|
||||||
|
has_abs_axes = ecodes.EV_ABS in caps
|
||||||
|
has_buttons = ecodes.EV_KEY in caps
|
||||||
|
|
||||||
|
if not (has_abs_axes or has_buttons):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Только для потенциальных джойстиков делаем udev-запрос
|
||||||
|
try:
|
||||||
|
udev_device = self.Devices.from_device_file(
|
||||||
|
self.udev_context,
|
||||||
|
device.path
|
||||||
|
)
|
||||||
|
is_joystick = udev_device.get('ID_INPUT_JOYSTICK')
|
||||||
|
|
||||||
|
if is_joystick == '1':
|
||||||
|
logger.info(f"Found gamepad: {device.name}")
|
||||||
|
return device
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Could not check udev properties for {device.path}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.debug("No gamepad found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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:
|
||||||
@@ -1433,18 +1957,43 @@ 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 монитором.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Mouse emulation cleanup
|
||||||
|
self.disable_mouse_emulation()
|
||||||
|
|
||||||
|
# Stop focus check timer
|
||||||
|
self.focus_check_timer.stop()
|
||||||
|
|
||||||
|
# Флаг для остановки udev monitor loop
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
# Останавливаем все таймеры
|
||||||
|
if hasattr(self, 'gamepad_check_timer'):
|
||||||
|
self.gamepad_check_timer.stop()
|
||||||
self.dpad_timer.stop()
|
self.dpad_timer.stop()
|
||||||
self.nav_timer.stop()
|
self.nav_timer.stop()
|
||||||
|
|
||||||
|
# Очистка геймпада
|
||||||
self.stop_rumble()
|
self.stop_rumble()
|
||||||
|
|
||||||
if self.gamepad_thread:
|
if self.gamepad_thread:
|
||||||
self.gamepad_thread.join()
|
self.gamepad_thread.join(timeout=2.0)
|
||||||
|
|
||||||
if self.gamepad:
|
if self.gamepad:
|
||||||
self.gamepad.close()
|
self.gamepad.close()
|
||||||
|
|
||||||
self.gamepad = None
|
self.gamepad = None
|
||||||
self.gamepad_type = GamepadType.UNKNOWN
|
self.gamepad_type = GamepadType.UNKNOWN
|
||||||
|
|
||||||
|
logger.info("Gamepad cleanup completed")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during cleanup: {e}", exc_info=True)
|
logger.error(f"Error during cleanup: {e}", exc_info=True)
|
||||||
|
|||||||
@@ -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-15 15:31+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 ""
|
||||||
@@ -252,13 +252,37 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -326,12 +350,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -347,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 ""
|
||||||
|
|
||||||
@@ -494,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."
|
||||||
@@ -606,6 +664,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -680,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 ""
|
||||||
|
|
||||||
@@ -738,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: 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-15 15:31+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 ""
|
||||||
@@ -252,13 +252,37 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -326,12 +350,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -347,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 ""
|
||||||
|
|
||||||
@@ -494,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."
|
||||||
@@ -606,6 +664,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -680,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 ""
|
||||||
|
|
||||||
@@ -738,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-15 15:31+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 ""
|
||||||
@@ -250,13 +250,37 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -324,12 +348,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -345,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 ""
|
||||||
|
|
||||||
@@ -492,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."
|
||||||
@@ -604,6 +662,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -678,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 ""
|
||||||
|
|
||||||
@@ -736,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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -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-15 15:31+0500\n"
|
"POT-Creation-Date: 2025-11-11 17:00+0500\n"
|
||||||
"PO-Revision-Date: 2025-10-15 15:31+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"
|
||||||
@@ -259,13 +259,37 @@ msgstr "Удалить"
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr "Выбрать всё"
|
msgstr "Выбрать всё"
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr "Открыть"
|
||||||
msgstr "Идёт запуск {0}"
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr "Выбрать папку"
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
|
msgstr "Предыдущий каталог"
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Отмена"
|
msgstr "Отмена"
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr "Переключить"
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr "Установить"
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr "Принудительно установить"
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr "Предыдущая вкладка"
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr "Следующая вкладка"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr "Идёт запуск {0}"
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr "Проводник"
|
msgstr "Проводник"
|
||||||
|
|
||||||
@@ -333,12 +357,6 @@ msgstr "Шрифты"
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "Настройки"
|
msgstr "Настройки"
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr "Принудительно установить"
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr "Установить"
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr "Winetricks не найден. Повторите попытку."
|
msgstr "Winetricks не найден. Повторите попытку."
|
||||||
|
|
||||||
@@ -354,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..."
|
||||||
|
|
||||||
@@ -501,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 "Не удалось запустить процесс резервного копирования."
|
||||||
@@ -615,6 +671,12 @@ msgstr "Запуск приложения в полноэкранном режи
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr "Режим полноэкранного отображения приложения:"
|
msgstr "Режим полноэкранного отображения приложения:"
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr "Сворачивать в трей при закрытии"
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr "Режим закрытия приложения:"
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
||||||
|
|
||||||
@@ -691,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 "Последний запуск"
|
||||||
|
|
||||||
@@ -749,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
|
||||||
@@ -29,7 +29,8 @@ from portprotonqt.config_utils import (
|
|||||||
read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method,
|
read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method,
|
||||||
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
|
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, 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
|
||||||
@@ -39,7 +40,7 @@ from portprotonqt.game_library_manager import GameLibraryManager
|
|||||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||||
|
|
||||||
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
|
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
|
||||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller)
|
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller, QSlider)
|
||||||
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot, QProcess
|
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot, QProcess
|
||||||
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
|
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
|
||||||
from typing import cast
|
from typing import cast
|
||||||
@@ -63,6 +64,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.theme = self.theme_manager.apply_theme(selected_theme)
|
self.theme = self.theme_manager.apply_theme(selected_theme)
|
||||||
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
|
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
|
||||||
self.card_width = read_card_size()
|
self.card_width = read_card_size()
|
||||||
|
self.auto_card_width = read_auto_card_size()
|
||||||
self._last_card_width = self.card_width
|
self._last_card_width = self.card_width
|
||||||
self.setWindowTitle(f"{app_name} {version}")
|
self.setWindowTitle(f"{app_name} {version}")
|
||||||
self.setMinimumSize(800, 600)
|
self.setMinimumSize(800, 600)
|
||||||
@@ -72,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)
|
||||||
|
|
||||||
@@ -260,6 +263,10 @@ class MainWindow(QMainWindow):
|
|||||||
GamepadType.XBOX: "xbox_y",
|
GamepadType.XBOX: "xbox_y",
|
||||||
GamepadType.PLAYSTATION: "ps_square",
|
GamepadType.PLAYSTATION: "ps_square",
|
||||||
},
|
},
|
||||||
|
'prev_dir': {
|
||||||
|
GamepadType.XBOX: "xbox_y",
|
||||||
|
GamepadType.PLAYSTATION: "ps_square",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return mappings.get(action, {}).get(gtype, "placeholder")
|
return mappings.get(action, {}).get(gtype, "placeholder")
|
||||||
|
|
||||||
@@ -452,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)
|
||||||
@@ -558,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)
|
||||||
@@ -874,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"))
|
||||||
@@ -894,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()
|
||||||
@@ -1096,8 +1148,7 @@ class MainWindow(QMainWindow):
|
|||||||
autoInstallPage = QWidget()
|
autoInstallPage = QWidget()
|
||||||
autoInstallPage.setStyleSheet(self.theme.LIBRARY_WIDGET_STYLE)
|
autoInstallPage.setStyleSheet(self.theme.LIBRARY_WIDGET_STYLE)
|
||||||
autoInstallLayout = QVBoxLayout(autoInstallPage)
|
autoInstallLayout = QVBoxLayout(autoInstallPage)
|
||||||
autoInstallLayout.setContentsMargins(20, 0, 20, 0)
|
autoInstallLayout.setSpacing(15)
|
||||||
autoInstallLayout.setSpacing(0)
|
|
||||||
|
|
||||||
# Верхняя панель с заголовком и поиском
|
# Верхняя панель с заголовком и поиском
|
||||||
headerWidget = QWidget()
|
headerWidget = QWidget()
|
||||||
@@ -1146,6 +1197,25 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
autoInstallLayout.addWidget(self.autoInstallScrollArea)
|
autoInstallLayout.addWidget(self.autoInstallScrollArea)
|
||||||
|
|
||||||
|
# Slider for card size
|
||||||
|
sliderLayout = QHBoxLayout()
|
||||||
|
sliderLayout.setSpacing(0)
|
||||||
|
sliderLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
sliderLayout.addStretch()
|
||||||
|
|
||||||
|
self.auto_size_slider = QSlider(Qt.Orientation.Horizontal)
|
||||||
|
self.auto_size_slider.setMinimum(200)
|
||||||
|
self.auto_size_slider.setMaximum(250)
|
||||||
|
self.auto_size_slider.setValue(self.auto_card_width)
|
||||||
|
self.auto_size_slider.setTickInterval(10)
|
||||||
|
self.auto_size_slider.setFixedWidth(150)
|
||||||
|
self.auto_size_slider.setToolTip(f"{self.auto_card_width} px")
|
||||||
|
self.auto_size_slider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
|
||||||
|
self.auto_size_slider.sliderReleased.connect(self.on_auto_slider_released)
|
||||||
|
sliderLayout.addWidget(self.auto_size_slider)
|
||||||
|
|
||||||
|
autoInstallLayout.addLayout(sliderLayout)
|
||||||
|
|
||||||
# Хранение карточек
|
# Хранение карточек
|
||||||
self.autoInstallGameCards = {}
|
self.autoInstallGameCards = {}
|
||||||
self.allAutoInstallCards = []
|
self.allAutoInstallCards = []
|
||||||
@@ -1155,7 +1225,7 @@ class MainWindow(QMainWindow):
|
|||||||
if exe_name in self.autoInstallGameCards and local_path:
|
if exe_name in self.autoInstallGameCards and local_path:
|
||||||
card = self.autoInstallGameCards[exe_name]
|
card = self.autoInstallGameCards[exe_name]
|
||||||
card.cover_path = local_path
|
card.cover_path = local_path
|
||||||
load_pixmap_async(local_path, self.card_width, int(self.card_width * 1.5), card.on_cover_loaded)
|
load_pixmap_async(local_path, self.auto_card_width, int(self.auto_card_width * 1.5), card.on_cover_loaded)
|
||||||
|
|
||||||
# Загрузка игр
|
# Загрузка игр
|
||||||
def on_autoinstall_games_loaded(games: list[tuple]):
|
def on_autoinstall_games_loaded(games: list[tuple]):
|
||||||
@@ -1191,7 +1261,7 @@ class MainWindow(QMainWindow):
|
|||||||
None, None, None, game_source,
|
None, None, None, game_source,
|
||||||
select_callback=select_callback,
|
select_callback=select_callback,
|
||||||
theme=self.theme,
|
theme=self.theme,
|
||||||
card_width=self.card_width,
|
card_width=self.auto_card_width,
|
||||||
parent=self.autoInstallContainer,
|
parent=self.autoInstallContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1229,10 +1299,30 @@ 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)
|
||||||
|
|
||||||
|
def on_auto_slider_released(self):
|
||||||
|
"""Handles auto-install slider release to update card size."""
|
||||||
|
if hasattr(self, 'auto_size_slider') and self.auto_size_slider:
|
||||||
|
self.auto_card_width = self.auto_size_slider.value()
|
||||||
|
self.auto_size_slider.setToolTip(f"{self.auto_card_width} px")
|
||||||
|
save_auto_card_size(self.auto_card_width)
|
||||||
|
for card in self.allAutoInstallCards:
|
||||||
|
card.update_card_size(self.auto_card_width)
|
||||||
|
self.autoInstallContainerLayout.invalidate()
|
||||||
|
self.autoInstallContainer.updateGeometry()
|
||||||
|
self.autoInstallScrollArea.updateGeometry()
|
||||||
|
|
||||||
def filterAutoInstallGames(self):
|
def filterAutoInstallGames(self):
|
||||||
"""Filter auto install game cards based on search text."""
|
"""Filter auto install game cards based on search text."""
|
||||||
search_text = self.autoInstallSearchLineEdit.text().lower().strip()
|
search_text = self.autoInstallSearchLineEdit.text().lower().strip()
|
||||||
@@ -1380,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)
|
||||||
@@ -1464,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(
|
||||||
@@ -1482,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()
|
||||||
@@ -1585,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."))
|
||||||
@@ -1605,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."))
|
||||||
@@ -1856,7 +1878,19 @@ class MainWindow(QMainWindow):
|
|||||||
self.fullscreenCheckBox.setChecked(current_fullscreen)
|
self.fullscreenCheckBox.setChecked(current_fullscreen)
|
||||||
formLayout.addRow(self.fullscreenTitle, self.fullscreenCheckBox)
|
formLayout.addRow(self.fullscreenTitle, self.fullscreenCheckBox)
|
||||||
|
|
||||||
# 7. Automatic fullscreen on gamepad connection
|
# 7. Minimize to tray setting
|
||||||
|
self.minimizeToTrayCheckBox = QCheckBox(_("Minimize to tray on close"))
|
||||||
|
self.minimizeToTrayCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
|
self.minimizeToTrayCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.minimizeToTrayTitle = QLabel(_("Application Close Mode:"))
|
||||||
|
self.minimizeToTrayTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
|
self.minimizeToTrayTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
|
current_minimize_to_tray = read_minimize_to_tray()
|
||||||
|
self.minimizeToTrayCheckBox.setChecked(current_minimize_to_tray)
|
||||||
|
self.minimizeToTrayCheckBox.toggled.connect(lambda checked: save_minimize_to_tray(checked))
|
||||||
|
formLayout.addRow(self.minimizeToTrayTitle, self.minimizeToTrayCheckBox)
|
||||||
|
|
||||||
|
# 8. Automatic fullscreen on gamepad connection
|
||||||
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
|
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
|
||||||
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
@@ -1868,7 +1902,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
|
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
|
||||||
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
|
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
|
||||||
|
|
||||||
# 8. Gamepad haptic feedback config
|
# 9. Gamepad haptic feedback config
|
||||||
self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback"))
|
self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback"))
|
||||||
self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
@@ -2270,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 = {}
|
||||||
@@ -2572,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:
|
||||||
@@ -2647,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:
|
||||||
@@ -2655,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()
|
||||||
@@ -2879,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]
|
||||||
|
|
||||||
@@ -2976,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)
|
||||||
@@ -3004,57 +3045,77 @@ class MainWindow(QMainWindow):
|
|||||||
logger.error(f"Failed to launch game {exe_name}: {e}")
|
logger.error(f"Failed to launch game {exe_name}: {e}")
|
||||||
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
||||||
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Обработчик закрытия окна: сворачивает приложение в трей, если не требуется принудительный выход."""
|
"""Обработчик закрытия окна: проверяет настройку minimize_to_tray.
|
||||||
if hasattr(self, 'is_exiting') and self.is_exiting:
|
Если True — сворачиваем в трей (по умолчанию). Иначе — полностью закрываем.
|
||||||
# Принудительное закрытие: завершаем процессы и приложение
|
"""
|
||||||
for proc in self.game_processes:
|
minimize_to_tray = read_minimize_to_tray()
|
||||||
try:
|
|
||||||
parent = psutil.Process(proc.pid)
|
|
||||||
children = parent.children(recursive=True)
|
|
||||||
for child in children:
|
|
||||||
try:
|
|
||||||
logger.debug(f"Terminating child process {child.pid}")
|
|
||||||
child.terminate()
|
|
||||||
except psutil.NoSuchProcess:
|
|
||||||
logger.debug(f"Child process {child.pid} already terminated")
|
|
||||||
psutil.wait_procs(children, timeout=5)
|
|
||||||
for child in children:
|
|
||||||
if child.is_running():
|
|
||||||
logger.debug(f"Killing child process {child.pid}")
|
|
||||||
child.kill()
|
|
||||||
logger.debug(f"Terminating process group {proc.pid}")
|
|
||||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
||||||
except (psutil.NoSuchProcess, ProcessLookupError) as e:
|
|
||||||
logger.debug(f"Process {proc.pid} already terminated: {e}")
|
|
||||||
|
|
||||||
self.game_processes = [] # Очищаем список процессов
|
if minimize_to_tray:
|
||||||
|
# Просто сворачиваем в трей
|
||||||
# Очищаем таймеры
|
|
||||||
if hasattr(self, 'games_load_timer') and self.games_load_timer is not None and self.games_load_timer.isActive():
|
|
||||||
self.games_load_timer.stop()
|
|
||||||
if hasattr(self, 'settingsDebounceTimer') and self.settingsDebounceTimer is not None and self.settingsDebounceTimer.isActive():
|
|
||||||
self.settingsDebounceTimer.stop()
|
|
||||||
if hasattr(self, 'searchDebounceTimer') and self.searchDebounceTimer is not None and self.searchDebounceTimer.isActive():
|
|
||||||
self.searchDebounceTimer.stop()
|
|
||||||
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None and self.checkProcessTimer.isActive():
|
|
||||||
self.checkProcessTimer.stop()
|
|
||||||
self.checkProcessTimer.deleteLater()
|
|
||||||
self.checkProcessTimer = None
|
|
||||||
if hasattr(self, 'wine_monitor_timer') and self.wine_monitor_timer is not None:
|
|
||||||
self.wine_monitor_timer.stop()
|
|
||||||
self.wine_monitor_timer.deleteLater()
|
|
||||||
self.wine_monitor_timer = None
|
|
||||||
|
|
||||||
# Сохраняем настройки окна
|
|
||||||
if not read_fullscreen_config():
|
|
||||||
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
|
|
||||||
save_window_geometry(self.width(), self.height())
|
|
||||||
save_card_size(self.card_width)
|
|
||||||
|
|
||||||
event.accept()
|
|
||||||
else:
|
|
||||||
# Сворачиваем в трей вместо закрытия
|
|
||||||
self.hide()
|
|
||||||
event.ignore()
|
event.ignore()
|
||||||
|
self.hide()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Полное закрытие приложения
|
||||||
|
self.is_exiting = True
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
# Скрываем и удаляем иконку трея
|
||||||
|
if hasattr(self, "tray_manager") and self.tray_manager.tray_icon:
|
||||||
|
self.tray_manager.tray_icon.hide()
|
||||||
|
self.tray_manager.tray_icon.deleteLater()
|
||||||
|
|
||||||
|
# Сохраняем размеры карточек
|
||||||
|
save_card_size(self.card_width)
|
||||||
|
save_auto_card_size(self.auto_card_width)
|
||||||
|
|
||||||
|
# Сохраняем размеры окна (если не в полноэкранном режиме)
|
||||||
|
if not read_fullscreen_config():
|
||||||
|
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
|
||||||
|
save_window_geometry(self.width(), self.height())
|
||||||
|
|
||||||
|
# Завершаем все игровые процессы
|
||||||
|
for proc in getattr(self, "game_processes", []):
|
||||||
|
try:
|
||||||
|
parent = psutil.Process(proc.pid)
|
||||||
|
children = parent.children(recursive=True)
|
||||||
|
for child in children:
|
||||||
|
try:
|
||||||
|
logger.debug(f"Terminating child process {child.pid}")
|
||||||
|
child.terminate()
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
logger.debug(f"Child process {child.pid} already terminated")
|
||||||
|
|
||||||
|
psutil.wait_procs(children, timeout=5)
|
||||||
|
for child in children:
|
||||||
|
if child.is_running():
|
||||||
|
logger.debug(f"Killing child process {child.pid}")
|
||||||
|
child.kill()
|
||||||
|
|
||||||
|
logger.debug(f"Terminating process group {proc.pid}")
|
||||||
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||||
|
|
||||||
|
except (psutil.NoSuchProcess, ProcessLookupError) as e:
|
||||||
|
logger.debug(f"Process {getattr(proc, 'pid', '?')} already terminated: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to terminate process {getattr(proc, 'pid', '?')}: {e}")
|
||||||
|
|
||||||
|
self.game_processes = []
|
||||||
|
|
||||||
|
# Универсальная остановка и удаление таймеров
|
||||||
|
timers = [
|
||||||
|
"games_load_timer",
|
||||||
|
"settingsDebounceTimer",
|
||||||
|
"searchDebounceTimer",
|
||||||
|
"checkProcessTimer",
|
||||||
|
"wine_monitor_timer",
|
||||||
|
]
|
||||||
|
|
||||||
|
for tname in timers:
|
||||||
|
timer = getattr(self, tname, None)
|
||||||
|
if timer and timer.isActive():
|
||||||
|
timer.stop()
|
||||||
|
if timer:
|
||||||
|
timer.deleteLater()
|
||||||
|
setattr(self, tname, None)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 |
|
After Width: | Height: | Size: 232 KiB |
BIN
portprotonqt/themes/standart/images/screenshots/Библиотека.jpg
Normal file
|
After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
BIN
portprotonqt/themes/standart/images/screenshots/Карточка.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
BIN
portprotonqt/themes/standart/images/screenshots/Темы.jpg
Normal file
|
After Width: | Height: | Size: 93 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"""
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from typing import cast
|
from typing import cast, Any
|
||||||
from PySide6.QtWidgets import (QFrame, QVBoxLayout, QPushButton, QGridLayout,
|
from PySide6.QtWidgets import (QFrame, QVBoxLayout, QPushButton, QGridLayout,
|
||||||
QSizePolicy, QWidget, QLineEdit)
|
QSizePolicy, QWidget, QLineEdit)
|
||||||
from PySide6.QtCore import Qt, Signal, QProcess
|
from PySide6.QtCore import Qt, Signal, QProcess, QSize
|
||||||
|
from PySide6.QtGui import QPixmap, QIcon
|
||||||
from portprotonqt.keyboard_layouts import keyboard_layouts
|
from portprotonqt.keyboard_layouts import keyboard_layouts
|
||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.config_utils import read_theme_from_config
|
from portprotonqt.config_utils import read_theme_from_config
|
||||||
@@ -43,6 +44,18 @@ class VirtualKeyboard(QFrame):
|
|||||||
self.margins = 10
|
self.margins = 10
|
||||||
self.num_cols = 14
|
self.num_cols = 14
|
||||||
|
|
||||||
|
# Find input_manager and main_window
|
||||||
|
self.input_manager: Any = None
|
||||||
|
self.main_window: Any = None
|
||||||
|
parent_widget: QWidget | None = self._parent
|
||||||
|
while parent_widget:
|
||||||
|
if hasattr(parent_widget, 'input_manager'):
|
||||||
|
self.input_manager = cast(Any, parent_widget).input_manager
|
||||||
|
self.main_window = cast(Any, parent_widget)
|
||||||
|
parent_widget = cast(QWidget | None, parent_widget.parent())
|
||||||
|
|
||||||
|
|
||||||
|
self.current_theme_name = read_theme_from_config()
|
||||||
self.initUI()
|
self.initUI()
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
@@ -119,6 +132,34 @@ class VirtualKeyboard(QFrame):
|
|||||||
self.buttons: dict[str, QPushButton] = {}
|
self.buttons: dict[str, QPushButton] = {}
|
||||||
self.update_keyboard()
|
self.update_keyboard()
|
||||||
|
|
||||||
|
def set_gamepad_icon(self, button, icon_type, gtype=''):
|
||||||
|
"""Set gamepad icon on button based on type"""
|
||||||
|
if icon_type in ['back', 'add_game']:
|
||||||
|
icon_name = self.main_window.get_button_icon(icon_type, gtype)
|
||||||
|
else: # nav left/right
|
||||||
|
if icon_type in ['left', 'right']:
|
||||||
|
direction = icon_type
|
||||||
|
icon_name = self.main_window.get_nav_icon(direction, gtype)
|
||||||
|
else:
|
||||||
|
direction = 'left' if icon_type == 'left' else 'right'
|
||||||
|
icon_name = self.main_window.get_nav_icon(direction, gtype)
|
||||||
|
|
||||||
|
icon_path = theme_manager.get_theme_image(icon_name, self.current_theme_name)
|
||||||
|
pixmap = QPixmap()
|
||||||
|
if icon_path:
|
||||||
|
pixmap.load(str(icon_path))
|
||||||
|
if not pixmap.isNull():
|
||||||
|
button.setIcon(QIcon(pixmap))
|
||||||
|
button.setIconSize(QSize(20, 20))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Fallback to placeholder
|
||||||
|
placeholder = theme_manager.get_theme_image("placeholder", self.current_theme_name)
|
||||||
|
if placeholder:
|
||||||
|
button.setIcon(QIcon(placeholder))
|
||||||
|
button.setIconSize(QSize(20, 20))
|
||||||
|
return
|
||||||
|
|
||||||
def update_keyboard(self):
|
def update_keyboard(self):
|
||||||
coords = self._save_focused_coords()
|
coords = self._save_focused_coords()
|
||||||
|
|
||||||
@@ -151,6 +192,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
button.setCheckable(True)
|
button.setCheckable(True)
|
||||||
button.setChecked(self.shift_pressed)
|
button.setChecked(self.shift_pressed)
|
||||||
button.clicked.connect(lambda checked: self.on_shift_click(checked))
|
button.clicked.connect(lambda checked: self.on_shift_click(checked))
|
||||||
|
# Add gamepad icon for Shift (RB/R)
|
||||||
|
gtype = self.input_manager.gamepad_type
|
||||||
|
self.set_gamepad_icon(button, 'right', gtype)
|
||||||
else:
|
else:
|
||||||
button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k))
|
button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k))
|
||||||
|
|
||||||
@@ -163,6 +207,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
shift.setCheckable(True)
|
shift.setCheckable(True)
|
||||||
shift.setChecked(self.shift_pressed)
|
shift.setChecked(self.shift_pressed)
|
||||||
shift.clicked.connect(lambda checked: self.on_shift_click(checked))
|
shift.clicked.connect(lambda checked: self.on_shift_click(checked))
|
||||||
|
# Add gamepad icon for Shift (RB/R)
|
||||||
|
gtype = self.input_manager.gamepad_type
|
||||||
|
self.set_gamepad_icon(shift, 'right', gtype)
|
||||||
self.keyboard_layout.addWidget(shift, 3, 11, 1, 3)
|
self.keyboard_layout.addWidget(shift, 3, 11, 1, 3)
|
||||||
|
|
||||||
button = QPushButton('CAPS')
|
button = QPushButton('CAPS')
|
||||||
@@ -179,6 +226,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
backspace.setFixedSize(fixed_w, fixed_h)
|
backspace.setFixedSize(fixed_w, fixed_h)
|
||||||
backspace.pressed.connect(self.on_backspace_pressed)
|
backspace.pressed.connect(self.on_backspace_pressed)
|
||||||
backspace.released.connect(self.stop_backspace_repeat)
|
backspace.released.connect(self.stop_backspace_repeat)
|
||||||
|
# Add gamepad icon for Backspace (X/Triangle)
|
||||||
|
gtype = self.input_manager.gamepad_type
|
||||||
|
self.set_gamepad_icon(backspace, 'add_game', gtype)
|
||||||
self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1)
|
self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1)
|
||||||
|
|
||||||
enter = QPushButton('Enter')
|
enter = QPushButton('Enter')
|
||||||
@@ -189,6 +239,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
lang = QPushButton('🌐')
|
lang = QPushButton('🌐')
|
||||||
lang.setFixedSize(fixed_w, fixed_h)
|
lang.setFixedSize(fixed_w, fixed_h)
|
||||||
lang.clicked.connect(self.on_lang_click)
|
lang.clicked.connect(self.on_lang_click)
|
||||||
|
# Add gamepad icon for Lang (LB/L)
|
||||||
|
gtype = self.input_manager.gamepad_type
|
||||||
|
self.set_gamepad_icon(lang, 'left', gtype)
|
||||||
self.keyboard_layout.addWidget(lang, 4, 0, 1, 1)
|
self.keyboard_layout.addWidget(lang, 4, 0, 1, 1)
|
||||||
|
|
||||||
clear = QPushButton('Clear')
|
clear = QPushButton('Clear')
|
||||||
@@ -219,6 +272,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
hide_button = QPushButton('Hide')
|
hide_button = QPushButton('Hide')
|
||||||
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
||||||
hide_button.clicked.connect(self.hide)
|
hide_button.clicked.connect(self.hide)
|
||||||
|
# Add gamepad icon for Hide (B/Circle)
|
||||||
|
gtype = self.input_manager.gamepad_type
|
||||||
|
self.set_gamepad_icon(hide_button, 'back', gtype)
|
||||||
self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2)
|
self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2)
|
||||||
|
|
||||||
if coords:
|
if coords:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "portprotonqt"
|
name = "portprotonqt"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "GPL-3.0" }
|
license = { text = "GPL-3.0" }
|
||||||
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||