forked from Boria138/PortProtonQt
Compare commits
7 Commits
0231073b19
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e07f3f06bc
|
|||
|
16a3f4e09a
|
|||
|
a448ba29b0
|
|||
|
06e55db54d
|
|||
|
5fce23f261
|
|||
|
|
96ad40d625 | ||
|
|
a30f6f2e74 |
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||
python3-build pyproject-rpm-macros python3-setuptools \
|
||||
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
|
||||
redhat-rpm-config nodejs npm
|
||||
|
||||
- name: Setup rpmbuild environment
|
||||
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||
python3-build pyproject-rpm-macros python3-setuptools \
|
||||
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
|
||||
redhat-rpm-config nodejs npm
|
||||
|
||||
- name: Setup rpmbuild environment
|
||||
|
||||
@@ -16,7 +16,7 @@ repos:
|
||||
- id: uv-lock
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.2
|
||||
rev: v0.14.3
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ script:
|
||||
- uv pip install --no-cache-dir ../
|
||||
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
|
||||
- cp -r share AppDir/usr
|
||||
- cp -r lib AppDir/usr
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetwork*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
|
||||
- shopt -s extglob
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Network*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
|
||||
AppDir:
|
||||
path: ./AppDir
|
||||
after_bundle:
|
||||
|
||||
@@ -20,4 +20,5 @@ package() {
|
||||
cd "$srcdir/PortProtonQt"
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
cp -r build-aux/share "$pkgdir/usr/"
|
||||
cp -r build-aux/lib "$pkgdir/usr/"
|
||||
}
|
||||
|
||||
@@ -25,4 +25,5 @@ package() {
|
||||
cd "$srcdir/PortProtonQt"
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
cp -r build-aux/share "$pkgdir/usr/"
|
||||
cp -r build-aux/lib "$pkgdir/usr/"
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ BuildRequires: python3-build
|
||||
BuildRequires: pyproject-rpm-macros
|
||||
BuildRequires: python3dist(setuptools)
|
||||
BuildRequires: git
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
%description
|
||||
%{summary}
|
||||
@@ -69,11 +70,13 @@ cd %{oname}
|
||||
%pyproject_install
|
||||
%pyproject_save_files %{pypi_name}
|
||||
cp -r build-aux/share %{buildroot}/usr/
|
||||
cp -r build-aux/lib %{buildroot}/usr/
|
||||
|
||||
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
||||
%{_bindir}/%{pypi_name}
|
||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||
%{_udevrulesdir}/60-portprotonqt.rules
|
||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||
%{bash_completions_dir}/portprotonqt
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ BuildRequires: python3-build
|
||||
BuildRequires: pyproject-rpm-macros
|
||||
BuildRequires: python3dist(setuptools)
|
||||
BuildRequires: git
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
%description
|
||||
%{summary}
|
||||
@@ -68,11 +69,13 @@ cd %{oname}
|
||||
%pyproject_install
|
||||
%pyproject_save_files %{pypi_name}
|
||||
cp -r build-aux/share %{buildroot}/usr/
|
||||
cp -r build-aux/lib %{buildroot}/usr/
|
||||
|
||||
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
||||
%{_bindir}/%{pypi_name}
|
||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||
%{_udevrulesdir}/60-portprotonqt.rules
|
||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||
%{bash_completions_dir}/portprotonqt
|
||||
|
||||
|
||||
1
build-aux/lib/udev/rules.d/60-portprotonqt.rules
Normal file
1
build-aux/lib/udev/rules.d/60-portprotonqt.rules
Normal file
@@ -0,0 +1 @@
|
||||
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"
|
||||
@@ -1021,7 +1021,7 @@
|
||||
},
|
||||
{
|
||||
"normalized_name": "farlight 84",
|
||||
"status": "Supported"
|
||||
"status": "Denied"
|
||||
},
|
||||
{
|
||||
"normalized_name": "riders republic",
|
||||
@@ -1436,8 +1436,8 @@
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "blue protocol",
|
||||
"status": "Broken"
|
||||
"normalized_name": "blue protocol star resonance",
|
||||
"status": "Running"
|
||||
},
|
||||
{
|
||||
"normalized_name": "dark and darker",
|
||||
|
||||
Binary file not shown.
12972
data/games_appid.json
12972
data/games_appid.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,4 +1,108 @@
|
||||
[
|
||||
{
|
||||
"normalized_title": "split/second",
|
||||
"slug": "split-second"
|
||||
},
|
||||
{
|
||||
"normalized_title": "warzone 2100",
|
||||
"slug": "warzone-2100"
|
||||
},
|
||||
{
|
||||
"normalized_title": "foundation",
|
||||
"slug": "foundation"
|
||||
},
|
||||
{
|
||||
"normalized_title": "земский собор [демо]",
|
||||
"slug": "zemskij-sobor-demo"
|
||||
},
|
||||
{
|
||||
"normalized_title": "crusader kings 3",
|
||||
"slug": "crusader-kings-3"
|
||||
},
|
||||
{
|
||||
"normalized_title": "nadir a grimdark deck builder",
|
||||
"slug": "nadir-a-grimdark-deck-builder"
|
||||
},
|
||||
{
|
||||
"normalized_title": "oriental empires",
|
||||
"slug": "oriental-empires"
|
||||
},
|
||||
{
|
||||
"normalized_title": "vampire the masquerade bloodlines 2",
|
||||
"slug": "vampire-the-masquerade-bloodlines-2"
|
||||
},
|
||||
{
|
||||
"normalized_title": "escape from duckov",
|
||||
"slug": "escape-from-duckov"
|
||||
},
|
||||
{
|
||||
"normalized_title": "xiii",
|
||||
"slug": "xiii"
|
||||
},
|
||||
{
|
||||
"normalized_title": "saints row 2",
|
||||
"slug": "saints-row-2"
|
||||
},
|
||||
{
|
||||
"normalized_title": "frozenheim",
|
||||
"slug": "frozenheim"
|
||||
},
|
||||
{
|
||||
"normalized_title": "saints row (2022)",
|
||||
"slug": "saints-row-2022"
|
||||
},
|
||||
{
|
||||
"normalized_title": "iron harvest",
|
||||
"slug": "iron-harvest"
|
||||
},
|
||||
{
|
||||
"normalized_title": "tom clancy's splinter cell blacklist",
|
||||
"slug": "tom-clancys-splinter-cell-blacklist"
|
||||
},
|
||||
{
|
||||
"normalized_title": "painkiller overdose",
|
||||
"slug": "painkiller-overdose"
|
||||
},
|
||||
{
|
||||
"normalized_title": "ancestors legacy",
|
||||
"slug": "ancestors-legacy"
|
||||
},
|
||||
{
|
||||
"normalized_title": "bye sweet carole",
|
||||
"slug": "bye-sweet-carole"
|
||||
},
|
||||
{
|
||||
"normalized_title": "painkiller black",
|
||||
"slug": "painkiller-black-edition"
|
||||
},
|
||||
{
|
||||
"normalized_title": "hogwarts legacy",
|
||||
"slug": "hogwarts-legacy"
|
||||
},
|
||||
{
|
||||
"normalized_title": "active matter",
|
||||
"slug": "active-matter"
|
||||
},
|
||||
{
|
||||
"normalized_title": "tom clancy's splinter cell",
|
||||
"slug": "tom-clancys-splinter-cell"
|
||||
},
|
||||
{
|
||||
"normalized_title": "sniper ghost warrior",
|
||||
"slug": "sniper-ghost-warrior"
|
||||
},
|
||||
{
|
||||
"normalized_title": "fate undiscovered realms",
|
||||
"slug": "fate-undiscovered-realms"
|
||||
},
|
||||
{
|
||||
"normalized_title": "dying light the beast deluxe",
|
||||
"slug": "dying-light-the-beast-deluxe-edition"
|
||||
},
|
||||
{
|
||||
"normalized_title": "spellforce platinum",
|
||||
"slug": "spellforce-platinum-edition"
|
||||
},
|
||||
{
|
||||
"normalized_title": "dirt rally 2.0 game of the year",
|
||||
"slug": "dirt-rally-2-0-game-of-the-year-edition"
|
||||
@@ -271,10 +375,6 @@
|
||||
"normalized_title": "steins;gate the distant valhalla",
|
||||
"slug": "steins-gate-the-distant-valhalla"
|
||||
},
|
||||
{
|
||||
"normalized_title": "hogwarts legacy",
|
||||
"slug": "hogwarts-legacy"
|
||||
},
|
||||
{
|
||||
"normalized_title": "osu!",
|
||||
"slug": "osu"
|
||||
|
||||
Binary file not shown.
@@ -17,17 +17,31 @@ import json
|
||||
|
||||
|
||||
class PySide6DependencyAnalyzer:
|
||||
def __init__(self):
|
||||
def __init__(self, project_root: Path = None):
|
||||
# Системные библиотеки, которые нужно всегда оставлять
|
||||
self.system_libs = {
|
||||
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
||||
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
|
||||
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
|
||||
'libQt6Svg'
|
||||
}
|
||||
|
||||
self.critical_modules = {
|
||||
'QtSvg',
|
||||
}
|
||||
|
||||
self.real_dependencies = {}
|
||||
self.used_modules_code = set()
|
||||
self.used_modules_ldd = set()
|
||||
self.all_required_modules = set()
|
||||
# Определяем корень проекта
|
||||
if project_root is None:
|
||||
# Корень проекта - две директории выше от скрипта
|
||||
self.project_root = Path(__file__).parent.parent
|
||||
else:
|
||||
self.project_root = project_root
|
||||
|
||||
self.venv_path = self.project_root / ".venv"
|
||||
self.build_path = self.project_root / "build-aux"
|
||||
|
||||
def find_python_files(self, directory: Path) -> List[Path]:
|
||||
"""Находит все Python файлы в директории"""
|
||||
@@ -44,24 +58,61 @@ class PySide6DependencyAnalyzer:
|
||||
"""Находит все PySide6 библиотеки (.so файлы)"""
|
||||
libs = {}
|
||||
|
||||
# Поиск в единственной локации
|
||||
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
|
||||
print(f"Поиск PySide6 библиотек в: {search_path}")
|
||||
# Ищем venv в корне проекта
|
||||
venv_candidates = [
|
||||
self.venv_path, # .venv
|
||||
self.project_root / "venv",
|
||||
self.project_root / ".virtualenv",
|
||||
]
|
||||
|
||||
if search_path.exists():
|
||||
# Ищем .so файлы модулей
|
||||
for so_file in search_path.glob("Qt*.*.so"):
|
||||
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
|
||||
if module_name.startswith('Qt'):
|
||||
libs[module_name] = so_file
|
||||
pyside6_path = None
|
||||
|
||||
# Также ищем в подпапках
|
||||
for subdir in search_path.iterdir():
|
||||
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
||||
for so_file in subdir.glob("*.so*"):
|
||||
if 'Qt' in so_file.name:
|
||||
libs[subdir.name] = so_file
|
||||
break
|
||||
# Пробуем найти PySide6 в venv
|
||||
for venv in venv_candidates:
|
||||
if venv.exists():
|
||||
# Ищем Python версию
|
||||
lib_path = venv / "lib"
|
||||
if lib_path.exists():
|
||||
for python_dir in lib_path.iterdir():
|
||||
if python_dir.name.startswith('python'):
|
||||
candidate = python_dir / "site-packages" / "PySide6"
|
||||
if candidate.exists():
|
||||
pyside6_path = candidate
|
||||
print(f"Найден PySide6 в: {candidate}")
|
||||
break
|
||||
if pyside6_path:
|
||||
break
|
||||
|
||||
if not pyside6_path:
|
||||
print(f"Предупреждение: PySide6 не найден в venv, проверяем AppDir...")
|
||||
# Если не нашли в venv, пробуем в AppDir
|
||||
if base_path:
|
||||
appdir_candidate = base_path / "AppDir/usr/local/lib"
|
||||
if appdir_candidate.exists():
|
||||
for python_dir in appdir_candidate.iterdir():
|
||||
if python_dir.name.startswith('python'):
|
||||
candidate = python_dir / "dist-packages" / "PySide6"
|
||||
if candidate.exists():
|
||||
pyside6_path = candidate
|
||||
print(f"Найден PySide6 в AppDir: {candidate}")
|
||||
break
|
||||
|
||||
if not pyside6_path:
|
||||
return libs
|
||||
|
||||
# Ищем .so файлы модулей
|
||||
for so_file in pyside6_path.glob("Qt*.*.so"):
|
||||
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
|
||||
if module_name.startswith('Qt'):
|
||||
libs[module_name] = so_file
|
||||
|
||||
# Также ищем в подпапках
|
||||
for subdir in pyside6_path.iterdir():
|
||||
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
||||
for so_file in subdir.glob("*.so*"):
|
||||
if 'Qt' in so_file.name:
|
||||
libs[subdir.name] = so_file
|
||||
break
|
||||
|
||||
return libs
|
||||
|
||||
@@ -257,8 +308,10 @@ class PySide6DependencyAnalyzer:
|
||||
|
||||
# Модули для удаления
|
||||
if removable_modules:
|
||||
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
|
||||
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
||||
removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
|
||||
if removable_filtered:
|
||||
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_filtered)])
|
||||
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
||||
|
||||
# Генерируем команду для удаления нативных библиотек с сохранением нужных
|
||||
required_libs = set()
|
||||
@@ -276,39 +329,82 @@ class PySide6DependencyAnalyzer:
|
||||
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
|
||||
])
|
||||
|
||||
# Заменяем блок очистки в рецепте
|
||||
import re
|
||||
|
||||
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
|
||||
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
|
||||
# Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
|
||||
# Паттерн: после " - cp -r lib AppDir/usr\n" идут команды rm, а затем "AppDir:"
|
||||
pattern = r'( - cp -r lib AppDir/usr\n)((?: - (?:rm|shopt).*\n)*?)(?=AppDir:)'
|
||||
|
||||
new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines)
|
||||
match = re.search(pattern, recipe_content)
|
||||
|
||||
updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL)
|
||||
if not match:
|
||||
print("ПРЕДУПРЕЖДЕНИЕ: Не удалось найти блок очистки в рецепте")
|
||||
print("Добавляем команды очистки перед блоком AppDir:")
|
||||
|
||||
# Просто вставим команды перед AppDir:
|
||||
appdir_pos = recipe_content.find('AppDir:')
|
||||
if appdir_pos != -1:
|
||||
new_content = (
|
||||
recipe_content[:appdir_pos] +
|
||||
'\n'.join(cleanup_lines) + '\n' +
|
||||
recipe_content[appdir_pos:]
|
||||
)
|
||||
return new_content
|
||||
else:
|
||||
print("ОШИБКА: Не найден блок AppDir: в рецепте")
|
||||
return ""
|
||||
|
||||
# Создаем замену - группа 1 (cp -r lib) + новые команды очистки
|
||||
replacement = r'\1' + '\n'.join(cleanup_lines) + '\n'
|
||||
|
||||
updated_recipe = re.sub(pattern, replacement, recipe_content, count=1)
|
||||
|
||||
return updated_recipe
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
|
||||
parser.add_argument('project_path', help='Путь к проекту для анализа')
|
||||
parser.add_argument('project_path', nargs='?', default='.',
|
||||
help='Путь к проекту для анализа (по умолчанию: текущая директория)')
|
||||
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
|
||||
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
||||
parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
project_path = Path(args.project_path)
|
||||
project_path = Path(args.project_path).resolve()
|
||||
if not project_path.exists():
|
||||
print(f"Ошибка: путь {project_path} не существует")
|
||||
sys.exit(1)
|
||||
|
||||
appdir_path = Path(args.appdir) if args.appdir else None
|
||||
appdir_path = Path(args.appdir).resolve() if args.appdir else None
|
||||
if appdir_path and not appdir_path.exists():
|
||||
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
||||
appdir_path = None
|
||||
|
||||
analyzer = PySide6DependencyAnalyzer()
|
||||
# Определяем корень проекта
|
||||
# Если запущен из подпапки проекта, ищем корень
|
||||
project_root = project_path
|
||||
if (project_path / ".git").exists() or (project_path / "pyproject.toml").exists():
|
||||
project_root = project_path
|
||||
else:
|
||||
# Пытаемся найти корень проекта
|
||||
current = project_path
|
||||
while current != current.parent:
|
||||
if (current / ".git").exists() or (current / "pyproject.toml").exists():
|
||||
project_root = current
|
||||
break
|
||||
current = current.parent
|
||||
|
||||
print(f"Корень проекта: {project_root}")
|
||||
|
||||
analyzer = PySide6DependencyAnalyzer(project_root=project_root)
|
||||
|
||||
# Если указан custom venv путь
|
||||
if args.venv:
|
||||
analyzer.venv_path = Path(args.venv).resolve()
|
||||
print(f"Использую указанный venv: {analyzer.venv_path}")
|
||||
|
||||
results = analyzer.analyze_project(project_path, appdir_path)
|
||||
|
||||
# Сохраняем в анализатор для генерации команд
|
||||
@@ -347,13 +443,13 @@ def main():
|
||||
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
||||
|
||||
if args.verbose and results['real_dependencies']:
|
||||
Devlin(f"\nРеальные зависимости (ldd):")
|
||||
print(f"\nРеальные зависимости (ldd):")
|
||||
for module, deps in results['real_dependencies'].items():
|
||||
if deps:
|
||||
print(f" {module} → {', '.join(deps)}")
|
||||
|
||||
# Обновляем AppImage рецепт
|
||||
recipe_path = Path("../build-aux/AppImageBuilder.yml")
|
||||
recipe_path = analyzer.build_path / "AppImageBuilder.yml"
|
||||
if recipe_path.exists():
|
||||
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
||||
if updated_recipe:
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import cast, TYPE_CHECKING
|
||||
from PySide6.QtGui import QPixmap, QIcon, QTextCursor, QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
|
||||
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget, 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 icoextract import IconExtractor, IconExtractorError
|
||||
@@ -1673,6 +1673,7 @@ class WinetricksDialog(QDialog):
|
||||
if self.input_manager:
|
||||
self.input_manager.disable_winetricks_mode()
|
||||
super().reject()
|
||||
|
||||
class ExeSettingsDialog(QDialog):
|
||||
def __init__(self, parent=None, theme=None, exe_path=None):
|
||||
super().__init__(parent)
|
||||
@@ -1699,7 +1700,6 @@ class ExeSettingsDialog(QDialog):
|
||||
self.locale_options = []
|
||||
self.logical_core_options = []
|
||||
self.amd_vulkan_drivers = []
|
||||
self.branch_name = _("Unknown")
|
||||
|
||||
self.setWindowTitle(_("Exe Settings"))
|
||||
self.setModal(True)
|
||||
@@ -2136,12 +2136,12 @@ class ExeSettingsDialog(QDialog):
|
||||
self.original_display_values[setting['key']] = current_val
|
||||
|
||||
elif setting['type'] == 'text':
|
||||
text_edit = QTextEdit()
|
||||
line_edit = QLineEdit()
|
||||
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_widgets[setting['key']] = text_edit
|
||||
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
|
||||
@@ -2151,15 +2151,27 @@ class ExeSettingsDialog(QDialog):
|
||||
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
self.advanced_table.setItem(row, 2, desc_item)
|
||||
|
||||
self.advanced_table.resizeRowsToContents()
|
||||
if self.advanced_table.rowCount() > 0:
|
||||
self.advanced_table.setCurrentCell(0, 0)
|
||||
# Make sure QLineEdit and QComboBox look consistent
|
||||
self.advanced_table.setStyleSheet("""
|
||||
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):
|
||||
"""Apply changes by collecting diffs from both main and advanced tabs."""
|
||||
changes = []
|
||||
|
||||
# --- 1. Обычные (toggle) настройки ---
|
||||
for key, orig_val in self.original_values.items():
|
||||
if key in self.blocked_keys:
|
||||
continue # Skip blocked keys
|
||||
@@ -2180,28 +2192,24 @@ class ExeSettingsDialog(QDialog):
|
||||
if new_val != orig_val:
|
||||
changes.append(f"{key}={new_val}")
|
||||
|
||||
# --- 2. Advanced настройки ---
|
||||
for key, widget in self.advanced_widgets.items():
|
||||
orig_val = self.original_display_values.get(key, '')
|
||||
if isinstance(widget, QComboBox):
|
||||
new_val = widget.currentText()
|
||||
# приведение disabled к 'disabled'
|
||||
if new_val.lower() == _('disabled').lower():
|
||||
new_val = 'disabled'
|
||||
elif isinstance(widget, QTextEdit):
|
||||
new_val = widget.toPlainText().strip()
|
||||
elif isinstance(widget, QLineEdit):
|
||||
new_val = widget.text().strip()
|
||||
else:
|
||||
continue
|
||||
|
||||
if new_val != orig_val:
|
||||
changes.append(f"{key}={new_val}")
|
||||
|
||||
# --- 3. Проверка на изменения ---
|
||||
if not changes:
|
||||
QMessageBox.information(self, _("Info"), _("No changes to apply."))
|
||||
return
|
||||
|
||||
# --- 4. Запуск процесса сохранения ---
|
||||
process = QProcess(self)
|
||||
process.finished.connect(self.on_edit_db_finished)
|
||||
args = ["cli", "--edit-db", self.exe_path] + changes
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
import math
|
||||
from typing import Protocol, cast
|
||||
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
||||
from evdev import InputDevice, InputEvent, UInput, ecodes, list_devices, ff
|
||||
from enum import Enum
|
||||
from pyudev import Context, Monitor, Device, Devices
|
||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QTableWidgetItem
|
||||
@@ -15,6 +16,7 @@ from portprotonqt.game_card import GameCard
|
||||
from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config, read_gamepad_type
|
||||
from portprotonqt.dialogs import AddGameDialog
|
||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||
import select
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -115,6 +117,31 @@ class InputManager(QObject):
|
||||
self.last_trigger_time = 0.0
|
||||
self.trigger_cooldown = 0.2
|
||||
|
||||
# Mouse emulation attributes
|
||||
self.mouse_emulation_enabled = True # Enable by default as crutch for external apps
|
||||
self.ui = None # UInput for virtual mouse
|
||||
self.stick_x_raw = 0
|
||||
self.stick_y_raw = 0
|
||||
self.deadzone = 8000 # Deadzone for sticks
|
||||
self.max_value = 32767 # Max stick value
|
||||
self.sensitivity = 8.0 # Cursor sensitivity
|
||||
self.scroll_accumulator = 0.0
|
||||
self.scroll_sensitivity = 0.15 # Scroll sensitivity
|
||||
self.scroll_threshold = 0.2 # Scroll threshold
|
||||
self.last_update = time.time()
|
||||
self.update_interval = 0.016 # ~60 FPS
|
||||
self.emulation_active = False # Flag for external focus (updated in main thread)
|
||||
|
||||
# Focus check timer for emulation flag (runs in main thread)
|
||||
self.focus_check_timer = QTimer(self)
|
||||
self.focus_check_timer.timeout.connect(self._update_emulation_flag)
|
||||
self.focus_check_timer.start(100) # Check every 100ms
|
||||
|
||||
logger.info("EMUL: Mouse emulation initialized (enabled=%s)", self.mouse_emulation_enabled)
|
||||
|
||||
if self.mouse_emulation_enabled:
|
||||
self.enable_mouse_emulation()
|
||||
|
||||
# FileExplorer specific attributes
|
||||
self.file_explorer = None
|
||||
self.original_button_handler = None
|
||||
@@ -151,6 +178,11 @@ class InputManager(QObject):
|
||||
# Initialize evdev + hotplug
|
||||
self.init_gamepad()
|
||||
|
||||
def _update_emulation_flag(self):
|
||||
"""Update emulation_active flag based on Qt app focus (main thread only)."""
|
||||
active = QApplication.activeWindow()
|
||||
self.emulation_active = (active is None) # True for external windows (e.g., winefile)
|
||||
|
||||
def _navigate_game_cards(self, container, tab_index: int, code: int, value: int) -> None:
|
||||
"""Common navigation logic for game cards in a container."""
|
||||
if container is None:
|
||||
@@ -637,6 +669,116 @@ class InputManager(QObject):
|
||||
except Exception as e:
|
||||
logger.error(f"Error in navigation repeat: {e}")
|
||||
|
||||
def enable_mouse_emulation(self):
|
||||
"""Enable mouse emulation mode (creates virtual mouse device)."""
|
||||
if self.mouse_emulation_enabled and self.ui is not None:
|
||||
logger.debug("EMUL: Mouse emulation already enabled, skipping")
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("EMUL: Attempting to create UInput virtual mouse...")
|
||||
if not os.path.exists('/dev/uinput'):
|
||||
logger.error("EMUL: /dev/uinput does not exist")
|
||||
self.mouse_emulation_enabled = False
|
||||
return
|
||||
|
||||
if not os.access('/dev/uinput', os.W_OK):
|
||||
logger.error("EMUL: No write access to /dev/uinput")
|
||||
self.mouse_emulation_enabled = False
|
||||
return
|
||||
|
||||
self.ui = UInput({
|
||||
ecodes.EV_KEY: [ecodes.BTN_LEFT, ecodes.BTN_RIGHT],
|
||||
ecodes.EV_REL: [ecodes.REL_X, ecodes.REL_Y, ecodes.REL_WHEEL],
|
||||
}, name="Virtual DPad Mouse")
|
||||
|
||||
self.mouse_emulation_enabled = True
|
||||
logger.info("EMUL: Virtual mouse created successfully")
|
||||
|
||||
except PermissionError as e:
|
||||
logger.error("EMUL: Permission denied for /dev/uinput: %s", e)
|
||||
self.mouse_emulation_enabled = False
|
||||
except Exception as ex:
|
||||
logger.error(f"EMUL: Error creating virtual mouse: {ex}", exc_info=True)
|
||||
self.mouse_emulation_enabled = False
|
||||
|
||||
def disable_mouse_emulation(self):
|
||||
"""Disable mouse emulation mode (closes virtual mouse device)."""
|
||||
logger.info("EMUL: Disabling mouse emulation...")
|
||||
if self.ui:
|
||||
try:
|
||||
self.ui.close()
|
||||
logger.info("EMUL: Virtual mouse closed")
|
||||
except Exception as e:
|
||||
logger.error("EMUL: Error closing virtual mouse: %s", e)
|
||||
self.ui = None
|
||||
self.mouse_emulation_enabled = False
|
||||
self.stick_x_raw = 0
|
||||
self.stick_y_raw = 0
|
||||
self.scroll_accumulator = 0.0
|
||||
|
||||
def handle_scroll(self, raw_value):
|
||||
"""Обработка прокрутки с правого стика Y"""
|
||||
if not self.mouse_emulation_enabled or not self.emulation_active or not self.ui:
|
||||
return
|
||||
if abs(raw_value) < self.deadzone:
|
||||
self.scroll_accumulator = 0.0
|
||||
return
|
||||
normalized = raw_value / self.max_value
|
||||
self.scroll_accumulator += normalized * self.scroll_sensitivity
|
||||
while abs(self.scroll_accumulator) >= self.scroll_threshold:
|
||||
scroll_step = 1 if self.scroll_accumulator > 0 else -1
|
||||
self.scroll_wheel(-scroll_step)
|
||||
self.scroll_accumulator -= scroll_step * self.scroll_threshold
|
||||
|
||||
def update_mouse_position(self):
|
||||
"""Постоянное обновление позиции мыши на основе состояния стика"""
|
||||
if not self.ui or not self.emulation_active:
|
||||
return
|
||||
x = self.stick_x_raw
|
||||
y = self.stick_y_raw
|
||||
magnitude = math.sqrt(x * x + y * y)
|
||||
if magnitude < self.deadzone:
|
||||
return
|
||||
norm_x = x / magnitude
|
||||
norm_y = y / magnitude
|
||||
adjusted_magnitude = max(0.0, min(1.0, (magnitude - self.deadzone) / (self.max_value - self.deadzone)))
|
||||
adjusted_magnitude = math.pow(adjusted_magnitude, 1.5)
|
||||
speed = adjusted_magnitude * self.sensitivity
|
||||
dx = int(norm_x * speed)
|
||||
dy = int(norm_y * speed)
|
||||
if dx != 0 or dy != 0:
|
||||
self.move_mouse(dx, dy)
|
||||
|
||||
def move_mouse(self, dx, dy):
|
||||
"""Сдвиг системного курсора"""
|
||||
if self.ui:
|
||||
self.ui.write(ecodes.EV_REL, ecodes.REL_X, dx)
|
||||
self.ui.write(ecodes.EV_REL, ecodes.REL_Y, dy)
|
||||
self.ui.syn()
|
||||
|
||||
def scroll_wheel(self, steps):
|
||||
"""Прокрутка колеса мыши"""
|
||||
if self.ui:
|
||||
self.ui.write(ecodes.EV_REL, ecodes.REL_WHEEL, steps)
|
||||
self.ui.syn()
|
||||
|
||||
def click_left(self):
|
||||
"""Клик левой кнопкой мыши"""
|
||||
if self.ui:
|
||||
self.ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 1)
|
||||
self.ui.syn()
|
||||
self.ui.write(ecodes.EV_KEY, ecodes.BTN_LEFT, 0)
|
||||
self.ui.syn()
|
||||
|
||||
def click_right(self):
|
||||
"""Клик правой кнопкой мыши"""
|
||||
if self.ui:
|
||||
self.ui.write(ecodes.EV_KEY, ecodes.BTN_RIGHT, 1)
|
||||
self.ui.syn()
|
||||
self.ui.write(ecodes.EV_KEY, ecodes.BTN_RIGHT, 0)
|
||||
self.ui.syn()
|
||||
|
||||
@Slot(bool)
|
||||
def handle_fullscreen_slot(self, enable: bool) -> None:
|
||||
try:
|
||||
@@ -1473,7 +1615,6 @@ class InputManager(QObject):
|
||||
logger.error(f"Failed to start udev monitor: {e}")
|
||||
return
|
||||
|
||||
import select
|
||||
fd = monitor.fileno()
|
||||
poller = select.poll()
|
||||
poller.register(fd, select.POLLIN)
|
||||
@@ -1692,60 +1833,103 @@ class InputManager(QObject):
|
||||
logger.error(f"Error finding gamepad: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def monitor_gamepad(self) -> None:
|
||||
try:
|
||||
if not self.gamepad:
|
||||
return
|
||||
for event in self.gamepad.read_loop():
|
||||
if not self.running:
|
||||
break
|
||||
if event.type not in (ecodes.EV_KEY, ecodes.EV_ABS):
|
||||
continue
|
||||
now = time.time()
|
||||
while self.running:
|
||||
current_time = time.time()
|
||||
|
||||
# Проверка фокуса: игнорируем события, если окно не в фокусе
|
||||
app = QApplication.instance()
|
||||
active = QApplication.activeWindow()
|
||||
if not app or not active:
|
||||
continue
|
||||
if self.gamepad:
|
||||
try:
|
||||
# Non-blocking read with short timeout
|
||||
events = []
|
||||
r, w, x = select.select([self.gamepad.fd], [], [], 0.001)
|
||||
if r:
|
||||
events = list(self.gamepad.read())
|
||||
|
||||
if event.type == ecodes.EV_KEY:
|
||||
# Emit on both press (1) and release (0)
|
||||
self.button_event.emit(event.code, event.value)
|
||||
# Special handling for menu on press only
|
||||
if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||
elif event.type == ecodes.EV_ABS:
|
||||
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
|
||||
# Проверяем, достаточно ли времени прошло с последнего срабатывания
|
||||
if now - self.last_trigger_time < self.trigger_cooldown:
|
||||
continue
|
||||
if event.code == ecodes.ABS_Z: # LT/L2
|
||||
if event.value > 128 and not self.lt_pressed:
|
||||
self.lt_pressed = True
|
||||
self.button_event.emit(event.code, 1) # Emit as press
|
||||
self.last_trigger_time = now
|
||||
elif event.value <= 128 and self.lt_pressed:
|
||||
self.lt_pressed = False
|
||||
self.button_event.emit(event.code, 0) # Emit as release
|
||||
elif event.code == ecodes.ABS_RZ: # RT/R2
|
||||
if event.value > 128 and not self.rt_pressed:
|
||||
self.rt_pressed = True
|
||||
self.button_event.emit(event.code, 1) # Emit as press
|
||||
self.last_trigger_time = now
|
||||
elif event.value <= 128 and self.rt_pressed:
|
||||
self.rt_pressed = False
|
||||
self.button_event.emit(event.code, 0) # Emit as release
|
||||
else:
|
||||
self.dpad_moved.emit(event.code, event.value, now)
|
||||
except OSError as e:
|
||||
if e.errno == 19: # ENODEV: No such device
|
||||
logger.info("Gamepad disconnected during event loop")
|
||||
else:
|
||||
logger.error(f"OSError in gamepad monitoring: {e}", exc_info=True)
|
||||
# Process events
|
||||
for event in events:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
# UI signal handling (always, for internal app)
|
||||
if event.type == ecodes.EV_KEY:
|
||||
self.button_event.emit(event.code, event.value)
|
||||
# Special handling for menu on press only
|
||||
if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||
elif event.type == ecodes.EV_ABS:
|
||||
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
|
||||
# Trigger handling for UI
|
||||
if current_time - self.last_trigger_time < self.trigger_cooldown:
|
||||
continue
|
||||
if event.code == ecodes.ABS_Z: # LT/L2
|
||||
if event.value > 128 and not self.lt_pressed:
|
||||
self.lt_pressed = True
|
||||
self.button_event.emit(event.code, 1)
|
||||
self.last_trigger_time = current_time
|
||||
elif event.value <= 128 and self.lt_pressed:
|
||||
self.lt_pressed = False
|
||||
self.button_event.emit(event.code, 0)
|
||||
elif event.code == ecodes.ABS_RZ: # RT/R2
|
||||
if event.value > 128 and not self.rt_pressed:
|
||||
self.rt_pressed = True
|
||||
self.button_event.emit(event.code, 1)
|
||||
self.last_trigger_time = current_time
|
||||
elif event.value <= 128 and self.rt_pressed:
|
||||
self.rt_pressed = False
|
||||
self.button_event.emit(event.code, 0)
|
||||
else:
|
||||
self.dpad_moved.emit(event.code, event.value, current_time)
|
||||
|
||||
# Mouse emulation (only for external windows)
|
||||
if self.mouse_emulation_enabled and self.emulation_active:
|
||||
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
|
||||
break
|
||||
except Exception as ex:
|
||||
logger.error(f"Unexpected error in gamepad monitoring: {ex}")
|
||||
break
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
if not self.running:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in gamepad monitoring: {e}", exc_info=True)
|
||||
logger.error(f"Error in gamepad monitoring thread: {e}", exc_info=True)
|
||||
finally:
|
||||
if self.gamepad:
|
||||
try:
|
||||
@@ -1760,6 +1944,12 @@ class InputManager(QObject):
|
||||
Корректное завершение работы с геймпадом и udev монитором.
|
||||
"""
|
||||
try:
|
||||
# Mouse emulation cleanup
|
||||
self.disable_mouse_emulation()
|
||||
|
||||
# Stop focus check timer
|
||||
self.focus_check_timer.stop()
|
||||
|
||||
# Флаг для остановки udev monitor loop
|
||||
self.running = False
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"pre-commit": {
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user