8 Commits

Author SHA1 Message Date
dde43f69d1 feat: trigger emulation by Xbox + B
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-07 22:22:38 +05:00
e07f3f06bc chore(build): return QtSvg to appimage
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-04 12:28:09 +05:00
16a3f4e09a chore(build): added udev rule to allow create virtual devices
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-04 11:14:17 +05:00
a448ba29b0 feat(input_manager): added mouse emulation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-03 12:34:27 +05:00
06e55db54d feat(settings): update styles
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-02 16:05:22 +05:00
5fce23f261 chore: disable pre-commit auto update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-02 15:31:01 +05:00
Renovate Bot
96ad40d625 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.3 2025-11-02 00:01:26 +00:00
Gitea Actions
a30f6f2e74 chore: update steam apps list 2025-11-01T00:01:57Z 2025-11-01 00:01:58 +00:00
19 changed files with 12218 additions and 1404 deletions

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ repos:
- 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.2 rev: v0.14.3
hooks: hooks:
- id: ruff-check - id: ruff-check

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

Binary file not shown.

View File

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

View File

@@ -5,7 +5,7 @@ from typing import cast, TYPE_CHECKING
from PySide6.QtGui import QPixmap, QIcon, QTextCursor, QColor 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, QComboBox 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
@@ -1673,6 +1673,7 @@ class WinetricksDialog(QDialog):
if self.input_manager: if self.input_manager:
self.input_manager.disable_winetricks_mode() self.input_manager.disable_winetricks_mode()
super().reject() super().reject()
class ExeSettingsDialog(QDialog): class ExeSettingsDialog(QDialog):
def __init__(self, parent=None, theme=None, exe_path=None): def __init__(self, parent=None, theme=None, exe_path=None):
super().__init__(parent) super().__init__(parent)
@@ -1699,7 +1700,6 @@ class ExeSettingsDialog(QDialog):
self.locale_options = [] self.locale_options = []
self.logical_core_options = [] self.logical_core_options = []
self.amd_vulkan_drivers = [] self.amd_vulkan_drivers = []
self.branch_name = _("Unknown")
self.setWindowTitle(_("Exe Settings")) self.setWindowTitle(_("Exe Settings"))
self.setModal(True) self.setModal(True)
@@ -2136,12 +2136,12 @@ class ExeSettingsDialog(QDialog):
self.original_display_values[setting['key']] = current_val self.original_display_values[setting['key']] = current_val
elif setting['type'] == 'text': elif setting['type'] == 'text':
text_edit = QTextEdit() line_edit = QLineEdit()
current_val = current.get(setting['key'], setting['default']) current_val = current.get(setting['key'], setting['default'])
text_edit.setPlainText(current_val) line_edit.setText(current_val)
self.advanced_table.setCellWidget(row, 1, text_edit) self.advanced_table.setCellWidget(row, 1, line_edit)
self.advanced_widgets[setting['key']] = text_edit self.advanced_widgets[setting['key']] = line_edit
self.original_display_values[setting['key']] = current_val self.original_display_values[setting['key']] = current_val
# Description column # Description column
@@ -2151,15 +2151,27 @@ class ExeSettingsDialog(QDialog):
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
self.advanced_table.setItem(row, 2, desc_item) self.advanced_table.setItem(row, 2, desc_item)
self.advanced_table.resizeRowsToContents() # Make sure QLineEdit and QComboBox look consistent
if self.advanced_table.rowCount() > 0: self.advanced_table.setStyleSheet("""
self.advanced_table.setCurrentCell(0, 0) QComboBox, QLineEdit {
padding: 3px 6px;
min-height: 26px;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 18px;
}
QTextEdit {
border-radius: 4px;
padding: 4px;
}
""")
def apply_changes(self): def apply_changes(self):
"""Apply changes by collecting diffs from both main and advanced tabs.""" """Apply changes by collecting diffs from both main and advanced tabs."""
changes = [] changes = []
# --- 1. Обычные (toggle) настройки ---
for key, orig_val in self.original_values.items(): for key, orig_val in self.original_values.items():
if key in self.blocked_keys: if key in self.blocked_keys:
continue # Skip blocked keys continue # Skip blocked keys
@@ -2180,28 +2192,24 @@ class ExeSettingsDialog(QDialog):
if new_val != orig_val: if new_val != orig_val:
changes.append(f"{key}={new_val}") changes.append(f"{key}={new_val}")
# --- 2. Advanced настройки ---
for key, widget in self.advanced_widgets.items(): for key, widget in self.advanced_widgets.items():
orig_val = self.original_display_values.get(key, '') orig_val = self.original_display_values.get(key, '')
if isinstance(widget, QComboBox): if isinstance(widget, QComboBox):
new_val = widget.currentText() new_val = widget.currentText()
# приведение disabled к 'disabled'
if new_val.lower() == _('disabled').lower(): if new_val.lower() == _('disabled').lower():
new_val = 'disabled' new_val = 'disabled'
elif isinstance(widget, QTextEdit): elif isinstance(widget, QLineEdit):
new_val = widget.toPlainText().strip() new_val = widget.text().strip()
else: else:
continue continue
if new_val != orig_val: if new_val != orig_val:
changes.append(f"{key}={new_val}") changes.append(f"{key}={new_val}")
# --- 3. Проверка на изменения ---
if not changes: if not changes:
QMessageBox.information(self, _("Info"), _("No changes to apply.")) QMessageBox.information(self, _("Info"), _("No changes to apply."))
return return
# --- 4. Запуск процесса сохранения ---
process = QProcess(self) process = QProcess(self)
process.finished.connect(self.on_edit_db_finished) process.finished.connect(self.on_edit_db_finished)
args = ["cli", "--edit-db", self.exe_path] + changes args = ["cli", "--edit-db", self.exe_path] + changes

View File

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

View File

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