├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── images │ ├── pupgui2-dark.png │ ├── pupgui2-light.png │ ├── pupgui2-screenshot1.png │ └── pupgui2-screenshot2.png └── workflows │ ├── appimage-ci.yml │ └── testing-ci.yml ├── .gitignore ├── .idea ├── .gitignore ├── ProtonUp-Qt.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── runConfigurations │ └── pupgui2.xml └── vcs.xml ├── AppImageBuilder.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── i18n ├── pupgui2_de.ts ├── pupgui2_el.ts ├── pupgui2_es.ts ├── pupgui2_fi.ts ├── pupgui2_fr.ts ├── pupgui2_hi.ts ├── pupgui2_hu.ts ├── pupgui2_it.ts ├── pupgui2_nb_NO.ts ├── pupgui2_nl.ts ├── pupgui2_pl.ts ├── pupgui2_pt.ts ├── pupgui2_pt_BR.ts ├── pupgui2_ru.ts ├── pupgui2_sv.ts ├── pupgui2_ta.ts ├── pupgui2_tr.ts ├── pupgui2_uk.ts ├── pupgui2_zh_TW.ts ├── release_i18n.sh └── update_i18n.sh ├── pupgui2 ├── __init__.py ├── __main__.py ├── constants.py ├── ctloader.py ├── datastructures.py ├── dbusutil.py ├── gamepadinputworker.py ├── heroicutil.py ├── lutrisutil.py ├── networkutil.py ├── pupgui2.py ├── pupgui2aboutdialog.py ├── pupgui2ctbatchupdatedialog.py ├── pupgui2ctinfodialog.py ├── pupgui2customiddialog.py ├── pupgui2exceptionhandler.py ├── pupgui2gamelistdialog.py ├── pupgui2gitaccesstokendialog.py ├── pupgui2installdialog.py ├── pupgui2shortcutdialog.py ├── resources │ ├── __init__.py │ ├── ctmods │ │ ├── __init__.py │ │ ├── ctmod_00protonge.py │ │ ├── ctmod_boxtron.py │ │ ├── ctmod_kron4ekvanilla.py │ │ ├── ctmod_lutriswine.py │ │ ├── ctmod_luxtorpeda.py │ │ ├── ctmod_northstarproton.py │ │ ├── ctmod_protoncachyos.py │ │ ├── ctmod_protonsarek.py │ │ ├── ctmod_protontkg.py │ │ ├── ctmod_protontkg_ntsync.py │ │ ├── ctmod_protontkg_winemaster.py │ │ ├── ctmod_roberta.py │ │ ├── ctmod_rtspgeproton.py │ │ ├── ctmod_steamplaynone.py │ │ ├── ctmod_steamtinkerlaunch.py │ │ ├── ctmod_steamtinkerlaunch_git.py │ │ ├── ctmod_vkd3dlutris.py │ │ ├── ctmod_vkd3dproton.py │ │ ├── ctmod_winetkg_valve_otherdistro.py │ │ ├── ctmod_winetkg_winemaster.py │ │ ├── ctmod_z0dxvk.py │ │ ├── ctmod_z1dxvkasync.py │ │ └── ctmod_z2dxvknightly.py │ ├── i18n │ │ ├── __init__.py │ │ ├── pupgui2_de.qm │ │ ├── pupgui2_el.qm │ │ ├── pupgui2_es.qm │ │ ├── pupgui2_fi.qm │ │ ├── pupgui2_fr.qm │ │ ├── pupgui2_hi.qm │ │ ├── pupgui2_hu.qm │ │ ├── pupgui2_it.qm │ │ ├── pupgui2_nb_NO.qm │ │ ├── pupgui2_nl.qm │ │ ├── pupgui2_pl.qm │ │ ├── pupgui2_pt.qm │ │ ├── pupgui2_pt_BR.qm │ │ ├── pupgui2_ru.qm │ │ ├── pupgui2_sv.qm │ │ ├── pupgui2_ta.qm │ │ ├── pupgui2_tr.qm │ │ ├── pupgui2_uk.qm │ │ └── pupgui2_zh_TW.qm │ ├── img │ │ ├── __init__.py │ │ ├── appicon256.png │ │ ├── arrow_down.png │ │ ├── awacy_broken.png │ │ ├── awacy_denied.png │ │ ├── awacy_planned.png │ │ ├── awacy_running.png │ │ ├── awacy_supported.png │ │ ├── awacy_unknown.png │ │ └── kofi_button_blue.png │ ├── themes │ │ ├── __init__.py │ │ └── steamdeck.qss │ └── ui │ │ ├── __init__.py │ │ ├── pupgui2_aboutdialog.ui │ │ ├── pupgui2_ctbatchupdatedialog.ui │ │ ├── pupgui2_ctinfodialog.ui │ │ ├── pupgui2_custominstalldirectorydialog.ui │ │ ├── pupgui2_gamelistdialog.ui │ │ ├── pupgui2_gitaccesstokendialog.ui │ │ ├── pupgui2_installdialog.ui │ │ ├── pupgui2_mainwindow.ui │ │ └── pupgui2_shortcutdialog.ui ├── steamutil.py └── util.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── share ├── applications │ └── net.davidotek.pupgui2.desktop ├── icons │ └── hicolor │ │ ├── 128x128 │ │ └── apps │ │ │ └── net.davidotek.pupgui2.png │ │ ├── 256x256 │ │ └── apps │ │ │ └── net.davidotek.pupgui2.png │ │ └── 64x64 │ │ └── apps │ │ └── net.davidotek.pupgui2.png └── metainfo │ └── net.davidotek.pupgui2.appdata.xml └── tests ├── requirements.txt ├── test_ctmods.py ├── test_heroicutil.py ├── test_steamutil.py └── test_util.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: davidotek 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please fill out following when reporting a new bug: 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. For example: 18 | 2. Go to '...' 19 | 3. Click on '....' 20 | 4. Scroll down to '....' 21 | 5. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - **Platform**: Are you using a Steam Deck or a PC/Laptop? 31 | - **System**: For example SteamOS 3.3 or Lubuntu 20.04 32 | - **Version**: For example ProtonUp-Qt 2.5.0 33 | - **How did you install ProtonUp-Qt?**: Discover app store / Flatpak / AppImage 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | 38 | **Terminal output** 39 | ``` 40 | Open the Terminal app and type one of the following commands and press Enter (The app is called "Konsole" on Steam Deck): 41 | 42 | If you installed ProtonUp-Qt via Flatpak (Discover on Steam Deck), type: 43 | flatpak run net.davidotek.pupgui2 44 | 45 | If you have downloaded the AppImage, navigate to the folder containing the AppImage file and type: 46 | ./ProtonUp-Qt*.AppImage 47 | 48 | Try to replicate the bug and paste the text output of the command here afterwards. 49 | ``` 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/images/pupgui2-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/.github/images/pupgui2-dark.png -------------------------------------------------------------------------------- /.github/images/pupgui2-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/.github/images/pupgui2-light.png -------------------------------------------------------------------------------- /.github/images/pupgui2-screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/.github/images/pupgui2-screenshot1.png -------------------------------------------------------------------------------- /.github/images/pupgui2-screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/.github/images/pupgui2-screenshot2.png -------------------------------------------------------------------------------- /.github/workflows/appimage-ci.yml: -------------------------------------------------------------------------------- 1 | name: Build AppImage CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-22.04 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | - name: Install required dependencies 16 | run: sudo apt install -y binutils coreutils desktop-file-utils fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-setuptools squashfs-tools strace util-linux zsync 17 | 18 | - name: Download AppImageBuilder etc. 19 | run: | 20 | pip3 install appimage-builder 21 | 22 | - name: Build AppImage 23 | run: | 24 | sed -i "s/BUILD_INFO = .*/BUILD_INFO = 'Official AppImage by DavidoTek'/" pupgui2/constants.py 25 | appimage-builder 26 | 27 | - name: Upload AppImage 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: ProtonUp-Qt 31 | path: ProtonUp-Qt*.AppImage* 32 | if-no-files-found: ignore 33 | -------------------------------------------------------------------------------- /.github/workflows/testing-ci.yml: -------------------------------------------------------------------------------- 1 | name: Test using Pytest 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | name: "Run PyTest" 12 | runs-on: ubuntu-22.04 13 | strategy: 14 | matrix: 15 | python-version: ["3.10"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "${{ matrix.python-version }}" 24 | cache: "pip" 25 | 26 | - name: Install dependencies 27 | run: | 28 | sudo apt install -y libegl1 libxkbcommon0 29 | pip install -r tests/requirements.txt 30 | 31 | - name: Install ProtonUp-Qt 32 | run: pip install -e . 33 | 34 | - name: Run pytest 35 | uses: pavelzw/pytest-action@v2 36 | env: 37 | QT_QPA_PLATFORM: "offscreen" 38 | with: 39 | verbose: true 40 | emoji: true 41 | job-summary: true 42 | custom-arguments: '-q' 43 | click-to-expand: true 44 | report-title: 'ProtonUp-Qt Test Report' 45 | 46 | validate_metainfo: 47 | name: "Run AppStream Metainfo Validation" 48 | runs-on: ubuntu-24.04 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Install dependencies 53 | run: sudo apt install -y appstream-util 54 | - name: Validate Metainfo file 55 | run: appstream-util validate-relax ./share/metainfo/net.davidotek.pupgui2.appdata.xml 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | .vscode/ 128 | .qt_for_python/ 129 | 130 | *.AppImage 131 | *.AppImage.zsync 132 | AppDir/ 133 | appimage-builder-cache/ 134 | appimage-build/ 135 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/ProtonUp-Qt.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/pupgui2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AppImageBuilder.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | script: 3 | - rm -rf AppDir | true 4 | - mkdir -p AppDir/usr/local/lib/python3.10/dist-packages 5 | - cp pupgui2 AppDir/usr/local/lib/python3.10/dist-packages -r 6 | - cp share AppDir/usr -r 7 | - python3 -m pip install --ignore-installed --prefix=/usr --no-cache-dir --root=AppDir -r ./requirements.txt 8 | - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/ 9 | - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate} 10 | - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3D*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtHelp*,QtMultimedia*,QtNetwork*,QtOpenGL*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialPort*,QtSql*,QtStateMachine*,QtSvg*,QtTest*,QtWeb*,QtXml*} 11 | - shopt -s extglob 12 | - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6OpenGL*|libQt6XcbQpa*|libQt6Wayland*|libQt6Egl*|libicudata*|libicuuc*|libicui18n*|libQt6DBus*|libQt6Network*|libQt6Qml*|libQt6Core*|libQt6Gui*|libQt6Widgets*|libQt6Svg*|libQt6UiTools*) 13 | 14 | 15 | AppDir: 16 | path: ./AppDir 17 | 18 | app_info: 19 | id: net.davidotek.pupgui2 20 | name: ProtonUp-Qt 21 | icon: net.davidotek.pupgui2 22 | version: 2.12.1 23 | exec: usr/bin/python3 24 | exec_args: "-m pupgui2 $@" 25 | 26 | apt: 27 | arch: amd64 28 | sources: 29 | - sourceline: 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse' 30 | key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c' 31 | 32 | include: 33 | - python3 34 | - python3-pkg-resources 35 | - libopengl0 36 | - libk5crypto3 37 | - libkrb5-3 38 | - libgssapi-krb5-2 39 | - libxcb-cursor0 40 | exclude: [] 41 | 42 | runtime: 43 | env: 44 | PYTHONHOME: '${APPDIR}/usr' 45 | PYTHONPATH: '${APPDIR}/usr/local/lib/python3.10/dist-packages' 46 | 47 | 48 | AppImage: 49 | update-information: gh-releases-zsync|DavidoTek|ProtonUp-Qt|latest|ProtonUp-Qt-*x86_64.AppImage.zsync 50 | sign-key: None 51 | arch: x86_64 52 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pupgui2/resources/i18n/*.qm 2 | include pupgui2/resources/img/*.png 3 | include pupgui2/resources/ui/*.ui 4 | include pupgui2/resources/themes/*.qss -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Downloads](https://img.shields.io/github/downloads/DavidoTek/ProtonUp-Qt/total.svg)](https://github.com/DavidoTek/ProtonUp-Qt/releases) 2 | [![Flathub Downloads](https://img.shields.io/flathub/downloads/net.davidotek.pupgui2?label=Flathub%20installs)](https://flathub.org/apps/details/net.davidotek.pupgui2) 3 | [![License](https://img.shields.io/github/license/DavidoTek/ProtonUp-Qt)](https://github.com/DavidoTek/ProtonUp-Qt/blob/main/LICENSE) 4 | [![Build AppImage CI](https://github.com/DavidoTek/ProtonUp-Qt/actions/workflows/appimage-ci.yml/badge.svg)](https://github.com/DavidoTek/ProtonUp-Qt/actions/workflows/appimage-ci.yml) 5 | 6 | # ProtonUp-Qt 7 | Install and manage [GE-Proton](https://github.com/GloriousEggroll/proton-ge-custom) and [Luxtorpeda](https://github.com/luxtorpeda-dev/luxtorpeda) for Steam and [Wine-GE](https://github.com/GloriousEggroll/wine-ge-custom) for Lutris with this graphical user interface. Based on AUNaseef's [ProtonUp](https://github.com/AUNaseef/protonup), made with Python 3 and Qt 6. 8 | 9 | ![ProtonUp-Qt Screenshot](.github/images/pupgui2-screenshot2.png) 10 | 11 | ## Disclaimer 12 | [**Affiliation Note**](https://github.com/DavidoTek/ProtonUp-Qt/pull/413): ProtonUp-Qt is an independent tool for managing gaming compatibility tools. It is neither directly affiliated with the compatibility tool creators nor with the providers of the individual game launchers. However, we try to work with them where possible. 13 | 14 | The official development takes place on GitHub at [DavidoTek/ProtonUp-Qt](https://github.com/DavidoTek/ProtonUp-Qt), and the official website is https://davidotek.github.io/protonup-qt. We distribute ProtonUp-Qt as a Flatpak on [Flathub](https://flathub.org/apps/net.davidotek.pupgui2) and as an AppImage in the [releases section](https://github.com/DavidoTek/ProtonUp-Qt/releases) of the GitHub repository. Additionally, we check the integrity of the AUR ([`protonup-qt`](https://aur.archlinux.org/packages/protonup-qt) and [`protonup-qt-bin`](https://aur.archlinux.org/packages/protonup-qt-bin)) and [Pacstall](https://pacstall.dev/packages/protonup-qt-app) distribution on an irregular basis. 15 | 16 | ## Download from Flathub or as AppImage (portable): 17 | [Download from Flathub](https://flathub.org/apps/details/net.davidotek.pupgui2) [Download AppImage](https://github.com/DavidoTek/ProtonUp-Qt/releases) 18 | 19 | Website: [https://davidotek.github.io/protonup-qt](https://davidotek.github.io/protonup-qt) 20 | 21 | Wiki/Support: https://github.com/DavidoTek/ProtonUp-Qt/wiki 22 | 23 | ## Install from AUR: (Arch, Manjaro, EndeavourOS, etc.) 24 | 25 | ### Source 26 | https://aur.archlinux.org/packages/protonup-qt (Maintained by yochananmarqos) 27 | 28 | ### Binary (Appimage) 29 | https://aur.archlinux.org/packages/protonup-qt-bin (Maintained by R1yuu) 30 | 31 | ## Run from source 32 | ### Install dependencies 33 | `pip3 install -r ./requirements.txt` 34 | ### Run ProtonUp-Qt 35 | `python3 -m pupgui2` 36 | 37 | ## Build AppImage 38 | ### Install dependencies 39 | 1. Install appimage-builder: https://appimage-builder.readthedocs.io/en/latest/intro/install.html 40 | ### Build AppImage 41 | `appimage-builder` 42 | 43 | ## Translate ProtonUp-Qt 44 | **Recommended: You can translate ProtonUp-Qt on Weblate: https://hosted.weblate.org/projects/protonup-qt/** 45 | 46 | 1. Generate an empty translation file *or* copy a template from [here](https://github.com/DavidoTek/ProtonUp-Qt/blob/main/i18n/pupgui2_de.ts). 47 | 2. Install [Qt Linguist](https://flathub.org/apps/details/io.qt.Linguist) (alternatively: edit the **.ts** file using a text editor). 48 | 3. Open the translation file (.ts) with Qt Linguist and translate the app. 49 | 4. The app summary can be found [here](https://github.com/DavidoTek/ProtonUp-Qt/blob/main/share/metainfo/net.davidotek.pupgui2.appdata.xml#L7). 50 | 5. The comment inside the .desktop file can be found [here](https://github.com/DavidoTek/ProtonUp-Qt/blob/main/share/applications/net.davidotek.pupgui2.desktop#L6). 51 | 6. Submit the translation: 52 | a) Create a Pull Request with the translation 53 | b) Simple method: Alternatively, upload the **.ts** file/texts [here](https://gist.github.com/) and [create a new issue](https://github.com/DavidoTek/ProtonUp-Qt/issues/new?labels=translation&title=Translation:%20language) with a link to your translation. 54 | 55 | ## Credits 56 | Special thanks to the authors of all services that ProtonUp-Qt uses, including **[AreWeAntiCheatYet](https://areweanticheatyet.com/)** and **[ProtonDB](https://www.protondb.com/)**. 57 | 58 | ## Licensing 59 | Project|License 60 | -------|-------- 61 | ProtonUp-Qt|GPL-3.0 62 | [ProtonUp](https://pypi.org/project/protonup/)|GPL-3.0 63 | [PySide6](https://pypi.org/project/PySide6/)|LGPL-3.0/GPL-2.0 64 | [inputs](https://pypi.org/project/inputs/)|BSD 65 | [pyxdg](https://pypi.org/project/pyxdg/)|LGPLv2 66 | [vdf@solstice](https://github.com/solsticegamestudios/vdf/)|MIT 67 | [steam@solstice](https://github.com/solsticegamestudios/steam/)|MIT 68 | [requests](https://pypi.org/project/requests/)|Apache 2.0 69 | [PyYAML](https://pypi.org/project/PyYAML/)|MIT 70 | -------------------------------------------------------------------------------- /i18n/release_i18n.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ProtonUp-Qt - Script for compiling all .ts translation files to .qm files 3 | BASE_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && cd .. && pwd ) 4 | TS_FILES="$BASE_DIR/i18n/*.ts" 5 | 6 | for ts in $TS_FILES 7 | do 8 | lang=$(sed 's/.ts//g' <<< "$(basename $ts)") 9 | qm="$BASE_DIR/pupgui2/resources/i18n/$lang.qm" 10 | pyside6-lrelease $ts -qm $qm 11 | done 12 | -------------------------------------------------------------------------------- /i18n/update_i18n.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ProtonUp-Qt - Script for updating the .ts translation files from the sources 3 | BASE_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && cd .. && pwd ) 4 | TS_FILES="$BASE_DIR/i18n/*.ts" 5 | 6 | SOURCES="$BASE_DIR/pupgui2/*.py $BASE_DIR/pupgui2/resources/ctmods/*.py $BASE_DIR/pupgui2/resources/ui/*.ui" 7 | 8 | for ts in $TS_FILES 9 | do 10 | pyside6-lupdate $SOURCES -ts $ts 11 | done 12 | -------------------------------------------------------------------------------- /pupgui2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/__init__.py -------------------------------------------------------------------------------- /pupgui2/__main__.py: -------------------------------------------------------------------------------- 1 | from pupgui2.pupgui2 import main 2 | main() 3 | -------------------------------------------------------------------------------- /pupgui2/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from xdg.BaseDirectory import xdg_config_home 3 | 4 | from PySide6.QtCore import Qt 5 | from PySide6.QtGui import QColor, QPalette 6 | 7 | 8 | APP_NAME = 'ProtonUp-Qt' 9 | APP_VERSION = '2.12.1' 10 | APP_ID = 'net.davidotek.pupgui2' 11 | APP_THEMES = ( 'light', 'dark', 'system', 'steam', None ) 12 | APP_ICON_FILE = os.path.join(xdg_config_home, 'pupgui/appicon256.png') 13 | APP_GHAPI_URL = 'https://api.github.com/repos/Davidotek/ProtonUp-qt/releases' 14 | DAVIDOTEK_KOFI_URL = 'https://ko-fi.com/davidotek' 15 | PROTONUPQT_GITHUB_URL = 'https://github.com/DavidoTek/ProtonUp-Qt' 16 | ABOUT_TEXT = '''\ 17 | {APP_NAME} v{APP_VERSION} by DavidoTek: https://github.com/DavidoTek/ProtonUp-Qt
18 | Copyright (C) 2021-2024 DavidoTek, licensed under GPLv3 19 | '''.format(APP_NAME=APP_NAME, APP_VERSION=APP_VERSION, PROTONUPQT_GITHUB_URL=PROTONUPQT_GITHUB_URL) 20 | BUILD_INFO = 'built from source' 21 | 22 | CONFIG_FILE = os.path.join(xdg_config_home, 'pupgui/config.ini') 23 | CACHE_DIR = os.path.join(xdg_cache, 'tmp') if (xdg_cache := os.getenv('XDG_CACHE_HOME', '')) else '' 24 | TEMP_DIR = os.path.join(CACHE_DIR, 'pupgui2.a70200/') if CACHE_DIR and os.path.exists(CACHE_DIR) else '/tmp/pupgui2.a70200/' 25 | HOME_DIR = os.path.expanduser('~') 26 | 27 | IS_FLATPAK: bool = os.path.exists('/.flatpak-info') 28 | 29 | # DBus constants 30 | DBUS_APPLICATION_URI = f'application://{APP_ID}.desktop' 31 | DBUS_DOWNLOAD_OBJECT_BASEPATH = '/net/davidotek/pupgui2' 32 | 33 | DBUS_INTERFACES_AND_SIGNALS = { 34 | 'LauncherEntryUpdate': { 35 | 'interface': 'com.canonical.Unity.LauncherEntry', 36 | 'signal': 'Update', 37 | } 38 | } 39 | 40 | # support different Steam root directories, building paths relative to HOME_DIR (i.e. /home/gaben/.local/share/Steam) 41 | # Use os.path.realpath to expand all _STEAM_ROOT paths 42 | _POSSIBLE_STEAM_ROOTS = [ 43 | os.path.realpath(os.path.join(HOME_DIR, _STEAM_ROOT)) for _STEAM_ROOT in ['.local/share/Steam', '.steam/root', '.steam/steam', '.steam/debian-installation'] 44 | ] 45 | 46 | # Remove duplicate paths while preserving order, as os.path.realpath may expand some symlinks to the real Steam root 47 | _POSSIBLE_STEAM_ROOTS = list(dict.fromkeys(_POSSIBLE_STEAM_ROOTS)) 48 | 49 | # Steam can be installled in any of the locations at '_POSSIBLE_STEAM_ROOTS' - usually only one, and the others (if they exist) are typically symlinks, 50 | # i.e. '~/.steam/root' is usually a symlink to '~/.local/share/Steam' 51 | # These paths may still not be valid installations however, as they could be leftother paths from an old Steam installation without the data files we need ('config.vdf' and 'libraryfolders.vdf') 52 | # We catch this later on in util#is_valid_launcher_installation though 53 | POSSIBLE_INSTALL_LOCATIONS = [ 54 | { 55 | 'install_dir': f'{_STEAM_ROOT}/compatibilitytools.d/', 56 | 'display_name': 'Steam', 57 | 'launcher': 'steam', 58 | 'type': 'native', 59 | 'icon': 'steam', 60 | 'vdf_dir': f'{_STEAM_ROOT}/config' 61 | } for _STEAM_ROOT in _POSSIBLE_STEAM_ROOTS if os.path.exists(_STEAM_ROOT) 62 | ] 63 | 64 | # Possible install locations for all other launchers, ensuring Steam paths are at the top of the list 65 | POSSIBLE_INSTALL_LOCATIONS += [ 66 | {'install_dir': '~/.var/app/com.valvesoftware.Steam/data/Steam/compatibilitytools.d/', 'display_name': 'Steam Flatpak', 'launcher': 'steam', 'type': 'flatpak', 'icon': 'steam', 'vdf_dir': '~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config'}, 67 | {'install_dir': '~/snap/steam/common/.steam/root/compatibilitytools.d/', 'display_name': 'Steam Snap', 'launcher': 'steam', 'type': 'snap', 'icon': 'steam', 'vdf_dir': '~/snap/steam/common/.steam/root/config'}, 68 | {'install_dir': '~/.local/share/lutris/runners/wine/', 'display_name': 'Lutris', 'launcher': 'lutris', 'type': 'native', 'icon': 'lutris', 'config_dir': '~/.config/lutris'}, 69 | {'install_dir': '~/.var/app/net.lutris.Lutris/data/lutris/runners/wine/', 'display_name': 'Lutris Flatpak', 'launcher': 'lutris', 'type': 'flatpak', 'icon': 'lutris', 'config_dir': '~/.var/app/net.lutris.Lutris/config/lutris'}, 70 | {'install_dir': '~/.config/heroic/tools/wine/', 'display_name': 'Heroic Wine', 'launcher': 'heroicwine', 'type': 'native', 'icon': 'heroic'}, 71 | {'install_dir': '~/.config/heroic/tools/proton/', 'display_name': 'Heroic Proton', 'launcher': 'heroicproton', 'type': 'native', 'icon': 'heroic'}, 72 | {'install_dir': '~/.var/app/com.heroicgameslauncher.hgl/config/heroic/tools/wine/', 'display_name': 'Heroic Wine Flatpak', 'launcher': 'heroicwine', 'type': 'flatpak', 'icon': 'heroic'}, 73 | {'install_dir': '~/.var/app/com.heroicgameslauncher.hgl/config/heroic/tools/proton/', 'display_name': 'Heroic Proton Flatpak', 'launcher': 'heroicproton', 'type': 'flatpak', 'icon': 'heroic'}, 74 | {'install_dir': '~/.local/share/bottles/runners/', 'display_name': 'Bottles', 'launcher': 'bottles', 'type': 'native', 'icon': 'com.usebottles.bottles'}, 75 | {'install_dir': '~/.var/app/com.usebottles.bottles/data/bottles/runners/', 'display_name': 'Bottles Flatpak', 'launcher': 'bottles', 'type': 'flatpak', 'icon': 'com.usebottles.bottles'}, 76 | {'install_dir': '~/.local/share/winezgui/Runners/', 'display_name': 'WineZGUI', 'launcher': 'winezgui', 'type': 'native', 'icon': 'io.github.fastrizwaan.WineZGUI'}, 77 | {'install_dir': '~/.var/app/io.github.fastrizwaan.WineZGUI/data/winezgui/Runners/', 'display_name': 'WineZGUI Flatpak', 'launcher': 'winezgui', 'type': 'flatpak', 'icon': 'io.github.fastrizwaan.WineZGUI'} 78 | ] 79 | 80 | def PALETTE_DARK(): 81 | """ returns dark color palette """ 82 | palette_base = QColor(12, 12, 12) 83 | 84 | palette_dark = QPalette() 85 | palette_dark.setColor(QPalette.Window, QColor(30, 30, 30)) 86 | palette_dark.setColor(QPalette.WindowText, Qt.white) 87 | palette_dark.setColor(QPalette.Base, palette_base) 88 | palette_dark.setColor(QPalette.AlternateBase, QColor(30, 30, 30)) 89 | palette_dark.setColor(QPalette.ToolTipBase, palette_base) 90 | palette_dark.setColor(QPalette.ToolTipText, Qt.white) 91 | palette_dark.setColor(QPalette.Text, Qt.white) 92 | palette_dark.setColor(QPalette.Button, QColor(30, 30, 30)) 93 | palette_dark.setColor(QPalette.ButtonText, Qt.white) 94 | palette_dark.setColor(QPalette.BrightText, Qt.red) 95 | palette_dark.setColor(QPalette.Link, QColor(40, 120, 200)) 96 | palette_dark.setColor(QPalette.Highlight, QColor(40, 120, 200)) 97 | palette_dark.setColor(QPalette.HighlightedText, Qt.black) 98 | return palette_dark 99 | 100 | def PALETTE_STEAMUI(): 101 | """ returns a Steam-ui like color palette """ 102 | palette_steam = QPalette() 103 | # Needed for theming Qt's Wayland Client Side Decoration 104 | palette_steam.setColor(QPalette.Window, QColor(23, 29, 37)) #171D25 105 | palette_steam.setColor(QPalette.WindowText, Qt.white) 106 | return palette_steam 107 | 108 | PROTONDB_COLORS = {'platinum': '#b4c7dc', 'gold': '#cfb53b', 'silver': '#a6a6a6', 'bronze': '#cd7f32', 'borked': '#ff0000', 'pending': '#748472' } 109 | 110 | STEAM_API_GETAPPLIST_URL = 'https://api.steampowered.com/ISteamApps/GetAppList/v2/' 111 | STEAM_APP_PAGE_URL = 'https://store.steampowered.com/app/' 112 | AWACY_GAME_LIST_URL = 'https://raw.githubusercontent.com/Starz0r/AreWeAntiCheatYet/master/games.json' 113 | AWACY_WEB_URL = 'https://areweanticheatyet.com/?search={GAMENAME}&sortOrder=&sortBy=' 114 | LOCAL_AWACY_GAME_LIST = os.path.join(TEMP_DIR, 'awacy_games.json') 115 | PROTONDB_API_URL = 'https://www.protondb.com/api/v1/reports/summaries/{game_id}.json' 116 | PROTONDB_APP_PAGE_URL = 'https://protondb.com/app/' 117 | 118 | STEAM_BOXTRON_FLATPAK_APPSTREAM = 'appstream://com.valvesoftware.Steam.CompatibilityTool.Boxtron' 119 | STEAM_STL_FLATPAK_APPSTREAM = 'appstream://com.valvesoftware.Steam.Utility.steamtinkerlaunch' 120 | 121 | STEAM_STL_INSTALL_PATH = os.path.join(HOME_DIR, 'stl') 122 | STEAM_STL_CONFIG_PATH = os.path.join(HOME_DIR, '.config', 'steamtinkerlaunch') 123 | STEAM_STL_CACHE_PATH = os.path.join(HOME_DIR, '.cache', 'steamtinkerlaunch') 124 | STEAM_STL_DATA_PATH = os.path.join(HOME_DIR, '.local', 'share', 'steamtinkerlaunch') 125 | STEAM_STL_SHELL_FILES = [ '.bashrc', '.zshrc', '.kshrc' ] 126 | STEAM_STL_FISH_VARIABLES = os.path.join(HOME_DIR, '.config/fish/fish_variables') 127 | 128 | LUTRIS_WEB_URL = 'https://lutris.net/games/' 129 | EPIC_STORE_URL = 'https://store.epicgames.com/p/' 130 | 131 | GITHUB_API = 'https://api.github.com/' 132 | # GitLab can have any self-hosted instance, so we store a list of known GitLab instances 133 | GITLAB_API = [ 134 | 'https://gitlab.com/api/' 135 | ] 136 | 137 | GITLAB_API_RATELIMIT_TEXT = [ 138 | 'Rate limit exceeded; see https://docs.gitlab.com/ee/user/gitlab_com/#gitlabcom-specific-rate-limits for more details', 139 | 'Rate limit exceeded', 140 | 'Retry later' 141 | ] 142 | 143 | PROTON_NEXT_APPID = 2230260 144 | PROTON_EAC_RUNTIME_APPID = 1826330 145 | PROTON_BATTLEYE_RUNTIME_APPID = 1161040 146 | STEAMLINUXRUNTIME_APPID = 1070560 147 | STEAMLINUXRUNTIME_SOLDIER_APPID = 1391110 148 | STEAMLINUXRUNTIME_SNIPER_APPID = 1628350 149 | -------------------------------------------------------------------------------- /pupgui2/ctloader.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | import importlib 3 | 4 | from PySide6.QtCore import QObject 5 | from PySide6.QtWidgets import QMessageBox 6 | 7 | from pupgui2.util import create_msgbox 8 | from pupgui2.resources import ctmods 9 | 10 | 11 | class CtLoader(QObject): 12 | 13 | ctmods = [] 14 | ctobjs = [] 15 | 16 | def __init__(self, main_window = None): 17 | self.main_window = main_window 18 | 19 | def load_ctmods(self) -> bool: 20 | """ 21 | Load ctmods 22 | Return Type: bool 23 | """ 24 | failed_ctmods: list[tuple[str, Exception]] = [] 25 | for _, mod, _ in pkgutil.iter_modules(ctmods.__path__): 26 | if mod.startswith('ctmod_'): 27 | try: 28 | ctmod = importlib.import_module(f'pupgui2.resources.ctmods.{mod}') 29 | if ctmod is None: 30 | failed_ctmods.append((mod.replace('ctmod_', ''), 'ctmod is None')) 31 | print('Could not load ctmod', mod) 32 | continue 33 | self.ctmods.append(ctmod) 34 | self.ctobjs.append({ 35 | 'name': ctmod.CT_NAME, 36 | 'launchers': ctmod.CT_LAUNCHERS, 37 | 'description': ctmod.CT_DESCRIPTION, 38 | 'installer': ctmod.CtInstaller(main_window=self.main_window) 39 | }) 40 | print('Loaded ctmod', ctmod.CT_NAME) 41 | except Exception as e: 42 | failed_ctmods.append((mod.replace('ctmod_', ''), e)) 43 | print('Could not load ctmod', mod, ':', e) 44 | if len(failed_ctmods) > 0: 45 | detailed_text = '' 46 | ctmods_name = [] 47 | for ctmod, e in failed_ctmods: 48 | ctmods_name.append(ctmod) 49 | detailed_text += f'{ctmod}: {e}\n' 50 | detailed_text = detailed_text.strip() 51 | create_msgbox( 52 | title=self.tr('Error!'), 53 | text=self.tr('Couldn\'t load the following compatibility tool(s):\n{TOOL_LIST}\n\nIf you believe this is an error, please report a bug on GitHub!') 54 | .format(TOOL_LIST=', '.join(ctmods_name)), 55 | icon=QMessageBox.Warning, 56 | detailed_text=detailed_text 57 | ) 58 | return len(failed_ctmods) == 0 59 | 60 | def get_ctmods(self, launcher=None, advanced_mode=True): 61 | """ 62 | Get loaded ctmods, optionally sort by launcher 63 | Return Type: [] 64 | """ 65 | if launcher is None: 66 | return [ctmod for ctmod in self.ctmods if ('advmode' not in ctmod.CT_LAUNCHERS or advanced_mode)] 67 | 68 | ctmods = [ctmod for ctmod in self.ctmods if launcher in ctmod.CT_LAUNCHERS and ('advmode' not in ctmod.CT_LAUNCHERS or advanced_mode)] 69 | 70 | return ctmods 71 | 72 | def get_ctobjs(self, launcher=None, advanced_mode=True): 73 | """ 74 | Get loaded compatibility tools, optionally sort by launcher 75 | Return Type: list[dict] 76 | Content(s): 77 | 'name', 'launchers', 'installer' 78 | """ 79 | if launcher is None: 80 | return self.ctobjs 81 | 82 | ctobjs = [] 83 | for ctobj in self.ctobjs: 84 | if launcher.get('launcher') in ctobj['launchers']: 85 | if 'advmode' in ctobj['launchers'] and not advanced_mode: 86 | continue 87 | if 'native-only' in ctobj['launchers'] and launcher.get('type') != 'native': 88 | continue 89 | ctobjs.append(ctobj) 90 | return ctobjs 91 | -------------------------------------------------------------------------------- /pupgui2/dbusutil.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any 3 | 4 | from PySide6.QtDBus import QDBusConnection, QDBusMessage 5 | 6 | from pupgui2.constants import DBUS_APPLICATION_URI, DBUS_DOWNLOAD_OBJECT_BASEPATH, DBUS_INTERFACES_AND_SIGNALS 7 | 8 | 9 | def create_and_send_dbus_message(object: str, interface: str, signal_name: str, arguments: list[Any], bus: QDBusConnection | None = None) -> bool: 10 | 11 | """ 12 | Create and send a QDBusMessage over a given bus. 13 | If no bus is given, will default to sessionBus 14 | 15 | Returns `True` if the message was sent to DBus successfully, `False` otherwise. 16 | 17 | Return Type: bool 18 | """ 19 | 20 | if bus is None: 21 | bus = QDBusConnection.sessionBus() 22 | 23 | # i.e. /net/davidotek/pupgui2/Update 24 | object_path: str = os.path.join(DBUS_DOWNLOAD_OBJECT_BASEPATH, object) 25 | 26 | message: QDBusMessage = QDBusMessage.createSignal(object_path, interface, signal_name) 27 | message.setArguments(arguments) 28 | 29 | # Don't send the message if bus is not valid (i.e. DBus is not running) 30 | if bus.isConnected(): 31 | return bus.send(message) 32 | 33 | return False 34 | 35 | 36 | def dbus_progress_message(progress: float, count: int = 0, bus: QDBusConnection | None = None) -> bool: 37 | 38 | """ 39 | Create and send download progress (between 0 and 1) information with optional count parameter on a given bus. 40 | If no bus is given, will default to sessionBus. 41 | 42 | Returns `True` if the message was sent to DBus successfully, `False` otherwise. 43 | 44 | Return Type: bool 45 | """ 46 | 47 | if bus is None: 48 | bus = QDBusConnection.sessionBus() 49 | 50 | arguments: dict[str, int | float | bool] = { 51 | 'progress': progress, 52 | 'progress-visible': progress >= 0 and progress < 1, 53 | 'count': count, 54 | 'count-visible': count > 0 55 | } 56 | 57 | # We need to tell the 'Update' signal to update on DBUS_APPLICATION_URI, 58 | # plus an 'arguments' dict with some extra information 59 | # 60 | # i.e. { 'progress': 0.7, 'progress-visible': True } 61 | message_arguments: list[str | dict[str, int | float | bool]] = [ 62 | DBUS_APPLICATION_URI, 63 | arguments 64 | ] 65 | 66 | launcher_entry_update: dict[str, str] = DBUS_INTERFACES_AND_SIGNALS['LauncherEntryUpdate'] 67 | 68 | interface: str = launcher_entry_update['interface'] 69 | signal: str = launcher_entry_update['signal'] 70 | object = 'Update' 71 | 72 | return create_and_send_dbus_message(object, interface, signal, message_arguments, bus=bus) 73 | 74 | 75 | -------------------------------------------------------------------------------- /pupgui2/gamepadinputworker.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Qt, QThread, Signal 2 | 3 | 4 | class GamepadInputWorker(QThread): 5 | 6 | press_virtual_key = Signal(int, Qt.KeyboardModifiers) 7 | 8 | def __init__(self): 9 | super().__init__() 10 | self.reset_pos = 0 11 | 12 | def run(self): 13 | try: 14 | import inputs 15 | 16 | while not self.isInterruptionRequested(): 17 | events = inputs.get_gamepad() 18 | for event in events: 19 | if event.code in ['ABS_HAT0Y', 'ABS_HAT0X']: 20 | if event.state == -1: 21 | self.press_virtual_key.emit(Qt.Key_Tab, Qt.ShiftModifier) 22 | elif event.state == 1: 23 | self.press_virtual_key.emit(Qt.Key_Tab, Qt.NoModifier) 24 | 25 | elif event.code == 'BTN_SOUTH' and event.state == 1: 26 | self.press_virtual_key.emit(Qt.Key_Space, Qt.NoModifier) 27 | elif event.code == 'BTN_EAST' and event.state == 1: 28 | self.press_virtual_key.emit(Qt.Key_Enter, Qt.NoModifier) 29 | 30 | elif event.code in ['ABS_Y', 'ABS_RY']: 31 | if event.state > -1800 and event.state < 1800: 32 | self.reset_pos = True 33 | elif event.state < 0: 34 | if self.reset_pos: 35 | self.press_virtual_key.emit(Qt.Key_Up, Qt.NoModifier) 36 | self.reset_pos = False 37 | elif event.state > 0: 38 | if self.reset_pos: 39 | self.press_virtual_key.emit(Qt.Key_Down, Qt.NoModifier) 40 | self.reset_pos = False 41 | elif event.code in ['ABS_X', 'ABS_RX']: 42 | if event.state > -1800 and event.state < 1800: 43 | self.reset_pos = True 44 | elif event.state < 0: 45 | if self.reset_pos: 46 | self.press_virtual_key.emit(Qt.Key_Left, Qt.NoModifier) 47 | self.reset_pos = False 48 | elif event.state > 0: 49 | if self.reset_pos: 50 | self.press_virtual_key.emit(Qt.Key_Right, Qt.NoModifier) 51 | self.reset_pos = False 52 | except Exception as e: 53 | print('Gamepad error:', e) 54 | 55 | def stop(self): 56 | self.requestInterruption() 57 | self.setTerminationEnabled(True) 58 | self.terminate() 59 | self.wait() 60 | -------------------------------------------------------------------------------- /pupgui2/heroicutil.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import re 4 | 5 | from typing import Any 6 | 7 | from pupgui2.datastructures import HeroicGame 8 | from pupgui2.constants import EPIC_STORE_URL 9 | 10 | 11 | def get_heroic_game_list(heroic_path: str) -> list[HeroicGame]: 12 | """ 13 | Returns a list of installed games for Heroic Games at 'heroic_path' (e.g., '~/.config/heroic', '~/.var/app/com.heroicgameslauncher.hgl/config/heroic') 14 | Return Type: list[HeroicGame] 15 | """ 16 | 17 | if not os.path.isdir(heroic_path): 18 | return [] 19 | 20 | # "Nile" refers to Amazon Games 21 | store_paths: list[str] = [ os.path.join(heroic_path, 'sideload_apps', 'library.json'), os.path.join(heroic_path, 'gog_store', 'library.json'), os.path.join(heroic_path, 'store_cache', 'nile_library.json') ] 22 | legendary_path: str = os.path.abspath(os.path.join(heroic_path, '..', 'legendary', 'installed.json')) 23 | 24 | games_json: list[dict[str, Any]] = [] 25 | for sp in store_paths: 26 | if os.path.isfile(sp): 27 | games_json_file: dict[str, Any] = json.load(open(sp)) 28 | 29 | # 'games' and 'library' is a JSON array containing objects representing each game 30 | games_json += games_json_file.get('games', []) # GOG + sideload use 'games' as top-level object 31 | games_json += games_json_file.get('library', []) # Nile uses 'library' as top-level object 32 | 33 | hgs: list[HeroicGame] = [] 34 | for game in games_json: 35 | game: dict[str, Any] 36 | hg = HeroicGame() 37 | 38 | hg.runner = game.get('runner', '') 39 | hg.app_name = game.get('app_name', '') 40 | hg.title = game.get('title', '') 41 | hg.developer = game.get('developer', '') 42 | hg.heroic_path = heroic_path 43 | # Sideloaded games uses folder_name as their full install path, GOG games store a folder_name but this is *just* their install folder name, Nile uses `"install": [ "install_path": "/foo/bar/foobar" ]` 44 | # Prioritise getting install_path for GOG games as this is the GOG game equivalent to 'folder_name' 45 | hg.install_path = get_gog_installed_game_entry(hg).get('install_path', '') or game.get('install', {}).get('install_path', '') or game.get('browserUrl', '') or game.get('folder_name', '') 46 | hg.store_url = game.get('store_url', '') 47 | hg.art_cover = game.get('art_cover', '') # May need to replace path if it has 'file:///app/blah in name - See example in #168 48 | hg.art_square = game.get('art_square', '') 49 | hg.is_installed = game.get('is_installed', False) or is_gog_game_installed(hg) # Some installed gog games may not be marked properly in library.json, so cross-reference with installed.json 50 | hg.wine_info = hg.get_game_config().get('wineVersion', {}) 51 | # Sideloaded games store platform in its library.json (it has no installed.json) under the 'install' object 52 | # GOG games store the platform for the version of the installed game in `installed.json` (as GOG games can target multiple platforms, installed will show if the user has the Windows or Linux version) 53 | hg.platform = get_gog_installed_game_entry(hg).get('platform', '').capitalize() if hg.runner.lower() == 'gog' else game.get('install', {}).get('platform', '').capitalize() # Capitalize ensures consistency 54 | # GOG and Epic store the exe name on its own, but sideloaded stores the full path, so for consistency get the basename for sideloaded apps 55 | # Native GOG games seem to just store the 'executable' as 'start.sh' script 56 | hg.executable = get_gog_game_executable(hg) if hg.runner.lower() == 'gog' else os.path.basename(game.get('install', {}).get('executable', '')) 57 | hg.is_dlc = game.get('install', {}).get('is_dlc', False) 58 | 59 | hgs.append(hg) 60 | 61 | # Legendary Games uses a separate structure, so build separately 62 | if os.path.isfile(legendary_path): 63 | legendary_json: dict[str, Any] = json.load(open(legendary_path)) 64 | for app_name, game_data in legendary_json.items(): 65 | lg = HeroicGame() 66 | 67 | lg.runner = 'legendary' # Hardcoded 68 | lg.app_name = app_name # installed.json key is always the app_name 69 | lg.title = game_data.get('title', '') 70 | lg.developer = '' # Not stored or stored elsewhere? 71 | lg.heroic_path = heroic_path 72 | lg.install_path = game_data.get('install_path', '') 73 | lg.store_url = f'{EPIC_STORE_URL}{re.sub("[^a-zA-Z0-9]", "-", lg.title.lower())}' 74 | lg.art_cover = '' # Not stored or stored elsewhere? 75 | lg.art_square = '' # Not stored or stored elsewhere? 76 | lg.is_installed = True # Games in Legendary `installed.json` should always be installed 77 | lg.wine_info = lg.get_game_config().get('wineVersion', {}) # Mirrors above, Legendary games should use the same GameConfig json structure 78 | lg.platform = game_data.get('platform', '').capitalize() # Legendary stores this in `installed.json` and like GOG this stores the platform for the version the user downloaded 79 | lg.executable = game_data.get('executable', '') 80 | lg.is_dlc = game_data.get('is_dlc', False) # If not set for some reason, assume its not DLC 81 | 82 | hgs.append(lg) 83 | 84 | return hgs 85 | 86 | 87 | def is_heroic_launcher(launcher: str) -> bool: 88 | """ Returns True if the supplied launcher name is a valid name for Heroic Games Launcher, e.g. "heroicwine" """ 89 | 90 | return any(hero in launcher for hero in ['heroicwine', 'heroicproton']) 91 | 92 | 93 | # `is_installed` for GOG games is not always set properly 94 | def is_gog_game_installed(game: HeroicGame) -> bool: 95 | """ Return True if a GOG game has an entry in heroic/gog_store/installed.json """ 96 | 97 | return bool(get_gog_installed_game_entry(game)) 98 | 99 | 100 | def get_gog_installed_game_entry(game: HeroicGame) -> dict[str, Any]: 101 | """ Return JSON entry as dict for an installed GOG game from heroic/gog_store/installed.json """ 102 | 103 | gog_installed_json_path = os.path.join(game.heroic_path, 'gog_store', 'installed.json') 104 | if not os.path.isfile(gog_installed_json_path) or not game.runner.lower() == 'gog': 105 | return {} 106 | 107 | gog_installed_json = json.load(open(gog_installed_json_path)).get('installed', []) 108 | for gog_game in gog_installed_json: 109 | if gog_game.get('appName', '') == game.app_name: 110 | return gog_game 111 | else: 112 | return {} 113 | 114 | 115 | def get_gog_game_executable(game: HeroicGame) -> str: 116 | """ Return the executable for a GOG game from its gameinfo file, or 'start.sh' for native Linux games. Will return empty string if no executable found. """ 117 | 118 | # Proton games store it in `/path/to/install/goggame-.info`, which is a JSON formatted file 119 | gog_gameinfo_filename = f'goggame-{game.app_name}.info' 120 | gog_gameinfo_json_path = os.path.join(game.install_path, gog_gameinfo_filename) 121 | 122 | # Native Linux games seem to only store 'start.sh' as their executable -- Assume native Linux if no wine_info 123 | if not game.wine_info: 124 | return 'start.sh' 125 | 126 | if not os.path.isfile(gog_gameinfo_json_path) or not game.runner.lower() == 'gog': 127 | return '' 128 | 129 | gog_gameinfo_json: dict[str, Any] = json.load(open(gog_gameinfo_json_path)) 130 | gog_gameinfo_name: str = gog_gameinfo_json.get('name', '') 131 | gog_gameinfo_playtasks: dict[str, Any] = gog_gameinfo_json.get('playTasks', {}) 132 | for playtasks in gog_gameinfo_playtasks: 133 | if playtasks.get('name', '').lower() == gog_gameinfo_name.lower(): 134 | return playtasks.get('path', '') 135 | else: 136 | return '' 137 | -------------------------------------------------------------------------------- /pupgui2/lutrisutil.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | 4 | from pupgui2.datastructures import LutrisGame 5 | 6 | 7 | LUTRIS_PGA_GAMELIST_QUERY = 'SELECT slug, name, runner, installer_slug, installed_at, directory FROM games' 8 | 9 | 10 | def get_lutris_game_list(install_loc) -> list[LutrisGame]: 11 | """ 12 | Returns a list of installed games in Lutris 13 | Return Type: list[LutrisGame] 14 | """ 15 | install_dir = os.path.expanduser(install_loc.get('install_dir')) 16 | lutris_data_dir = os.path.join(install_dir, os.pardir, os.pardir) 17 | pga_db_file = os.path.join(lutris_data_dir, 'pga.db') 18 | lgs = [] 19 | try: 20 | con = sqlite3.connect(pga_db_file) 21 | cur = con.cursor() 22 | cur.execute(LUTRIS_PGA_GAMELIST_QUERY) 23 | res = cur.fetchall() 24 | for g in res: 25 | lg = LutrisGame() 26 | lg.install_loc = install_loc 27 | lg.slug = g[0] 28 | lg.name = g[1] 29 | lg.runner = g[2] 30 | lg.installer_slug = g[3] 31 | lg.installed_at = g[4] 32 | 33 | # Lutris database file will only store some fields for games installed via an installer and not if it was manually added 34 | # If a game doesn't have an install dir (e.g. it was manually added to Lutris), try to use the following for the install dir: 35 | # - Working directory (may not be specified) 36 | # - Executable: may not be accurate as the exe could be heavily nested, but a good fallback) 37 | lutris_install_dir = g[5] 38 | if not lutris_install_dir: 39 | lg_config = lg.get_game_config() 40 | lg_game_config = lg_config.get('game', {}) 41 | 42 | working_dir = lg_game_config.get('working_dir') 43 | exe_dir = lg_game_config.get('exe') 44 | 45 | lutris_install_dir = working_dir or (os.path.dirname(str(exe_dir)) if exe_dir else None) 46 | 47 | # If a LutrisGame config has an 'appid' in its 'game' section in its yml, assume runner is Steam 48 | if lg_game_config.get('appid', None) is not None: 49 | lg.runner = 'steam' 50 | 51 | lg.install_dir = os.path.abspath(lutris_install_dir) if lutris_install_dir else '' 52 | lgs.append(lg) 53 | except Exception as e: 54 | print('Error: Could not get lutris game list:', e) 55 | return lgs 56 | 57 | 58 | def is_lutris_game_using_runner(game: LutrisGame, runner: str) -> bool: 59 | 60 | """ Determine if a LutrisGame is using a given runner. """ 61 | 62 | is_runner_name_valid = game.runner is not None and len(game.runner) > 0 63 | is_using_runner = game.runner == runner 64 | 65 | return is_runner_name_valid and is_using_runner 66 | 67 | 68 | def is_lutris_game_using_wine(game: LutrisGame, wine_version: str = '') -> bool: 69 | 70 | """ Determine if a LutrisGame is using a given wine_version string. """ 71 | 72 | is_using_wine = is_lutris_game_using_runner(game, 'wine') 73 | 74 | # Only check wine_version if it is passed 75 | if len(wine_version) > 0: 76 | is_using_wine_version = game.get_game_config().get('wine', {}).get('version', '') == wine_version 77 | else: 78 | is_using_wine_version = True 79 | 80 | return is_using_wine and is_using_wine_version 81 | -------------------------------------------------------------------------------- /pupgui2/networkutil.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import math 4 | 5 | from PySide6.QtCore import Property 6 | 7 | from typing import Callable 8 | 9 | 10 | def download_file(url: str, destination: str, progress_callback: Callable[[int], None] | Callable[..., None] = lambda *args, **kwargs: None, download_cancelled: Property | None = None, buffer_size: int = 65536, stream: bool = True, known_size: int = 0): 11 | """ 12 | Download a file from a given URL using `requests` to a destination directory with download progress, with some optional parameters: 13 | * `progress_callback`: Function or Lambda that gets called with the download progress each time it changes 14 | * `download_cancelled`: Qt Property that can stop the download 15 | * `buffer_size`: Size of chunks to download the file in 16 | * `stream`: Lazily parse response - If response headers won't contain `'Content-Length'` and the file size is not known ahead of time, set this to `False` to get file size from response content length 17 | * `known_size`: If size is known ahead of time, this can be given to calculate download progress in place of Content-Length header (e.g. where it may be missing) 18 | 19 | Returns `True` if download succeeds, `False` otherwise. 20 | 21 | Raises: `OSError`, `requests.ConnectionError`, `requests.Timeout` 22 | Return Type: bool 23 | """ 24 | 25 | # Try to get the data for the file we want 26 | try: 27 | response: requests.Response = requests.get(url, stream=stream) 28 | except (OSError, requests.ConnectionError, requests.Timeout) as e: 29 | print(f"Error: Failed to make request to URL '{url}', cannot complete download! Reason: {e}") 30 | raise e 31 | 32 | progress_callback(1) # 1 = download started 33 | 34 | # Figure out file size for reporting download progress 35 | if stream and response.headers.get('Transfer-Encoding', '').lower() == 'chunked': 36 | print("Warning: Using 'stream=True' in request but 'Transfer-Encoding' in Response is 'Chunked', so we may not get 'Content-Length' to parse file size!") 37 | 38 | # Sometimes ctmods can have access to the asset size, so they can give it to us 39 | # If it is not specified, or if it is zero/Falsey, try to get it from the response 40 | file_size = known_size 41 | if not known_size: 42 | file_size = int(response.headers.get('Content-Length', 0)) 43 | 44 | # Sometimes Content-Length is not sent (such as for DXVK Async), so use response length in that case 45 | # See: https://stackoverflow.com/questions/53797628/request-has-no-content-length#53797919 46 | # 47 | # Only get response.content if we aren't streaming so that we don't hold up the entire function, 48 | # and defeating the point of streaming to begin with 49 | if not stream: 50 | file_size = len(response.content) 51 | 52 | if file_size <= 0: 53 | print('Warning: Failed to get file size, the progress bar may not display accurately!') 54 | 55 | if buffer_size <= 0: 56 | print(f"Warning: Buffer Size was '{buffer_size}', defaulting to '65536'") 57 | buffer_size = 65536 58 | 59 | # NOTE: If we don't get a known_size or if we can't get the size from Cotent-Length or the response size, 60 | # we cannot report download progress! 61 | # 62 | # Right now, only GitLab doesn't give us Content-Length because it uses Chunked Transfer-Encoding, 63 | # but ctmods should be able to get the size and pass it as known_size. 64 | # 65 | # If we ever make it this far without a file_size (e.g. we are stream=True and we don't get a 66 | # Content-Length, or len(response.content) is 0), then then the progress bar will stall at 1% until 67 | # the download finishes where it will jump to 99%, until extraction completes. 68 | try: 69 | chunk_count = math.ceil(file_size / buffer_size) 70 | except ZeroDivisionError as e: 71 | print(f'Error: Could not calculate chunk_count, {e}') 72 | print('Defaulting to chunk count of 1') 73 | chunk_count = 1 74 | 75 | current_chunk = 1 76 | 77 | # Get download filepath and download directory path without filename 78 | destination_file_path: str = os.path.expanduser(destination) 79 | destination_dir_path: str = os.path.dirname(destination_file_path) 80 | 81 | # Create download path if it doesn't exist (and make sure we have permission to do so) 82 | try: 83 | os.makedirs(destination_dir_path, exist_ok=True) 84 | except OSError as e: 85 | print(f'Error: Failed to create path to destination directory, cannot complete download! Reason: {e}') 86 | raise e 87 | 88 | # Download file and return progress to any given callback 89 | with open(destination, 'wb') as destination_file: 90 | for chunk in response.iter_content(chunk_size=buffer_size): 91 | chunk: bytes 92 | 93 | if download_cancelled: 94 | progress_callback(-2) # -2 = Download cancelled 95 | return False 96 | 97 | if not chunk: 98 | continue 99 | 100 | _ = destination_file.write(chunk) 101 | destination_file.flush() 102 | 103 | download_progress = int(min(current_chunk / chunk_count * 98.0, 98.0)) # 1...98 = Download in progress 104 | progress_callback(download_progress) 105 | 106 | current_chunk += 1 107 | 108 | progress_callback(99) # 99 = Download completed successfully 109 | return True 110 | 111 | -------------------------------------------------------------------------------- /pupgui2/pupgui2aboutdialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkgutil 3 | import requests 4 | 5 | from PySide6.QtCore import Qt, QObject, QDataStream, QByteArray, QSize 6 | from PySide6.QtGui import QPixmap, QIcon 7 | from PySide6.QtWidgets import QApplication, QMessageBox 8 | from PySide6.QtUiTools import QUiLoader 9 | 10 | from pupgui2.constants import APP_NAME, APP_VERSION, APP_ID, APP_GHAPI_URL, ABOUT_TEXT, BUILD_INFO, APP_THEMES 11 | from pupgui2.constants import DAVIDOTEK_KOFI_URL, PROTONUPQT_GITHUB_URL 12 | from pupgui2.pupgui2gitaccesstokendialog import PupguiGitAccessTokenDialog 13 | from pupgui2.steamutil import install_steam_library_shortcut 14 | from pupgui2.util import config_theme, apply_dark_theme, config_advanced_mode 15 | from pupgui2.util import open_webbrowser_thread 16 | from pupgui2.util import install_directory 17 | 18 | 19 | class PupguiAboutDialog(QObject): 20 | 21 | def __init__(self, parent=None): 22 | super(PupguiAboutDialog, self).__init__(parent) 23 | self.parent = parent 24 | self.is_update_available = lambda current, newest: tuple(map(int, current.split('.'))) < tuple(map(int, newest.split('.'))) 25 | 26 | self.load_ui() 27 | self.setup_ui() 28 | self.ui.show() 29 | 30 | def load_ui(self): 31 | data = pkgutil.get_data(__name__, 'resources/ui/pupgui2_aboutdialog.ui') 32 | ui_file = QDataStream(QByteArray(data)) 33 | self.ui = QUiLoader().load(ui_file.device()) 34 | 35 | def setup_ui(self): 36 | self.ui.setWindowTitle(f'{APP_NAME} {APP_VERSION}') 37 | 38 | translator_text = QApplication.instance().translate('translator-text', 'Translated by DavidoTek') 39 | 40 | self.ui.lblAppIcon.setPixmap(QIcon.fromTheme(APP_ID).pixmap(QSize(96, 96))) 41 | 42 | self.ui.lblAboutTranslator.setText(translator_text) 43 | self.ui.lblAboutVersion.setTextFormat(Qt.RichText) 44 | self.ui.lblAboutVersion.setOpenExternalLinks(True) 45 | self.ui.lblAboutVersion.setText(ABOUT_TEXT) 46 | 47 | self.ui.lblBuildInfo.setText(BUILD_INFO) 48 | 49 | try: 50 | p = QPixmap() 51 | p.loadFromData(pkgutil.get_data(__name__, 'resources/img/kofi_button_blue.png')) 52 | self.ui.btnDonate.setIcon(QIcon(p)) 53 | self.ui.btnDonate.setIconSize(p.rect().size()) 54 | self.ui.btnDonate.setFlat(True) 55 | finally: 56 | self.ui.btnDonate.setText('') 57 | self.ui.btnDonate.clicked.connect(lambda: open_webbrowser_thread(DAVIDOTEK_KOFI_URL)) 58 | 59 | self.ui.btnGitHub.clicked.connect(lambda: open_webbrowser_thread(PROTONUPQT_GITHUB_URL)) 60 | 61 | self.ui.comboColorTheme.addItems([self.tr('light'), self.tr('dark'), self.tr('system (restart required)'), 'Steam Deck']) 62 | self.ui.comboColorTheme.setCurrentIndex(APP_THEMES.index(config_theme()) if config_theme() in APP_THEMES else (len(APP_THEMES) - 1)) 63 | 64 | self.ui.btnClose.clicked.connect(lambda: self.ui.close()) 65 | self.ui.btnAboutQt.clicked.connect(lambda: QMessageBox.aboutQt(self.parent)) 66 | self.ui.btnCheckForUpdates.clicked.connect(self.btn_check_for_updates_clicked) 67 | self.ui.comboColorTheme.currentIndexChanged.connect(self.combo_color_theme_current_index_changed) 68 | 69 | self.ui.checkAdvancedMode.setChecked(config_advanced_mode() == 'enabled') 70 | self.ui.checkAdvancedMode.stateChanged.connect(self.check_advanced_mode_state_changed) 71 | self.ui.layoutAdvancedOptions.setVisible(config_advanced_mode() == 'enabled') 72 | 73 | self.ui.btnEditGitAccessTokens.clicked.connect(self.btn_edit_git_access_tokens_clicked) 74 | 75 | self.ui.btnAddSteamShortcut.clicked.connect(self.btn_add_steam_shortcut_clicked) 76 | self.ui.btnCheckForUpdates.setVisible(os.getenv('APPIMAGE') is not None) 77 | 78 | def combo_color_theme_current_index_changed(self): 79 | config_theme(APP_THEMES[:-1][self.ui.comboColorTheme.currentIndex()]) 80 | apply_dark_theme(QApplication.instance()) 81 | 82 | def btn_check_for_updates_clicked(self): 83 | releases = requests.get(f'{APP_GHAPI_URL}?per_page=1').json() 84 | if len(releases) == 0: 85 | return 86 | 87 | newest_release = releases[0] 88 | v_newest = newest_release.get('tag_name', 'v0.0.0').replace('v', '') 89 | 90 | if self.is_update_available(APP_VERSION, v_newest): 91 | QMessageBox.information( 92 | self.ui, 93 | self.tr('Update available'), 94 | self.tr('There is a newer version available.\nYou are running {APP_VERSION} but {newest_version} is available.').format(APP_VERSION=f'v{APP_VERSION}', newest_version=f'v{v_newest}') 95 | ) 96 | open_webbrowser_thread(newest_release['html_url']) 97 | else: 98 | QMessageBox.information(self.ui, self.tr('Up to date'), self.tr('You are running the newest version!')) 99 | 100 | def btn_add_steam_shortcut_clicked(self): 101 | result = install_steam_library_shortcut(install_directory()) 102 | if result != 1: 103 | self.ui.btnAddSteamShortcut.setText(self.tr('Added shortcut!')) 104 | self.ui.btnAddSteamShortcut.setEnabled(False) 105 | 106 | def check_advanced_mode_state_changed(self, state: int): 107 | config_advanced_mode('enabled' if state > 0 else 'disabled') 108 | self.ui.layoutAdvancedOptions.setVisible(bool(state)) 109 | 110 | def btn_edit_git_access_tokens_clicked(self): 111 | PupguiGitAccessTokenDialog(self.ui) 112 | -------------------------------------------------------------------------------- /pupgui2/pupgui2ctbatchupdatedialog.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | 3 | from PySide6.QtCore import Signal, Qt, QObject, QDataStream, QByteArray 4 | from PySide6.QtWidgets import QFormLayout, QLabel 5 | from PySide6.QtUiTools import QUiLoader 6 | 7 | from pupgui2.steamutil import is_steam_running, steam_update_ctool 8 | from pupgui2.util import sort_compatibility_tool_names, list_installed_ctools, install_directory 9 | 10 | 11 | class PupguiCtBatchUpdateDialog(QObject): 12 | 13 | batch_update_complete = Signal(bool) 14 | 15 | def __init__(self, parent=None, current_ctool_name: str='', games=[], steam_config_folder=''): 16 | super(PupguiCtBatchUpdateDialog, self).__init__(parent) 17 | self.games = games 18 | self.steam_config_folder = steam_config_folder 19 | 20 | self.ctools = sort_compatibility_tool_names(list_installed_ctools(install_directory()), reverse=True) 21 | 22 | self.load_ui() 23 | self.setup_ui(current_ctool_name) 24 | self.ui.show() 25 | 26 | def load_ui(self) -> None: 27 | data = pkgutil.get_data(__name__, 'resources/ui/pupgui2_ctbatchupdatedialog.ui') 28 | ui_file = QDataStream(QByteArray(data)) 29 | self.ui = QUiLoader().load(ui_file.device()) 30 | 31 | def setup_ui(self, current_ctool_name: str) -> None: 32 | # Doing ctool checks here instead of before showing the batch update button on ctinfo dialog covers case where 33 | # compatibility tool may have been available but then was removed (maybe manually?) -- Is potentially just more robust 34 | combobox_ctools = [ctool for ctool in self.ctools if 'Proton' in ctool and current_ctool_name not in ctool] 35 | self.ui.comboNewCtool.addItems(combobox_ctools) 36 | self.ui.oldVersionText.setText(f' {current_ctool_name}') 37 | 38 | # Batch update only disabled when no ctools installed 39 | # Not disabled when Steam Client is running because it can be closed while this dialog is open 40 | self.ui.comboNewCtool.setEnabled(len(combobox_ctools) > 0) 41 | self.ui.btnBatchUpdate.setEnabled(len(combobox_ctools) > 0) 42 | 43 | self.ui.btnBatchUpdate.clicked.connect(self.btn_batch_update_clicked) 44 | self.ui.btnClose.clicked.connect(lambda: self.ui.close()) 45 | 46 | if len(combobox_ctools) <= 0: # No ctools to migrate to installed 47 | self.add_warning_message(self.tr('No supported compatibility tools found.'), self.ui.formLayout, stylesheet='QLabel { color: red; }') 48 | elif is_steam_running(): # Steam is running so any writes to config.vdf will get overwritten on Steam Client exit 49 | self.add_warning_message(self.tr('Warning: Close the Steam Client beforehand.'), self.ui.formLayout) 50 | else: # Spacer label 51 | self.ui.formLayout.addRow(QLabel()) 52 | 53 | def add_warning_message(self, msg: str, layout: QFormLayout, stylesheet: str = 'QLabel { color: orange; }'): 54 | """ 55 | Add a QLabel warning message with a default Orange stylesheet to display a warning message in a Layout. 56 | """ 57 | 58 | lblWarning = QLabel(msg) 59 | lblWarning.setAlignment(Qt.AlignmentFlag.AlignCenter) 60 | lblWarning.setStyleSheet(stylesheet) 61 | layout.addRow(lblWarning) 62 | 63 | def btn_batch_update_clicked(self): 64 | self.update_games_to_ctool(self.ui.comboNewCtool.currentText()) 65 | self.batch_update_complete.emit(True) 66 | self.ui.close() 67 | 68 | def update_games_to_ctool(self, ctool): 69 | for game in self.games: 70 | steam_update_ctool(game, ctool, self.steam_config_folder) 71 | -------------------------------------------------------------------------------- /pupgui2/pupgui2ctinfodialog.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | import os 3 | 4 | from pupgui2.constants import STEAM_APP_PAGE_URL 5 | from pupgui2.datastructures import BasicCompatTool, CTType, SteamApp, LutrisGame, HeroicGame 6 | from pupgui2.lutrisutil import get_lutris_game_list, is_lutris_game_using_wine 7 | from pupgui2.pupgui2ctbatchupdatedialog import PupguiCtBatchUpdateDialog 8 | from pupgui2.steamutil import get_steam_game_list 9 | from pupgui2.util import open_webbrowser_thread, get_random_game_name 10 | from pupgui2.heroicutil import get_heroic_game_list, is_heroic_launcher 11 | 12 | from PySide6.QtCore import QObject, Signal, QDataStream, QByteArray 13 | from PySide6.QtWidgets import QTableWidgetItem 14 | from PySide6.QtUiTools import QUiLoader 15 | from PySide6.QtCore import Qt 16 | from PySide6.QtGui import QShortcut, QKeySequence 17 | 18 | 19 | class PupguiCtInfoDialog(QObject): 20 | 21 | batch_update_complete = Signal(bool) 22 | 23 | def __init__(self, parent=None, ctool: BasicCompatTool = None, install_loc=None): 24 | super(PupguiCtInfoDialog, self).__init__(parent) 25 | self.parent = parent 26 | self.ctool = ctool 27 | self.games: list[SteamApp | LutrisGame | HeroicGame] = [] 28 | self.install_loc = install_loc 29 | self.is_batch_update_available = False 30 | 31 | self.load_ui() 32 | self.setup_ui() 33 | self.ui.show() 34 | 35 | def load_ui(self): 36 | data = pkgutil.get_data(__name__, 'resources/ui/pupgui2_ctinfodialog.ui') 37 | ui_file = QDataStream(QByteArray(data)) 38 | loader = QUiLoader() 39 | self.ui = loader.load(ui_file.device()) 40 | 41 | def setup_ui(self): 42 | self.ui.txtToolName.setText(self.ctool.displayname) 43 | self.ui.txtLauncherName.setText(self.install_loc.get('display_name')) 44 | self.ui.txtInstallDirectory.setText(self.ctool.get_install_dir()) 45 | self.ui.btnBatchUpdate.setVisible(False) 46 | self.ui.searchBox.setVisible(False) 47 | 48 | self.update_game_list() 49 | 50 | self.ui.btnSearch.clicked.connect(self.btn_search_clicked) 51 | self.ui.btnRefreshGames.clicked.connect(self.btn_refresh_games_clicked) 52 | self.ui.btnClose.clicked.connect(lambda: self.ui.close()) 53 | self.ui.listGames.cellDoubleClicked.connect(self.list_games_cell_double_clicked) 54 | self.ui.searchBox.textChanged.connect(self.search_ctinfo_games) 55 | 56 | QShortcut(QKeySequence.Find, self.ui).activated.connect(self.btn_search_clicked) 57 | 58 | def update_game_list(self, cached=True): 59 | if self.install_loc.get('launcher') == 'steam' and 'vdf_dir' in self.install_loc: 60 | self.update_game_list_steam(cached=cached) 61 | if 'Proton' in self.ctool.displayname and self.ctool.ct_type == CTType.CUSTOM: # 'batch update' option for Proton-GE 62 | self.is_batch_update_available = True 63 | self.ui.btnBatchUpdate.setVisible(not self.ui.searchBox.isVisible()) 64 | self.ui.btnBatchUpdate.clicked.connect(self.btn_batch_update_clicked) 65 | elif self.install_loc.get('launcher') == 'lutris': 66 | self.update_game_list_lutris() 67 | elif is_heroic_launcher(self.install_loc.get('launcher')): 68 | self.update_game_list_heroic() 69 | else: 70 | self.ui.txtNumGamesUsingTool.setText('-') 71 | self.ui.listGames.setHorizontalHeaderLabels(['', '']) 72 | self.ui.listGames.setEnabled(False) 73 | 74 | self.update_game_list_ui() 75 | 76 | def update_game_list_steam(self, cached=True): 77 | if self.install_loc.get('launcher') == 'steam' and 'vdf_dir' in self.install_loc: 78 | self.games = get_steam_game_list(self.install_loc.get('vdf_dir'), self.ctool, cached=cached) 79 | self.ui.txtNumGamesUsingTool.setText(str(len(self.games))) 80 | 81 | self.ui.listGames.clear() 82 | self.ui.listGames.setRowCount(len(self.games)) 83 | self.ui.listGames.setHorizontalHeaderLabels([self.tr('AppID'), self.tr('Name')]) 84 | for i, game in enumerate(self.games): 85 | dataitem_appid = QTableWidgetItem() 86 | dataitem_appid.setData(Qt.DisplayRole, int(game.get_app_id_str())) 87 | 88 | self.ui.listGames.setItem(i, 0, dataitem_appid) 89 | self.ui.listGames.setItem(i, 1, QTableWidgetItem(game.game_name)) 90 | 91 | self.batch_update_complete.emit(True) 92 | 93 | def update_game_list_lutris(self): 94 | self.games = [game for game in get_lutris_game_list(self.install_loc) if is_lutris_game_using_wine(game, self.ctool.displayname)] 95 | 96 | self.setup_game_list(len(self.games), [self.tr('Slug'), self.tr('Name')]) 97 | 98 | for i, game in enumerate(self.games): 99 | self.ui.listGames.setItem(i, 0, QTableWidgetItem(game.slug)) 100 | self.ui.listGames.setItem(i, 1, QTableWidgetItem(game.name)) 101 | 102 | def update_game_list_heroic(self): 103 | heroic_dir = os.path.join(os.path.expanduser(self.install_loc.get('install_dir')), '../..') 104 | self.games = [game for game in get_heroic_game_list(heroic_dir) if game.is_installed and self.ctool.displayname in game.wine_info.get('name', '')] 105 | 106 | self.setup_game_list(len(self.games), [self.tr('Runner'), self.tr('Game')]) 107 | 108 | for i, game in enumerate(self.games): 109 | self.ui.listGames.setItem(i, 0, QTableWidgetItem(game.runner)) 110 | self.ui.listGames.setItem(i, 1, QTableWidgetItem(game.title)) 111 | 112 | def setup_game_list(self, row_count: int, header_labels: list[str]): 113 | self.ui.listGames.clear() 114 | self.ui.listGames.setRowCount(row_count) 115 | self.ui.listGames.setHorizontalHeaderLabels(header_labels) 116 | self.ui.txtNumGamesUsingTool.setText(str(row_count)) 117 | 118 | def update_game_list_ui(self): 119 | # switch between showing the QTableWidget (listGames) or the QLabel (lblGamesList) 120 | self.ui.stackTableOrText.setCurrentIndex(0 if len(self.games) > 0 and not self.ctool.is_global else 1) 121 | self.ui.btnBatchUpdate.setEnabled(len(self.games) > 0 and not self.ctool.is_global) 122 | self.ui.btnSearch.setEnabled(len(self.games) > 0 and not self.ctool.is_global) 123 | 124 | if self.ctool.is_global: 125 | self.ui.lblGamesList.setText(self.tr('Tool is Global')) 126 | 127 | if len(self.games) < 0 or self.ctool.is_global: 128 | self.ui.btnClose.setFocus() 129 | 130 | def list_games_cell_double_clicked(self, row): 131 | if self.install_loc.get('launcher') == 'steam': 132 | steam_game_id = str(self.ui.listGames.item(row, 0).text()) 133 | open_webbrowser_thread(STEAM_APP_PAGE_URL + steam_game_id) 134 | 135 | def btn_batch_update_clicked(self): 136 | steam_config_folder = self.install_loc.get('vdf_dir') 137 | ctbu_dialog = PupguiCtBatchUpdateDialog(parent=self.ui, current_ctool_name=self.ctool.displayname, games=self.games, steam_config_folder=steam_config_folder) 138 | ctbu_dialog.batch_update_complete.connect(self.update_game_list_steam) 139 | 140 | def btn_refresh_games_clicked(self): 141 | self.update_game_list(cached=False) 142 | 143 | def btn_search_clicked(self): 144 | if not self.ui.btnSearch.isEnabled(): 145 | return 146 | 147 | # Generate random tooltip here, updating so if a game is removed on refresh, we won't list a game no longer listed 148 | # If game is not found, fall back to tooltip defined in UI file 149 | if tooltip_game_name := get_random_game_name(self.games): 150 | self.ui.searchBox.setToolTip(self.tr('e.g. {GAME_NAME}').format(GAME_NAME=tooltip_game_name)) 151 | 152 | self.ui.searchBox.setVisible(not self.ui.searchBox.isVisible()) 153 | self.ui.btnBatchUpdate.setVisible(self.is_batch_update_available and not self.ui.searchBox.isVisible()) 154 | self.ui.searchBox.setFocus() 155 | 156 | self.search_ctinfo_games(self.ui.searchBox.text() if self.ui.searchBox.isVisible() else '') 157 | 158 | def search_ctinfo_games(self, text): 159 | for row in range(self.ui.listGames.rowCount()): 160 | should_hide: bool = not text.lower() in self.ui.listGames.item(row, 1).text().lower() 161 | self.ui.listGames.setRowHidden(row, should_hide) 162 | -------------------------------------------------------------------------------- /pupgui2/pupgui2customiddialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkgutil 3 | 4 | from PySide6.QtCore import Signal, QDataStream, QByteArray, QObject, QDir 5 | from PySide6.QtGui import QIcon 6 | from PySide6.QtWidgets import QFileDialog, QLineEdit 7 | from PySide6.QtUiTools import QUiLoader 8 | 9 | from pupgui2.util import config_custom_install_location, get_install_location_from_directory_name, get_dict_key_from_value, get_combobox_index_by_value 10 | from pupgui2.constants import HOME_DIR 11 | 12 | 13 | class PupguiCustomInstallDirectoryDialog(QObject): 14 | 15 | custom_id_set = Signal(str) 16 | 17 | def __init__(self, install_dir, parent=None): 18 | super(PupguiCustomInstallDirectoryDialog, self).__init__(parent) 19 | 20 | self.install_loc = get_install_location_from_directory_name(install_dir) 21 | self.launcher = self.install_loc.get('launcher', '') 22 | 23 | self.install_locations_dict = { 24 | 'steam': 'Steam', 25 | 'lutris': 'Lutris', 26 | 'heroicwine': 'Heroic (Wine)', 27 | 'heroicproton': 'Heroic (Proton)', 28 | 'bottles': 'Bottles', 29 | 'winezgui': 'WineZGUI', 30 | } 31 | 32 | self.load_ui() 33 | self.setup_ui() 34 | self.ui.show() 35 | 36 | def load_ui(self): 37 | data = pkgutil.get_data(__name__, 'resources/ui/pupgui2_custominstalldirectorydialog.ui') 38 | ui_file = QDataStream(QByteArray(data)) 39 | loader = QUiLoader() 40 | self.ui = loader.load(ui_file.device()) 41 | 42 | def setup_ui(self): 43 | self.ui.setWindowTitle(self.tr('Custom Install Directory')) 44 | 45 | self.txtIdBrowseAction = self.ui.txtInstallDirectory.addAction(QIcon.fromTheme('document-open'), QLineEdit.TrailingPosition) 46 | self.txtIdBrowseAction.triggered.connect(self.txt_id_browse_action_triggered) 47 | 48 | self.ui.txtInstallDirectory.textChanged.connect(lambda text: self.ui.btnSave.setEnabled(self.is_valid_custom_install_path(text))) 49 | custom_install_directory = config_custom_install_location().get('install_dir', '') 50 | self.ui.txtInstallDirectory.setText(custom_install_directory) 51 | self.ui.btnDefault.setEnabled(self.has_custom_install_directory(custom_install_directory)) # Don't enable btnDefault if there is no Custom Install Directory set 52 | 53 | self.ui.comboLauncher.addItems([ 54 | display_name for display_name in self.install_locations_dict.values() 55 | ]) 56 | 57 | self.set_selected_launcher(self.install_locations_dict[self.launcher] if self.launcher in self.install_locations_dict else 'steam') # Default combobox selection to "Steam" if unknown launcher for some reason 58 | 59 | self.ui.btnSave.clicked.connect(self.btn_save_clicked) 60 | self.ui.btnDefault.clicked.connect(self.btn_default_clicked) 61 | self.ui.btnClose.clicked.connect(self.ui.close) 62 | 63 | def btn_save_clicked(self): 64 | install_dir: str = os.path.expanduser(self.ui.txtInstallDirectory.text().strip()) 65 | if not install_dir.endswith(os.sep): 66 | install_dir += '/' 67 | launcher = get_dict_key_from_value(self.install_locations_dict, self.ui.comboLauncher.currentText()) or '' 68 | 69 | if self.is_valid_custom_install_path(install_dir): 70 | config_custom_install_location(install_dir, launcher) 71 | 72 | self.custom_id_set.emit(install_dir) 73 | self.ui.close() 74 | 75 | def btn_default_clicked(self): 76 | self.ui.txtInstallDirectory.setText('') 77 | 78 | custom_install_directory = config_custom_install_location(remove=True).get('install_dir', '') 79 | self.ui.btnDefault.setEnabled(self.has_custom_install_directory(custom_install_directory)) 80 | 81 | self.custom_id_set.emit('') 82 | 83 | def txt_id_browse_action_triggered(self): 84 | # Open dialog at entered path if it exists, and fall back to HOME_DIR 85 | txt_install_dir: str = os.path.expanduser(self.ui.txtInstallDirectory.text()) 86 | initial_dir = txt_install_dir if self.is_valid_custom_install_path(txt_install_dir) else HOME_DIR 87 | 88 | dialog = QFileDialog(self.ui, directory=initial_dir) 89 | dialog.setFileMode(QFileDialog.Directory) 90 | dialog.setOption(QFileDialog.ShowDirsOnly) 91 | dialog.setFilter(QDir.Dirs | QDir.Hidden | QDir.NoDotAndDotDot) 92 | dialog.setWindowTitle(self.tr('Select Custom Install Directory — ProtonUp-Qt')) 93 | dialog.fileSelected.connect(self.ui.txtInstallDirectory.setText) 94 | dialog.open() 95 | 96 | def set_selected_launcher(self, ctool_name: str): 97 | if not ctool_name: 98 | return 99 | 100 | if (index := get_combobox_index_by_value(self.ui.comboLauncher, ctool_name)) >= 0: 101 | self.ui.comboLauncher.setCurrentIndex(index) 102 | 103 | def is_valid_custom_install_path(self, path: str) -> bool: 104 | expand_path = os.path.expanduser(path) 105 | return len(path.strip()) > 0 and os.path.isdir(expand_path) and os.access(expand_path, os.W_OK) 106 | 107 | def has_custom_install_directory(self, custom_install_directory: str = '') -> bool: 108 | 109 | """ 110 | Returns whether a Custom Install Directory is set to a Truthy value. 111 | If `custom_install_directory` is not passed, it will be retrieved. 112 | 113 | Return Type: bool 114 | """ 115 | 116 | if not custom_install_directory: 117 | return bool(config_custom_install_location().get('install_dir', '')) 118 | 119 | return bool(custom_install_directory) 120 | -------------------------------------------------------------------------------- /pupgui2/pupgui2exceptionhandler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import traceback 4 | 5 | from types import TracebackType 6 | 7 | from PySide6.QtCore import QObject, Signal, Slot 8 | from PySide6.QtWidgets import QMessageBox, QApplication 9 | 10 | 11 | class PupguiExceptionHandler(QObject): 12 | exception = Signal(type, BaseException, TracebackType) 13 | 14 | def __init__(self, parent): 15 | self.logger = logging.getLogger(type(self).__name__) 16 | super(PupguiExceptionHandler, self).__init__(parent) 17 | sys.excepthook = self._excepthook 18 | self.exception.connect(self._on_exception) 19 | 20 | def _excepthook(self, exc_type: type, exc_value: BaseException, exc_tb: TracebackType): 21 | self.exception.emit(exc_type, exc_value, exc_tb) 22 | 23 | @Slot(type, BaseException, TracebackType) 24 | def _on_exception(self, exc_type: type, exc_value: BaseException, exc_tb: TracebackType): 25 | message = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) 26 | 27 | self.logger.fatal(message) 28 | QMessageBox.critical(None, exc_type.__name__, message) 29 | QApplication.quit() 30 | -------------------------------------------------------------------------------- /pupgui2/pupgui2gitaccesstokendialog.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | 3 | from PySide6.QtCore import QDataStream, QByteArray, QObject 4 | from PySide6.QtUiTools import QUiLoader 5 | 6 | from pupgui2.util import config_github_access_token, config_gitlab_access_token 7 | 8 | 9 | class PupguiGitAccessTokenDialog(QObject): 10 | 11 | def __init__(self, parent=None): 12 | super(PupguiGitAccessTokenDialog, self).__init__(parent) 13 | 14 | self.load_ui() 15 | self.setup_ui() 16 | self.ui.show() 17 | 18 | def load_ui(self): 19 | data = pkgutil.get_data(__name__, 'resources/ui/pupgui2_gitaccesstokendialog.ui') 20 | ui_file = QDataStream(QByteArray(data)) 21 | loader = QUiLoader() 22 | self.ui = loader.load(ui_file.device()) 23 | 24 | def setup_ui(self): 25 | self.ui.txtGitHubToken.setText(config_github_access_token()) 26 | self.ui.txtGitLabToken.setText(config_gitlab_access_token()) 27 | 28 | self.ui.btnSave.clicked.connect(self.btn_save_clicked) 29 | self.ui.btnClose.clicked.connect(self.ui.close) 30 | 31 | def btn_save_clicked(self): 32 | github_token = self.ui.txtGitHubToken.text() 33 | gitlab_token = self.ui.txtGitLabToken.text() 34 | 35 | config_github_access_token(github_token) 36 | config_gitlab_access_token(gitlab_token) 37 | 38 | self.ui.close() 39 | -------------------------------------------------------------------------------- /pupgui2/pupgui2installdialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | import pkgutil 4 | 5 | from PySide6.QtCore import Signal, QLocale, QDataStream, QByteArray 6 | from PySide6.QtGui import QIcon, QPixmap, Qt 7 | from PySide6.QtWidgets import QDialog 8 | from PySide6.QtUiTools import QUiLoader 9 | 10 | from pupgui2.util import open_webbrowser_thread, config_advanced_mode, get_combobox_index_by_value 11 | 12 | 13 | RELEASES_PER_PAGE = 50 # Number of releases to fetch per page 14 | 15 | 16 | class PupguiInstallDialog(QDialog): 17 | 18 | is_fetching_releases = Signal(bool) 19 | compat_tool_selected = Signal(dict) 20 | 21 | def __init__(self, install_location, ct_loader, parent=None): 22 | super(PupguiInstallDialog, self).__init__(parent) 23 | self.install_location = install_location 24 | advanced_mode = (config_advanced_mode() == 'enabled') 25 | self.ct_objs = ct_loader.get_ctobjs(self.install_location, advanced_mode=advanced_mode) 26 | self.current_ct_obj = None 27 | self.loaded_page = 1 28 | self.more_releases_loadable = True # Set to False when no more versions are available 29 | 30 | self.load_ui() 31 | self.load_assets() 32 | self.setup_ui() 33 | self.ui.show() 34 | 35 | def load_ui(self): 36 | data = pkgutil.get_data(__name__, 'resources/ui/pupgui2_installdialog.ui') 37 | ui_file = QDataStream(QByteArray(data)) 38 | self.ui = QUiLoader().load(ui_file.device()) 39 | 40 | def load_assets(self): 41 | p = QPixmap() 42 | p.loadFromData(pkgutil.get_data(__name__, os.path.join('resources/img/arrow_down.png'))) 43 | self.arrow_down_icon = QIcon(p) 44 | 45 | def setup_ui(self): 46 | self.ui.btnInfo.clicked.connect(self.btn_info_clicked) 47 | self.ui.btnInstall.clicked.connect(self.btn_install_clicked) 48 | self.ui.btnCancel.clicked.connect(lambda: self.ui.close()) 49 | self.ui.comboCompatTool.currentIndexChanged.connect(self.combo_compat_tool_current_index_changed) 50 | self.ui.comboCompatTool.view().setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 51 | self.ui.comboCompatToolVersion.currentIndexChanged.connect(self.combo_compat_tool_version_current_index_changed) 52 | self.ui.comboCompatToolVersion.view().setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 53 | self.is_fetching_releases.connect(lambda x: self.ui.comboCompatTool.setEnabled(not x)) 54 | self.is_fetching_releases.connect(lambda x: self.ui.comboCompatToolVersion.setEnabled(not x)) 55 | self.is_fetching_releases.connect(lambda x: self.ui.btnInfo.setEnabled(not x)) 56 | self.is_fetching_releases.connect(lambda x: self.ui.btnInstall.setEnabled(not x)) 57 | 58 | combobox_style = 'QComboBox { combobox-popup: 0; } QComboBox QAbstractItemView::item { padding: 3px; }' 59 | self.ui.comboCompatTool.setStyleSheet(combobox_style) 60 | self.ui.comboCompatToolVersion.setStyleSheet(combobox_style) 61 | 62 | self.ui.comboCompatTool.addItems([ctobj['name'] for ctobj in self.ct_objs]) 63 | 64 | def btn_info_clicked(self): 65 | for ctobj in self.ct_objs: 66 | if ctobj['name'] == self.ui.comboCompatTool.currentText(): 67 | ver = self.ui.comboCompatToolVersion.currentText() 68 | open_webbrowser_thread(ctobj['installer'].get_info_url(ver) if ver else ctobj['installer'].get_info_url(ver).replace('tag', '')) 69 | return 70 | 71 | def btn_install_clicked(self): 72 | self.compat_tool_selected.emit({ 73 | 'name': self.ui.comboCompatTool.currentText(), 74 | 'version': self.ui.comboCompatToolVersion.currentText(), 75 | 'install_dir': self.install_location['install_dir'] 76 | }) 77 | self.ui.close() 78 | 79 | def update_releases(self): 80 | """ 81 | Update the versions combobox with the releases of the selected compatibility tool. 82 | It will add additional releases to the combobox when self.loaded_page is >1. 83 | """ 84 | if not self.current_ct_obj: 85 | print("Error, InstallDialog: Could not find compatibility tool object.") 86 | return 87 | 88 | def _threadupdate_releases_thread(): 89 | self.is_fetching_releases.emit(True) 90 | 91 | if self.loaded_page == 1: 92 | self.ui.comboCompatToolVersion.clear() 93 | else: 94 | self.ui.comboCompatToolVersion.removeItem(self.ui.comboCompatToolVersion.count() - 1) 95 | 96 | vers = self.current_ct_obj['installer'].fetch_releases(count=RELEASES_PER_PAGE, page=self.loaded_page) 97 | 98 | # If the number of fetched releases is less than RELEASES_PER_PAGE, there are no more releases to fetch 99 | if len(vers) < RELEASES_PER_PAGE: 100 | self.more_releases_loadable = False 101 | 102 | # Stops install dialog UI elements from being enabled when rate-limited to prevent switching/installing tools 103 | if len(vers) > 0: 104 | self.ui.comboCompatToolVersion.addItems(vers) 105 | self.ui.comboCompatToolVersion.setCurrentIndex(0) 106 | 107 | if self.more_releases_loadable: 108 | self.ui.comboCompatToolVersion.addItem(self.tr('Load more...')) 109 | self.ui.comboCompatToolVersion.setItemIcon(self.ui.comboCompatToolVersion.count() - 1, self.arrow_down_icon) 110 | 111 | self.is_fetching_releases.emit(False) 112 | 113 | t = threading.Thread(target=_threadupdate_releases_thread) 114 | t.start() 115 | 116 | def combo_compat_tool_current_index_changed(self): 117 | """ fetch and show available releases for selected compatibility tool """ 118 | for ctobj in self.ct_objs: 119 | if ctobj['name'] == self.ui.comboCompatTool.currentText(): 120 | self.current_ct_obj = ctobj 121 | self.loaded_page = 1 122 | self.more_releases_loadable = True 123 | self.update_releases() 124 | self.update_description(ctobj) 125 | return 126 | 127 | def combo_compat_tool_version_current_index_changed(self): 128 | """ load more releases when the "Load more..." item is selected """ 129 | # The last item in the combobox is the "Load more..." item if self.more_releases_loadable is True 130 | if not self.more_releases_loadable: 131 | return 132 | 133 | if self.ui.comboCompatToolVersion.currentIndex() == self.ui.comboCompatToolVersion.count() - 1: 134 | self.loaded_page += 1 135 | self.update_releases() 136 | 137 | def update_description(self, ctobj): 138 | """ get (translated) description and update description text """ 139 | app_lang = QLocale.languageToCode(QLocale().language()) 140 | app_lname = QLocale().name() 141 | 142 | if app_lname in ctobj['description']: # Examples: zh_TW, de_DE 143 | desc = ctobj['description'][app_lname] 144 | elif app_lang in ctobj['description']: # Examples: de, nl 145 | desc = ctobj['description'][app_lang] 146 | else: 147 | desc = ctobj['description']['en'] 148 | 149 | self.ui.txtDescription.setHtml(desc) 150 | 151 | def set_selected_compat_tool(self, ctool_name: str): 152 | """ Set compat tool dropdown selected index to the index of the compat tool name passed """ 153 | if ctool_name: 154 | index = get_combobox_index_by_value(self.ui.comboCompatTool, ctool_name) 155 | if index >= 1: 156 | self.ui.comboCompatTool.setCurrentIndex(index) 157 | -------------------------------------------------------------------------------- /pupgui2/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/__init__.py -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/ctmods/__init__.py -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_00protonge.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Proton-GE 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | import os 6 | import requests 7 | import hashlib 8 | 9 | from PySide6.QtWidgets import QMessageBox 10 | from PySide6.QtCore import QObject, QCoreApplication, Signal, Property 11 | 12 | from pupgui2.datastructures import Launcher 13 | from pupgui2.util import fetch_project_release_data, fetch_project_releases 14 | from pupgui2.util import get_launcher_from_installdir, extract_tar 15 | from pupgui2.util import build_headers_with_authorization 16 | from pupgui2.networkutil import download_file 17 | 18 | 19 | CT_NAME = 'GE-Proton' 20 | CT_LAUNCHERS = ['steam', 'lutris', 'heroicproton', 'bottles'] 21 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_00protonge', '''Steam compatibility tool for running Windows games with improvements over Valve's default Proton.

Use this when you don't know what to choose.''')} 22 | 23 | 24 | class CtInstaller(QObject): 25 | 26 | BUFFER_SIZE = 65536 27 | CT_URL = 'https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases' 28 | CT_INFO_URL = 'https://github.com/GloriousEggroll/proton-ge-custom/releases/tag/' 29 | 30 | p_download_progress_percent = 0 31 | download_progress_percent = Signal(int) 32 | message_box_message = Signal(str, str, QMessageBox.Icon) 33 | 34 | def __init__(self, main_window = None): 35 | super(CtInstaller, self).__init__() 36 | self.p_download_canceled = False 37 | 38 | self.release_format = 'tar.gz' 39 | 40 | self.rs = requests.Session() 41 | rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'github') 42 | self.rs.headers.update(rs_headers) 43 | 44 | def get_download_canceled(self): 45 | return self.p_download_canceled 46 | 47 | def set_download_canceled(self, val): 48 | self.p_download_canceled = val 49 | 50 | download_canceled = Property(bool, get_download_canceled, set_download_canceled) 51 | 52 | def __set_download_progress_percent(self, value : int): 53 | if self.p_download_progress_percent == value: 54 | return 55 | self.p_download_progress_percent = value 56 | self.download_progress_percent.emit(value) 57 | 58 | def __download(self, url: str, destination: str, known_size: int = 0) -> bool: 59 | """ 60 | Download files from url to destination 61 | Return Type: bool 62 | """ 63 | try: 64 | return download_file( 65 | url=url, 66 | destination=destination, 67 | progress_callback=self.__set_download_progress_percent, 68 | download_cancelled=self.download_canceled, 69 | buffer_size=self.BUFFER_SIZE, 70 | stream=True, 71 | known_size=known_size 72 | ) 73 | except Exception as e: 74 | print(f"Failed to download tool {CT_NAME} - Reason: {e}") 75 | 76 | self.message_box_message.emit( 77 | self.tr("Download Error!"), 78 | self.tr( 79 | "Failed to download tool '{CT_NAME}'!\n\nReason: {EXCEPTION}".format(CT_NAME=CT_NAME, EXCEPTION=e)), 80 | QMessageBox.Icon.Warning 81 | ) 82 | 83 | def __sha512sum(self, filename): 84 | """ 85 | Get SHA512 checksum of a file 86 | Return Type: str 87 | """ 88 | sha512sum = hashlib.sha512() 89 | with open(filename, 'rb') as file: 90 | while True: 91 | data = file.read(self.BUFFER_SIZE) 92 | if not data: 93 | break 94 | sha512sum.update(data) 95 | return sha512sum.hexdigest() 96 | 97 | def __fetch_github_data(self, tag): 98 | """ 99 | Fetch GitHub release information 100 | Return Type: dict 101 | Content(s): 102 | 'version', 'date', 'download', 'size', 'checksum' 103 | """ 104 | 105 | return fetch_project_release_data(self.CT_URL, self.release_format, self.rs, tag=tag, checksum_suffix='.sha512sum') 106 | 107 | def __get_data(self, version: str, install_dir: str) -> tuple[dict | None, str | None]: 108 | 109 | """ 110 | Get needed download data and path to extract directory. 111 | Return Type: tuple[dict | None, str | None] 112 | """ 113 | 114 | data = self.__fetch_github_data(version) 115 | if not data or 'download' not in data: 116 | return (None, None) 117 | 118 | protondir = os.path.join(install_dir, data['version']) 119 | 120 | return (data, protondir) 121 | 122 | def is_system_compatible(self) -> bool: 123 | """ 124 | Are the system requirements met? 125 | Return Type: bool 126 | """ 127 | return True 128 | 129 | def fetch_releases(self, count: int = 100, page: int = 1) -> list[str]: 130 | """ 131 | List available releases 132 | Return Type: str[] 133 | """ 134 | 135 | return fetch_project_releases(self.CT_URL, self.rs, count=count) 136 | 137 | def get_tool(self, version, install_dir, temp_dir): 138 | """ 139 | Download and install the compatibility tool 140 | Return Type: bool 141 | """ 142 | 143 | install_dir = self.get_extract_dir(install_dir) 144 | 145 | data, protondir = self.__get_data(version, install_dir) 146 | if not data: 147 | return False 148 | 149 | # Note: protondir is only used for checksums 150 | if not protondir or not os.path.exists(protondir): 151 | protondir = os.path.join(install_dir, 'Proton-' + data['version']) # Check if we have an older Proton-GE folder name 152 | 153 | checksum_dir = f'{protondir}/sha512sum' 154 | source_checksum = self.rs.get(data['checksum']).text if 'checksum' in data else None 155 | local_checksum = open(checksum_dir).read() if os.path.exists(checksum_dir) else None 156 | 157 | if os.path.exists(protondir): 158 | if local_checksum and source_checksum: 159 | if local_checksum in source_checksum: 160 | return False 161 | else: 162 | return False 163 | 164 | proton_tar = os.path.join(temp_dir, data['download'].split('/')[-1]) 165 | if not self.__download(url=data['download'], destination=proton_tar): 166 | return False 167 | 168 | download_checksum = self.__sha512sum(proton_tar) 169 | if source_checksum and (download_checksum not in source_checksum): 170 | return False 171 | 172 | if not extract_tar(proton_tar, install_dir, mode=self.release_format.split('.')[-1]): 173 | return False 174 | 175 | if os.path.exists(checksum_dir): 176 | open(checksum_dir, 'w').write(download_checksum) 177 | 178 | self.__set_download_progress_percent(100) 179 | 180 | return True 181 | 182 | def get_extract_dir(self, install_dir: str) -> str: 183 | 184 | """ 185 | Return the directory to extract GE-Proton archive based on the current launcher 186 | Return Type: str 187 | """ 188 | 189 | launcher = get_launcher_from_installdir(install_dir) 190 | extract_dir: str = install_dir 191 | 192 | if launcher == Launcher.LUTRIS: 193 | # GE-Proton for Lutris needs to go into 'runners/proton' and not 'runners/wine' 194 | extract_dir = os.path.abspath(os.path.join(install_dir, '../../runners/proton')) 195 | 196 | # Lutris may not be guaranteed to make this path, so ensure it exists 197 | if not os.path.exists(extract_dir): 198 | os.mkdir(extract_dir) 199 | 200 | return extract_dir # Default to install_dir 201 | 202 | def get_info_url(self, version: str) -> str: 203 | """ 204 | Get link with info about version (eg. GitHub release page) 205 | Return Type: str 206 | """ 207 | return self.CT_INFO_URL + version 208 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_boxtron.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Boxtron 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_luxtorpeda import CtInstaller as LuxtorpedaInstaller 8 | 9 | 10 | CT_NAME = 'Boxtron' 11 | CT_LAUNCHERS = ['steam'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_boxtron', '''Steam Play compatibility tool to run DOS games using native Linux DOSBox.''')} 13 | 14 | 15 | class CtInstaller(LuxtorpedaInstaller): 16 | 17 | BUFFER_SIZE = 4096 18 | CT_URL = 'https://api.github.com/repos/dreamer/boxtron/releases' 19 | CT_INFO_URL = 'https://github.com/dreamer/boxtron/releases/tag/' 20 | 21 | def __init__(self, main_window = None): 22 | super().__init__(main_window) 23 | self.extract_dir_name = 'boxtron' 24 | self.deps = [ 'dosbox', 'inotifywait', 'timidity' ] 25 | 26 | def is_system_compatible(self) -> bool: 27 | """ 28 | Are the system requirements met? 29 | Return Type: bool 30 | """ 31 | 32 | return super().is_system_compatible(ct_name = CT_NAME) 33 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_kron4ekvanilla.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Kron4ek Wine-Builds Vanilla 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | import subprocess 6 | 7 | from PySide6.QtCore import QCoreApplication 8 | 9 | from pupgui2.constants import IS_FLATPAK 10 | from pupgui2.util import fetch_project_release_data 11 | 12 | from pupgui2.resources.ctmods.ctmod_00protonge import CtInstaller as GEProtonInstaller 13 | 14 | 15 | CT_NAME = 'Kron4ek Wine-Builds Vanilla' 16 | CT_LAUNCHERS = ['lutris', 'winezgui'] 17 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_kron4ekvanilla', '''Compatibility tool "Wine" to run Windows games on Linux. Official version from the WineHQ sources, compiled by Kron4ek.''')} 18 | 19 | 20 | class CtInstaller(GEProtonInstaller): 21 | 22 | BUFFER_SIZE = 65536 23 | CT_URL = 'https://api.github.com/repos/Kron4ek/Wine-Builds/releases' 24 | CT_INFO_URL = 'https://github.com/Kron4ek/Wine-Builds/releases/tag/' 25 | 26 | def __init__(self, main_window = None) -> None: 27 | 28 | super().__init__(main_window) 29 | 30 | self.release_format = 'tar.xz' 31 | 32 | def __fetch_github_data(self, tag: str) -> dict: 33 | 34 | """ 35 | Fetch GitHub release information 36 | Return Type: dict 37 | Content(s): 38 | 'version', 'date', 'download', 'size' 39 | """ 40 | 41 | asset_condition = lambda asset: 'amd64' in asset.get('name', '') and 'staging' not in asset.get('name', '') 42 | return fetch_project_release_data(self.CT_URL, self.release_format, self.rs, tag=tag, asset_condition=asset_condition) 43 | 44 | def is_system_compatible(self) -> bool: 45 | 46 | """ 47 | Are the system requirements met? 48 | Return Type: bool 49 | """ 50 | 51 | proc_prefix = ['flatpak-spawn', '--host'] if IS_FLATPAK else [] 52 | ldd = subprocess.run(proc_prefix + ['ldd', '--version'], capture_output=True) 53 | ldd_out = ldd.stdout.split(b'\n')[0].split(b' ') 54 | ldd_ver = ldd_out[len(ldd_out) - 1] 55 | ldd_maj = int(ldd_ver.split(b'.')[0]) 56 | ldd_min = int(ldd_ver.split(b'.')[1]) 57 | return False if ldd_maj < 2 else ldd_min >= 27 or ldd_maj != 2 58 | 59 | def get_extract_dir(self, install_dir: str) -> str: 60 | 61 | """ 62 | Return the directory to extract Lutris-Wine archive based on the current launcher 63 | Return Type: str 64 | """ 65 | 66 | # GE-Proton ctmod figures out if it needs to into a different folder 67 | # 68 | # kron4ek can use default 'install_dir' always because it is Wine and not Proton, 69 | # so override to return unmodified 'install_dir' 70 | return install_dir 71 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_lutriswine.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Lutris-Wine 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.util import fetch_project_release_data, ghapi_rlcheck 8 | 9 | from pupgui2.resources.ctmods.ctmod_00protonge import CtInstaller as GEProtonInstaller 10 | 11 | 12 | CT_NAME = 'Lutris-Wine' 13 | CT_LAUNCHERS = ['lutris', 'bottles', 'winezgui'] 14 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_lutriswine', '''Compatibility tool "Wine" to run Windows games on Linux. Improved by Lutris to offer better compatibility or performance in certain games.''')} 15 | 16 | 17 | class CtInstaller(GEProtonInstaller): 18 | 19 | BUFFER_SIZE = 65536 20 | CT_URL = 'https://api.github.com/repos/lutris/wine/releases' 21 | CT_INFO_URL = 'https://github.com/lutris/wine/releases/tag/' 22 | 23 | def __init__(self, main_window = None): 24 | 25 | super().__init__(main_window) 26 | 27 | self.release_format = 'tar.xz' 28 | 29 | def fetch_releases(self, count: int = 100, page: int = 1): 30 | """ 31 | List available releases 32 | Return Type: str[] 33 | """ 34 | tags = [] 35 | for release in ghapi_rlcheck(self.rs.get(f'{self.CT_URL}?per_page={count}&page={page}').json()): 36 | if not 'tag_name' in release: 37 | continue 38 | 39 | tags.append(release['tag_name']) 40 | if 'assets' not in release or len(release['assets']) <= 0: 41 | continue 42 | 43 | if any('lutris-fshack' in asset['name'] for asset in release['assets']): 44 | tags.append(release['tag_name'].replace('lutris-', 'lutris-fshack-')) 45 | 46 | return tags 47 | 48 | def __fetch_github_data(self, tag: str, is_fshack: bool): 49 | 50 | """ 51 | Fetch GitHub release information 52 | Return Type: dict 53 | Content(s): 54 | 'version', 'date', 'download', 'size', 'checksum' 55 | """ 56 | 57 | asset_condition = None 58 | if is_fshack: 59 | asset_condition = lambda asset: 'fshack' in asset['name'] 60 | 61 | return fetch_project_release_data(self.CT_URL, self.release_format, self.rs, tag=tag, asset_condition=asset_condition) 62 | 63 | def __get_data(self, version: str, install_dir: str) -> tuple[dict | None, str | None]: 64 | 65 | """ 66 | Get needed download data and path to extract directory. 67 | Return Type: tuple[dict | None, str | None] 68 | """ 69 | 70 | is_fshack = 'fshack-' in version 71 | if is_fshack: 72 | version = version.replace('fshack-', '') 73 | 74 | data = self.__fetch_github_data(version, is_fshack) 75 | if not data or 'download' not in data: 76 | return (None, None) 77 | 78 | # Overwrite the Proton installation directory as the format we need for Lutris Wine 79 | protondir = f'{install_dir}wine-{data["version"].lower()}-x86_64' 80 | 81 | return (data, protondir) 82 | 83 | def get_info_url(self, version: str) -> str: 84 | 85 | """ 86 | Get link with info about version (eg. GitHub release page) 87 | Return Type: str 88 | """ 89 | 90 | return super().get_info_url(version.replace('fshack-', '')) 91 | 92 | def get_extract_dir(self, install_dir: str) -> str: 93 | 94 | """ 95 | Return the directory to extract Lutris-Wine archive based on the current launcher 96 | Return Type: str 97 | """ 98 | 99 | # GE-Proton ctmod figures out if it needs to into a different folder 100 | # 101 | # Lutris-Wine can use default 'install_dir' always because it is Wine and not Proton, 102 | # so override to return unmodified 'install_dir' 103 | return install_dir 104 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_luxtorpeda.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Luxtorpeda 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | import os 6 | import requests 7 | 8 | from PySide6.QtCore import QObject, QCoreApplication, Signal, Property 9 | from PySide6.QtWidgets import QMessageBox 10 | 11 | from pupgui2.networkutil import download_file 12 | from pupgui2.util import extract_tar, write_tool_version, fetch_project_releases 13 | from pupgui2.util import fetch_project_release_data, build_headers_with_authorization 14 | from pupgui2.util import create_missing_dependencies_message 15 | 16 | 17 | CT_NAME = 'Luxtorpeda' 18 | CT_LAUNCHERS = ['steam'] 19 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_luxtorpeda', '''Luxtorpeda provides Linux-native game engines for specific Windows-only games.''')} 20 | 21 | 22 | class CtInstaller(QObject): 23 | 24 | BUFFER_SIZE = 65536 25 | CT_URL = 'https://api.github.com/repos/luxtorpeda-dev/luxtorpeda/releases' 26 | CT_INFO_URL = 'https://github.com/luxtorpeda-dev/luxtorpeda/releases/tag/' 27 | 28 | p_download_progress_percent = 0 29 | download_progress_percent = Signal(int) 30 | message_box_message = Signal((str, str, QMessageBox.Icon)) 31 | 32 | def __init__(self, main_window = None): 33 | super(CtInstaller, self).__init__() 34 | self.p_download_canceled = False 35 | 36 | # Allows override for Boxtron/Roberta 37 | self.extract_dir_name = 'luxtorpeda' 38 | self.deps = [] 39 | self.release_format = 'tar.xz' 40 | 41 | self.rs = requests.Session() 42 | rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'github') 43 | self.rs.headers.update(rs_headers) 44 | 45 | def get_download_canceled(self): 46 | return self.p_download_canceled 47 | 48 | def set_download_canceled(self, val): 49 | self.p_download_canceled = val 50 | 51 | download_canceled = Property(bool, get_download_canceled, set_download_canceled) 52 | 53 | def __set_download_progress_percent(self, value : int): 54 | if self.p_download_progress_percent == value: 55 | return 56 | self.p_download_progress_percent = value 57 | self.download_progress_percent.emit(value) 58 | 59 | def __download(self, url: str, destination: str, known_size: int = 0): 60 | """ 61 | Download files from url to destination 62 | Return Type: bool 63 | """ 64 | 65 | try: 66 | return download_file( 67 | url=url, 68 | destination=destination, 69 | progress_callback=self.__set_download_progress_percent, 70 | download_cancelled=self.download_canceled, 71 | buffer_size=self.BUFFER_SIZE, 72 | stream=True, 73 | known_size=known_size 74 | ) 75 | except Exception as e: 76 | print(f"Failed to download tool {CT_NAME} - Reason: {e}") 77 | 78 | self.message_box_message.emit( 79 | self.tr("Download Error!"), 80 | self.tr("Failed to download tool '{CT_NAME}'!\n\nReason: {EXCEPTION}".format(CT_NAME=CT_NAME, EXCEPTION=e)), 81 | QMessageBox.Icon.Warning 82 | ) 83 | 84 | def __fetch_github_data(self, tag): 85 | """ 86 | Fetch GitHub release information 87 | Return Type: dict 88 | Content(s): 89 | 'version', 'date', 'download', 'size' 90 | """ 91 | 92 | return fetch_project_release_data(self.CT_URL, self.release_format, self.rs, tag=tag) 93 | 94 | def is_system_compatible(self, ct_name: str = CT_NAME) -> bool: 95 | """ 96 | Are the system requirements met? 97 | Return Type: bool 98 | """ 99 | 100 | if not self.deps: 101 | return True # Skip check if we have no dependencies 102 | 103 | # Emit warning if we generated a missing dependencies message 104 | msg_tr_title = self.tr('Missing dependencies!') 105 | msg, success = create_missing_dependencies_message(ct_name, self.deps) 106 | if not success: 107 | self.message_box_message.emit(msg_tr_title, msg, QMessageBox.Warning) 108 | 109 | return True # install Boxtron anyway 110 | 111 | 112 | def fetch_releases(self, count=100, page=1): 113 | """ 114 | List available releases 115 | Return Type: str[] 116 | """ 117 | 118 | return fetch_project_releases(self.CT_URL, self.rs, count=count, page=page) 119 | 120 | def get_tool(self, version, install_dir, temp_dir): 121 | """ 122 | Download and install the compatibility tool 123 | Return Type: bool 124 | """ 125 | data = self.__fetch_github_data(version) 126 | 127 | if not data or 'download' not in data: 128 | return False 129 | 130 | luxtorpeda_tar = os.path.join(temp_dir, data['download'].split('/')[-1]) 131 | if not self.__download(url=data['download'], destination=luxtorpeda_tar, known_size=data.get('size', 0)): 132 | return False 133 | 134 | luxtorpeda_dir = os.path.join(install_dir, self.extract_dir_name) 135 | if not extract_tar(luxtorpeda_tar, install_dir, mode='xz'): 136 | return False 137 | write_tool_version(luxtorpeda_dir, version) 138 | 139 | self.__set_download_progress_percent(100) 140 | 141 | return True 142 | 143 | def get_info_url(self, version): 144 | """ 145 | Get link with info about version (eg. GitHub release page) 146 | Return Type: str 147 | """ 148 | return self.CT_INFO_URL + version 149 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_northstarproton.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # cyrv6737's NorthstarProton for TitanFall 2 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.util import fetch_project_release_data 8 | 9 | from pupgui2.resources.ctmods.ctmod_00protonge import CtInstaller as GEProtonInstaller 10 | 11 | 12 | CT_NAME = 'Northstar Proton (Titanfall 2)' 13 | CT_LAUNCHERS = ['steam', 'heroicproton', 'bottles', 'advmode'] 14 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_northstarproton', '''Proton build based on TKG's proton-tkg to run the Northstar client + TitanFall 2. By cyrv6737.

Read the following before proceeding:
https://github.com/R2NorthstarTools/NorthstarProton''')} 15 | 16 | 17 | class CtInstaller(GEProtonInstaller): 18 | 19 | BUFFER_SIZE = 65536 20 | CT_URL = 'https://api.github.com/repos/R2NorthstarTools/NorthstarProton/releases' 21 | CT_INFO_URL = 'https://github.com/R2NorthstarTools/NorthstarProton/releases/tag/' 22 | 23 | def __init__(self, main_window = None) -> None: 24 | 25 | super().__init__(main_window) 26 | 27 | self.release_format: str = 'tar.gz' 28 | 29 | def __fetch_github_data(self, tag: str) -> dict: 30 | """ 31 | Fetch GitHub release information 32 | Return Type: dict 33 | Content(s): 34 | 'version', 'date', 'download', 'size' 35 | """ 36 | 37 | return fetch_project_release_data(self.CT_URL, self.release_format, self.rs, tag=tag) 38 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_protoncachyos.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Proton-CachyOS 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | import os 6 | 7 | from PySide6.QtCore import QCoreApplication 8 | 9 | from pupgui2.util import ghapi_rlcheck, extract_tar 10 | from .ctmod_00protonge import CtInstaller as ProtonGECtInstaller 11 | 12 | CT_NAME = 'Proton-CachyOS' 13 | CT_LAUNCHERS = ['steam', 'heroicproton', 'bottles'] 14 | CT_DESCRIPTION = { 15 | 'en': QCoreApplication.instance().translate( 16 | 'ctmod_protoncachyos', 17 | ''' 18 | Steam compatibility tool from the CachyOS Linux distribution for running Windows games 19 | with improvements over Valve's default Proton. Choose the one corresponding to your CPU. 20 |

21 | * x86_64: Works on any x64_64 CPU 22 |
23 | * x86_64_v3: For CPUs that support AVX2 and up 24 | ''' 25 | ) 26 | } 27 | 28 | 29 | class CtInstaller(ProtonGECtInstaller): 30 | 31 | BUFFER_SIZE = 65536 32 | CT_URL = 'https://api.github.com/repos/CachyOS/proton-cachyos/releases' 33 | CT_INFO_URL = 'https://github.com/CachyOS/proton-cachyos/releases/tag/' 34 | 35 | def __init__(self, main_window = None) -> None: 36 | 37 | super().__init__(main_window) 38 | 39 | self.release_format = 'tar.xz' 40 | 41 | def __fetch_github_data(self, tag: str, arch: str) -> dict | None: 42 | """ 43 | Fetch GitHub release information 44 | Return Type: dict 45 | Content(s): 46 | 'version', 'date', 'download', 'size', 'checksum' 47 | """ 48 | url = self.CT_URL + (f'/tags/{tag}' if tag else '/latest') 49 | data = self.rs.get(url).json() 50 | if 'tag_name' not in data: 51 | return None 52 | 53 | values = {'version': data['tag_name'], 'date': data['published_at'].split('T')[0]} 54 | for asset in data['assets']: 55 | if asset['name'].endswith(f'{arch}.sha512sum'): 56 | values['checksum'] = asset['browser_download_url'] 57 | elif asset['name'].endswith(f'{arch}.tar.xz'): 58 | values['download'] = asset['browser_download_url'] 59 | values['size'] = asset['size'] 60 | return values 61 | 62 | def get_hwcaps(self) -> set[str]: 63 | hwcaps = {'x86_64'} 64 | # flags according to https://gitlab.com/x86-psABIs/x86-64-ABI/-/blob/master/x86-64-ABI/low-level-sys-info.tex 65 | flags_v2 = {'sse4_1', 'sse4_2', 'ssse3'} 66 | flags_v3 = {*flags_v2, 'avx', 'avx2'} 67 | flags_v4 = {*flags_v3, 'avx512f', 'avx512bw', 'avx512cd', 'avx512dq', 'avx512vl'} 68 | with open('/proc/cpuinfo', 'r') as cpuinfo: 69 | for line in cpuinfo: 70 | if line.startswith('flags'): 71 | flags = line.split(":")[1].strip().split() 72 | flags = set(flags) 73 | if flags_v4.issubset(flags): 74 | hwcaps.add('x86_64_v4') 75 | if flags_v3.issubset(flags): 76 | hwcaps.add('x86_64_v3') 77 | if flags_v2.issubset(flags): 78 | hwcaps.add('x86_64_v2') 79 | return hwcaps 80 | 81 | def fetch_releases(self, count: int = 100, page: int = 1) -> list: 82 | """ 83 | List available releases 84 | Return Type: str[] 85 | """ 86 | hwcaps = self.get_hwcaps() 87 | assets = [] 88 | releases = ghapi_rlcheck(self.rs.get(f'{self.CT_URL}?per_page={count}&page={page}').json()) 89 | for release in releases: 90 | for asset in release['assets']: 91 | name = asset["name"] 92 | if name.endswith(".tar.xz"): 93 | name = name.strip(".tar.xz") 94 | _, _, major, minor, _, arch = name.split("-") 95 | name = "-".join((major, minor, arch)) 96 | if arch in hwcaps and name not in assets: 97 | assets.append(name) 98 | return assets 99 | 100 | def __get_data(self, version: str, install_dir: str) -> tuple[dict | None, str | None]: 101 | 102 | """ 103 | Get needed download data and path to extract directory. 104 | Return Type: tuple[dict | None, str | None] 105 | """ 106 | 107 | major, minor, arch = version.split("-") 108 | tag = "-".join(('cachyos', major, minor, 'slr')) 109 | data = self.__fetch_github_data(tag, arch) 110 | 111 | if not data or 'download' not in data: 112 | return (None, None) 113 | 114 | protondir = os.path.join(install_dir, data['version']) 115 | 116 | return (data, protondir) 117 | 118 | def get_info_url(self, version: str) -> str: 119 | """ 120 | Get link with info about version (eg. GitHub release page) 121 | Return Type: str 122 | """ 123 | major, minor, arch = version.split("-") 124 | tag = "-".join(('cachyos', major, minor, 'slr')) 125 | return self.CT_INFO_URL + tag 126 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_protonsarek.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # pythonlover02's Proton-Sarek 3 | # Copyright (C) 2025 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from typing import Callable 6 | 7 | 8 | from PySide6.QtCore import QCoreApplication 9 | 10 | from pupgui2.util import fetch_project_release_data, fetch_project_releases 11 | 12 | from pupgui2.resources.ctmods.ctmod_00protonge import CtInstaller as GEProtonInstaller 13 | 14 | 15 | CT_NAME = 'Proton-Sarek' 16 | CT_LAUNCHERS = ['steam', 'lutris', 'heroicproton', 'bottles', 'advmode'] 17 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_protonsarek', '''A custom Proton build with DXVK-Sarek for users with GPUs that support Vulkan 1.1+ but not Vulkan 1.3, or for those with non-Vulkan support who want a plug-and-play option featuring personal patches.''')} 18 | 19 | 20 | class CtInstaller(GEProtonInstaller): 21 | 22 | BUFFER_SIZE = 65536 23 | CT_URL = 'https://api.github.com/repos/pythonlover02/Proton-Sarek/releases' 24 | CT_INFO_URL = 'https://github.com/pythonlover02/Proton-Sarek/releases/tag/' 25 | 26 | def __init__(self, main_window = None) -> None: 27 | 28 | super().__init__(main_window) 29 | 30 | self.release_format: str = 'tar.gz' 31 | self.async_suffix = '-async' 32 | 33 | def fetch_releases(self, count: int = 100, page: int = 1) -> list[str]: 34 | 35 | """ 36 | List available releases 37 | Return Type: str[] 38 | """ 39 | 40 | include_extra_asset: Callable[..., list[str]] = lambda release: [str(release.get('tag_name', '')) + self.async_suffix for asset in release.get('assets', {}) if self.async_suffix in asset.get('name', '')] 41 | return fetch_project_releases(self.CT_URL, self.rs, count=count, page=page, include_extra_asset=include_extra_asset) 42 | 43 | def __fetch_github_data(self, tag: str) -> dict: 44 | 45 | """ 46 | Fetch GitHub release information 47 | Return Type: dict 48 | Content(s): 49 | 'version', 'date', 'download', 'size' 50 | """ 51 | 52 | # Exclude async builds by default -- Maybe 'get_download_url_from_asset' needs to be stricter? 53 | asset_condition = lambda asset: self.async_suffix in asset['name'] if self.async_suffix in tag else self.async_suffix not in asset['name'] 54 | return fetch_project_release_data(self.CT_URL, self.release_format, self.rs, tag=tag.replace(self.async_suffix, ''), asset_condition=asset_condition) 55 | 56 | def get_info_url(self, version: str) -> str: 57 | 58 | """ 59 | Get link with info about version (eg. GitHub release page) 60 | Return Type: str 61 | """ 62 | 63 | return super().get_info_url(version.replace(self.async_suffix, '')) 64 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_protontkg_ntsync.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Proton-Tkg https://github.com/Frogging-Family/wine-tkg-git 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_protontkg import CtInstaller as TKGCtInstaller # Use ProtonTKg Ctmod as base 8 | 9 | 10 | CT_NAME = 'Proton Tkg (Wine Master NTSYNC)' 11 | CT_LAUNCHERS = ['steam', 'heroicproton', 'advmode'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_protontkg_winemaster_ntsync', '''Custom Proton build for running Windows games, built with the Wine-tkg build system. 13 |
14 |
15 | This build is based on Wine Master and includes the NTSYNC patches (Requires a Kernel with NTSYNC support).''')} 16 | 17 | 18 | class CtInstaller(TKGCtInstaller): 19 | 20 | PROTON_PACKAGE_NAME = 'proton-arch-ntsync-nopackage.yml' 21 | 22 | def __init__(self, main_window = None): 23 | super().__init__(main_window) 24 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_protontkg_winemaster.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Proton-Tkg https://github.com/Frogging-Family/wine-tkg-git 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_protontkg import CtInstaller as TKGCtInstaller # Use ProtonTKg Ctmod as base 8 | 9 | 10 | CT_NAME = 'Proton Tkg (Wine Master)' 11 | CT_LAUNCHERS = ['steam', 'heroicproton', 'advmode'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_protontkg_winemaster', '''Custom Proton build for running Windows games, built with the Wine-tkg build system. 13 |
14 |
15 | This build is based on Wine Master.''')} 16 | 17 | 18 | class CtInstaller(TKGCtInstaller): 19 | 20 | PROTON_PACKAGE_NAME = 'proton-arch-nopackage.yml' 21 | 22 | def __init__(self, main_window = None): 23 | super().__init__(main_window) 24 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_roberta.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Roberta 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_luxtorpeda import CtInstaller as LuxtorpedaInstaller 8 | 9 | 10 | CT_NAME = 'Roberta' 11 | CT_LAUNCHERS = ['steam'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_roberta', '''Steam Play compatibility tool to run adventure games using native Linux ScummVM.''')} 13 | 14 | 15 | class CtInstaller(LuxtorpedaInstaller): 16 | 17 | BUFFER_SIZE = 4096 18 | CT_URL = 'https://api.github.com/repos/dreamer/roberta/releases' 19 | CT_INFO_URL = 'https://github.com/dreamer/roberta/releases/tag/' 20 | 21 | def __init__(self, main_window = None): 22 | super().__init__(main_window) 23 | self.extract_dir_name = 'roberta' 24 | self.deps = [ 'scummvm', 'inotifywait' ] 25 | 26 | def is_system_compatible(self) -> bool: 27 | """ 28 | Are the system requirements met? 29 | Return Type: bool 30 | """ 31 | 32 | return super().is_system_compatible(ct_name = CT_NAME) 33 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_rtspgeproton.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # SpookySkeleton's RTSP-GE-Proton 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_00protonge import CtInstaller as GEProtonInstaller 8 | 9 | 10 | CT_NAME = 'RTSP Proton' 11 | CT_LAUNCHERS = ['steam', 'advmode'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_rtspgeproton', '''Fork of GE-Proton with enhanced Windows Media Foundation support.''')} 13 | 14 | 15 | class CtInstaller(GEProtonInstaller): 16 | 17 | BUFFER_SIZE = 4096 18 | CT_URL = 'https://api.github.com/repos/SpookySkeletons/proton-ge-rtsp/releases' 19 | CT_INFO_URL = 'https://github.com/SpookySkeletons/proton-ge-rtsp/releases/tag/' 20 | 21 | def __init__(self, main_window = None): 22 | super().__init__(main_window) 23 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_steamplaynone.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Steam-Play-None https://github.com/Scrumplex/Steam-Play-None 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | import os 6 | import requests 7 | 8 | from PySide6.QtWidgets import QMessageBox 9 | from PySide6.QtCore import QObject, QCoreApplication, Signal, Property 10 | 11 | from pupgui2.util import extract_tar, remove_if_exists 12 | from pupgui2.util import build_headers_with_authorization 13 | from pupgui2.networkutil import download_file 14 | 15 | 16 | CT_NAME = 'Steam-Play-None' 17 | CT_LAUNCHERS = ['steam', 'advmode'] 18 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_steamplaynone', '''Run Linux games as is, even if Valve recommends Proton for a game.
Created by Scrumplex.

Useful for Steam Deck.

Note: The internal name has been changed from none to Steam-Play-None!''')} 19 | 20 | 21 | class CtInstaller(QObject): 22 | 23 | CT_URL = 'https://github.com/Scrumplex/Steam-Play-None/archive/refs/heads/main.tar.gz' # no releases 24 | CT_INFO_URL = 'https://github.com/Scrumplex/Steam-Play-None' 25 | 26 | p_download_progress_percent = 0 27 | download_progress_percent = Signal(int) 28 | message_box_message = Signal((str, str, QMessageBox.Icon)) 29 | 30 | def __init__(self, main_window = None): 31 | super(CtInstaller, self).__init__() 32 | self.p_download_canceled = False 33 | 34 | self.rs = requests.Session() 35 | rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'github') 36 | self.rs.headers.update(rs_headers) 37 | 38 | def get_download_canceled(self): 39 | return self.p_download_canceled 40 | 41 | def set_download_canceled(self, val): 42 | self.p_download_canceled = val 43 | 44 | download_canceled = Property(bool, get_download_canceled, set_download_canceled) 45 | 46 | def __set_download_progress_percent(self, value : int): 47 | if self.p_download_progress_percent == value: 48 | return 49 | self.p_download_progress_percent = value 50 | self.download_progress_percent.emit(value) 51 | 52 | def __download(self, url: str, destination: str) -> bool: 53 | """ 54 | Download files from url to destination 55 | Return Type: bool 56 | """ 57 | 58 | try: 59 | return download_file( 60 | url=url, 61 | destination=os.path.expanduser(destination), 62 | progress_callback=self.__set_download_progress_percent, 63 | download_cancelled=self.download_canceled, 64 | ) 65 | except Exception as e: 66 | print(f"Failed to download tool {CT_NAME} - Reason: {e}") 67 | 68 | self.message_box_message.emit( 69 | self.tr("Download Error!"), 70 | self.tr("Failed to download tool '{CT_NAME}'!\n\nReason: {EXCEPTION}".format(CT_NAME=CT_NAME, EXCEPTION=e)), 71 | QMessageBox.Icon.Warning 72 | ) 73 | 74 | return False 75 | 76 | def is_system_compatible(self): 77 | """ 78 | Are the system requirements met? 79 | Return Type: bool 80 | """ 81 | return True 82 | 83 | def fetch_releases(self, count=100, page=1): 84 | """ 85 | List available releases 86 | Return Type: str[] 87 | """ 88 | return ['main'] 89 | 90 | def get_tool(self, version, install_dir, temp_dir): 91 | """ 92 | Download and install the compatibility tool 93 | Return Type: bool 94 | """ 95 | steam_play_none_tar = os.path.join(temp_dir, 'main.tar.gz') 96 | 97 | # Rename extracted Steam-Play-None-main to Steam-Play-None 98 | steam_play_none_main = os.path.join(install_dir, 'Steam-Play-None-main') 99 | steam_play_none_dir = os.path.join(install_dir, 'Steam-Play-None') 100 | 101 | dl_url = self.CT_URL 102 | 103 | remove_if_exists(steam_play_none_main) 104 | if not self.__download(url=dl_url, destination=steam_play_none_tar): 105 | return False 106 | 107 | remove_if_exists(steam_play_none_dir) 108 | if not extract_tar(steam_play_none_tar, install_dir, mode='gz'): 109 | return False 110 | 111 | os.rename(steam_play_none_main, steam_play_none_dir) 112 | 113 | self.__set_download_progress_percent(100) 114 | 115 | return True 116 | 117 | def get_info_url(self, version): 118 | """ 119 | Get link with info about version (eg. GitHub release page) 120 | Return Type: str 121 | """ 122 | return self.CT_INFO_URL 123 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_steamtinkerlaunch_git.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # SteamTinkerLaunch-git 3 | # Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_steamtinkerlaunch import CtInstaller as stlCtInstaller 8 | 9 | 10 | CT_NAME = 'SteamTinkerLaunch-git' 11 | CT_LAUNCHERS = ['steam', 'advmode', 'native-only'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_steamtinkerlaunch_git', ''' 13 | Git release - May be unstable 14 |

15 | Linux wrapper tool for use with the Steam client which allows for easy graphical configuration of game tools for Proton and native Linux games. 16 |

17 | On Steam Deck, relevant dependencies will be installed for you. If you are not on Steam Deck, ensure you have the following dependencies installed: 18 | 32 | More information is available on the SteamTinkerLaunch Installation wiki page. 33 |

34 | SteamTinkerLaunch has a number of Optional Dependencies which have to be installed separately for extra functionality. Please see the Optional Dependencies section 35 | of the SteamTinkerLaunch Installation guide on its GitHub page.''')} 36 | 37 | 38 | class CtInstaller(stlCtInstaller): 39 | 40 | def __init__(self, main_window = None): 41 | super().__init__(main_window, allow_git=True) 42 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_vkd3dlutris.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # vkd3d-lutris for Lutris: https://github.com/lutris/vkd3d/ 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_vkd3dproton import CtInstaller as VKD3DInstaller 8 | 9 | 10 | CT_NAME = 'vkd3d-lutris' 11 | CT_LAUNCHERS = ['lutris', 'heroicwine', 'heroicproton'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_vkd3d-lutris', '''Fork of Wine's VKD3D which aims to implement the full Direct3D 12 API on top of Vulkan (Lutris Release).

https://github.com/lutris/docs/blob/master/HowToDXVK.md''')} 13 | 14 | class CtInstaller(VKD3DInstaller): 15 | 16 | CT_URL = 'https://api.github.com/repos/lutris/vkd3d/releases' 17 | CT_INFO_URL = 'https://github.com/lutris/vkd3d/releases/tag/' 18 | 19 | def __init__(self, main_window = None): 20 | super().__init__(main_window) 21 | self.release_format = 'tar.xz' 22 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_vkd3dproton.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # vkd3d-proton and vkd3d for Lutris: https://github.com/HansKristian-Work/vkd3d-proton/ 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | import os 6 | import requests 7 | 8 | from PySide6.QtCore import QObject, QCoreApplication, Signal, Property 9 | from PySide6.QtWidgets import QMessageBox 10 | 11 | 12 | from pupgui2.datastructures import Launcher 13 | from pupgui2.networkutil import download_file 14 | from pupgui2.util import extract_tar, extract_tar_zst, get_launcher_from_installdir 15 | from pupgui2.util import build_headers_with_authorization, fetch_project_release_data, fetch_project_releases 16 | 17 | 18 | CT_NAME = 'vkd3d-proton' 19 | CT_LAUNCHERS = ['lutris', 'heroicwine', 'heroicproton'] 20 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_vkd3d-proton', '''Fork of Wine's VKD3D which aims to implement the full Direct3D 12 API on top of Vulkan (Valve Release).

https://github.com/lutris/docs/blob/master/HowToDXVK.md''')} 21 | 22 | class CtInstaller(QObject): 23 | 24 | BUFFER_SIZE = 65536 25 | CT_URL = 'https://api.github.com/repos/HansKristian-Work/vkd3d-proton/releases' 26 | CT_INFO_URL = 'https://github.com/HansKristian-Work/vkd3d-proton/releases/tag/' 27 | 28 | p_download_progress_percent = 0 29 | download_progress_percent = Signal(int) 30 | message_box_message = Signal((str, str, QMessageBox.Icon)) 31 | 32 | def __init__(self, main_window = None): 33 | super(CtInstaller, self).__init__() 34 | self.p_download_canceled = False 35 | self.release_format = 'tar.zst' 36 | 37 | self.rs = requests.Session() 38 | rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'github') 39 | self.rs.headers.update(rs_headers) 40 | 41 | def get_download_canceled(self): 42 | return self.p_download_canceled 43 | 44 | def set_download_canceled(self, val): 45 | self.p_download_canceled = val 46 | 47 | download_canceled = Property(bool, get_download_canceled, set_download_canceled) 48 | 49 | def __set_download_progress_percent(self, value : int): 50 | if self.p_download_progress_percent == value: 51 | return 52 | self.p_download_progress_percent = value 53 | self.download_progress_percent.emit(value) 54 | 55 | def __download(self, url: str, destination: str) -> bool: 56 | """ 57 | Download files from url to destination 58 | Return Type: bool 59 | """ 60 | 61 | try: 62 | return download_file( 63 | url=url, 64 | destination=os.path.expanduser(destination), 65 | progress_callback=self.__set_download_progress_percent, 66 | download_cancelled=self.download_canceled, 67 | ) 68 | except Exception as e: 69 | print(f"Failed to download tool {CT_NAME} - Reason: {e}") 70 | 71 | self.message_box_message.emit( 72 | self.tr("Download Error!"), 73 | self.tr("Failed to download tool '{CT_NAME}'!\n\nReason: {EXCEPTION}".format(CT_NAME=CT_NAME, EXCEPTION=e)), 74 | QMessageBox.Icon.Warning 75 | ) 76 | 77 | return False 78 | 79 | def __fetch_github_data(self, tag): 80 | """ 81 | Fetch GitHub release information 82 | Return Type: dict 83 | Content(s): 84 | 'version', 'date', 'download', 'size', 'checksum' 85 | """ 86 | 87 | return fetch_project_release_data(self.CT_URL, self.release_format, self.rs, tag=tag) 88 | 89 | def is_system_compatible(self): 90 | """ 91 | Are the system requirements met? 92 | Return Type: bool 93 | """ 94 | return True 95 | 96 | def fetch_releases(self, count=100, page=1): 97 | """ 98 | List available releases 99 | Return Type: str[] 100 | """ 101 | 102 | return fetch_project_releases(self.CT_URL, self.rs, count=count, page=page) 103 | 104 | def get_tool(self, version, install_dir, temp_dir): 105 | """ 106 | Download and install the compatibility tool 107 | Return Type: bool 108 | """ 109 | data = self.__fetch_github_data(version) 110 | 111 | if not data or 'download' not in data: 112 | return False 113 | 114 | vkd3d_archive = os.path.join(temp_dir, data['download'].split('/')[-1]) # e.g. /tmp/[...]/vkd3d-proton-2.7.tar.zst 115 | if not self.__download(url=data['download'], destination=vkd3d_archive): 116 | return False 117 | 118 | vkd3d_dir = self.get_extract_dir(install_dir) 119 | 120 | has_extract_tar_zst = vkd3d_archive.endswith('.tar.zst') and extract_tar_zst(vkd3d_archive, vkd3d_dir) 121 | has_extract_tar_xz = vkd3d_archive.endswith('.tar.xz') and extract_tar(vkd3d_archive, vkd3d_dir, mode='xz') 122 | 123 | if not has_extract_tar_zst and not has_extract_tar_xz: 124 | return False 125 | 126 | self.__set_download_progress_percent(100) 127 | 128 | return True 129 | 130 | def get_info_url(self, version): 131 | """ 132 | Get link with info about version (eg. GitHub release page) 133 | Return Type: str 134 | """ 135 | return self.CT_INFO_URL + version 136 | 137 | def get_extract_dir(self, install_dir: str) -> str: 138 | """ 139 | Return the directory to extract vkd3d archive based on the current launcher 140 | Return Type: str 141 | """ 142 | 143 | launcher = get_launcher_from_installdir(install_dir) 144 | if launcher == Launcher.LUTRIS: 145 | return os.path.abspath(os.path.join(install_dir, '../../runtime/vkd3d')) 146 | if launcher == Launcher.HEROIC: 147 | return os.path.abspath(os.path.join(install_dir, '../vkd3d')) 148 | else: 149 | return install_dir # Default to install_dir 150 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_winetkg_valve_otherdistro.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Proton-Tkg https://github.com/Frogging-Family/wine-tkg-git 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_protontkg import CtInstaller as TKGCtInstaller # Use ProtonTKg Ctmod as base 8 | 9 | 10 | CT_NAME = 'Wine Tkg (Valve Wine Bleeding Edge)' 11 | CT_LAUNCHERS = ['lutris', 'heroicwine', 'winezgui'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_winetkg_valve_otherdistro', '''Custom Wine build for running Windows games, built with the Wine-tkg build system based on Valve Wine bleeding_edge.''')} 13 | 14 | 15 | class CtInstaller(TKGCtInstaller): 16 | 17 | PROTON_PACKAGE_NAME = 'wine-valvexbe' 18 | 19 | def __init__(self, main_window = None): 20 | super().__init__(main_window) 21 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_winetkg_winemaster.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # Proton-Tkg https://github.com/Frogging-Family/wine-tkg-git 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_protontkg import CtInstaller as TKGCtInstaller # Use ProtonTKg Ctmod as base 8 | 9 | 10 | CT_NAME = 'Wine Tkg (Wine Master)' 11 | CT_LAUNCHERS = ['lutris', 'heroicwine', 'winezgui', 'advmode'] 12 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_winetkg_vanilla_ubuntu', '''Custom Wine build for running Windows games, built with the Wine-tkg build system (Ubuntu CI) based on Wine Master.''')} 13 | 14 | 15 | class CtInstaller(TKGCtInstaller): 16 | 17 | PROTON_PACKAGE_NAME = 'wine-ubuntu.yml' 18 | 19 | def __init__(self, main_window = None): 20 | super().__init__(main_window) 21 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_z0dxvk.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # DXVK for Lutris: https://github.com/doitsujin/dxvk/ 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | import os 6 | from PySide6.QtWidgets import QMessageBox 7 | import requests 8 | 9 | from PySide6.QtCore import QObject, QCoreApplication, Signal, Property 10 | 11 | from pupgui2.networkutil import download_file 12 | from pupgui2.util import extract_tar, get_launcher_from_installdir, fetch_project_releases 13 | from pupgui2.util import fetch_project_release_data, build_headers_with_authorization 14 | from pupgui2.datastructures import Launcher 15 | 16 | 17 | CT_NAME = 'DXVK' 18 | CT_LAUNCHERS = ['lutris', 'heroicwine', 'heroicproton'] 19 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_z0dxvk', '''Vulkan based implementation of Direct3D 8, 9, 10, and 11 for Linux/Wine.

https://github.com/lutris/docs/blob/master/HowToDXVK.md''')} 20 | 21 | 22 | class CtInstaller(QObject): 23 | 24 | BUFFER_SIZE: int = 65536 25 | CT_URL: str = 'https://api.github.com/repos/doitsujin/dxvk/releases' 26 | CT_INFO_URL: str = 'https://github.com/doitsujin/dxvk/releases/tag/' 27 | 28 | p_download_progress_percent: int = 0 29 | download_progress_percent: Signal = Signal(int) 30 | message_box_message: Signal = Signal((str, str, QMessageBox.Icon)) 31 | 32 | def __init__(self, main_window = None): 33 | super(CtInstaller, self).__init__() 34 | self.p_download_canceled: bool = False 35 | self.release_format: str = 'tar.gz' 36 | 37 | self.rs: requests.Session = requests.Session() 38 | rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'github') 39 | self.rs.headers.update(rs_headers) 40 | 41 | def get_download_canceled(self): 42 | return self.p_download_canceled 43 | 44 | def set_download_canceled(self, val): 45 | self.p_download_canceled = val 46 | 47 | download_canceled = Property(bool, get_download_canceled, set_download_canceled) 48 | 49 | def __set_download_progress_percent(self, value : int): 50 | if self.p_download_progress_percent == value: 51 | return 52 | self.p_download_progress_percent = value 53 | self.download_progress_percent.emit(value) 54 | 55 | def __download(self, url: str, destination: str, known_size: int = 0): 56 | """ 57 | Download files from url to destination 58 | Return Type: bool 59 | """ 60 | 61 | try: 62 | return download_file( 63 | url=url, 64 | destination=destination, 65 | progress_callback=self.__set_download_progress_percent, 66 | download_cancelled=self.download_canceled, 67 | buffer_size=self.BUFFER_SIZE, 68 | stream=True, 69 | known_size=known_size 70 | ) 71 | except Exception as e: 72 | print(f"Failed to download tool {CT_NAME} - Reason: {e}") 73 | 74 | self.message_box_message.emit( 75 | self.tr("Download Error!"), 76 | self.tr("Failed to download tool '{CT_NAME}'!\n\nReason: {EXCEPTION}".format(CT_NAME=CT_NAME, EXCEPTION=e)), 77 | QMessageBox.Icon.Warning 78 | ) 79 | 80 | def __fetch_data(self, tag: str = '') -> dict: 81 | """ 82 | Fetch release information 83 | Return Type: dict 84 | Content(s): 85 | 'version', 'date', 'download', 'size' 86 | """ 87 | 88 | asset_condition = lambda asset: 'native' not in [asset.get('name', ''), asset.get('url', '')] # 'name' for github asset, 'url' for gitlab asset 89 | return fetch_project_release_data(self.CT_URL, self.release_format, self.rs, tag=tag, asset_condition=asset_condition) 90 | 91 | def is_system_compatible(self): 92 | """ 93 | Are the system requirements met? 94 | Return Type: bool 95 | """ 96 | return True 97 | 98 | def fetch_releases(self, count: int = 100, page: int = 1): 99 | """ 100 | List available releases 101 | Return Type: list[str] 102 | """ 103 | return fetch_project_releases(self.CT_URL, self.rs, count=count, page=page) 104 | 105 | def __get_data(self, version: str, install_dir: str) -> tuple[dict | None, str | None]: 106 | 107 | """ 108 | Get needed download data and path to extract directory. 109 | Return Type: diple[dict | None, str | None] 110 | """ 111 | 112 | data = self.__fetch_data(version) 113 | if not data or 'download' not in data: 114 | return (None, None) 115 | 116 | dxvk_dir = self.get_extract_dir(install_dir) 117 | 118 | return (data, dxvk_dir) 119 | 120 | def __extract(self, archive_path: str, extract_dir: str) -> bool: 121 | 122 | """ 123 | Extract the tool archive at the given path. 124 | Return Type: bool 125 | """ 126 | 127 | if not archive_path or not extract_dir: 128 | return False 129 | 130 | # DXVK and DXVK Async are 'tar.gz' 131 | tar_type = self.release_format.split('.')[-1] 132 | 133 | return extract_tar(archive_path, extract_dir, mode=tar_type) 134 | 135 | def get_tool(self, version: str, install_dir: str, temp_dir: str) -> bool: 136 | """ 137 | Download and install the compatibility tool 138 | Return Type: bool 139 | """ 140 | 141 | data, dxvk_dir = self.__get_data(version, install_dir) 142 | if not data: 143 | return False 144 | 145 | # Should be updated to support Heroic, like ctmod_d8vk 146 | dxvk_archive: str = os.path.join(temp_dir, data['download'].split('/')[-1]) 147 | if not self.__download(url=data['download'], destination=dxvk_archive, known_size=data.get('size', 0)): 148 | return False 149 | 150 | if not dxvk_dir or not self.__extract(dxvk_archive, dxvk_dir): 151 | return False 152 | 153 | self.__set_download_progress_percent(100) 154 | 155 | return True 156 | 157 | def get_info_url(self, version: str) -> str: 158 | """ 159 | Get link with info about version (eg. GitHub release page) 160 | Return Type: str 161 | """ 162 | return self.CT_INFO_URL + version 163 | 164 | def get_extract_dir(self, install_dir: str) -> str: 165 | """ 166 | Return the directory to extract DXVK archive based on the current launcher 167 | Return Type: str 168 | """ 169 | 170 | launcher = get_launcher_from_installdir(install_dir) 171 | if launcher == Launcher.LUTRIS: 172 | return os.path.abspath(os.path.join(install_dir, '../../runtime/dxvk')) 173 | if launcher == Launcher.HEROIC: 174 | return os.path.abspath(os.path.join(install_dir, '../dxvk')) 175 | else: 176 | return install_dir # Default to install_dir 177 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_z1dxvkasync.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # DXVK with async patch for Lutris: https://github.com/Sporif/dxvk-async/ 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | from PySide6.QtCore import QCoreApplication 6 | 7 | from pupgui2.resources.ctmods.ctmod_z0dxvk import CtInstaller as DXVKInstaller 8 | from pupgui2.util import build_headers_with_authorization 9 | 10 | 11 | CT_NAME = 'DXVK Async' 12 | CT_LAUNCHERS = ['lutris'] 13 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_z1dxvkasync', '''Vulkan based implementation of Direct3D 8, 9, 10, and 11 for Linux/Wine with gplasync patch by Ph42oN.

Warning: Use only with singleplayer games!''')} 14 | 15 | 16 | class CtInstaller(DXVKInstaller): 17 | 18 | CT_URL = 'https://gitlab.com/api/v4/projects/43488626/releases' 19 | CT_INFO_URL = 'https://gitlab.com/Ph42oN/dxvk-gplasync/-/releases/' 20 | 21 | def __init__(self, main_window = None): 22 | super().__init__(main_window) 23 | 24 | rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'gitlab') 25 | self.rs.headers.update(rs_headers) 26 | -------------------------------------------------------------------------------- /pupgui2/resources/ctmods/ctmod_z2dxvknightly.py: -------------------------------------------------------------------------------- 1 | # pupgui2 compatibility tools module 2 | # DXVK for Lutris (nightly version): https://github.com/doitsujin/dxvk/ 3 | # Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup 4 | 5 | import os 6 | 7 | from PySide6.QtCore import QCoreApplication 8 | 9 | from pupgui2.util import extract_zip, ghapi_rlcheck 10 | 11 | from pupgui2.resources.ctmods.ctmod_z0dxvk import CtInstaller as DXVKInstaller 12 | 13 | 14 | CT_NAME = 'DXVK (nightly)' 15 | CT_LAUNCHERS = ['lutris', 'advmode'] 16 | CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_z2dxvknightly', '''Nightly version of DXVK (master branch), a Vulkan based implementation of Direct3D 8, 9, 10 and 11 for Linux/Wine.

Warning: Nightly version is unstable, use with caution!''')} 17 | 18 | 19 | class CtInstaller(DXVKInstaller): 20 | 21 | BUFFER_SIZE: int = 65536 22 | CT_WORKFLOW_URL: str = 'https://api.github.com/repos/doitsujin/dxvk/actions/workflows' 23 | CT_ARTIFACT_URL: str = 'https://api.github.com/repos/doitsujin/dxvk/actions/runs/{}/artifacts' 24 | CT_ALL_ARTIFACTS_URL: str = 'https://api.github.com/repos/doitsujin/dxvk/actions/artifacts' 25 | CT_INFO_URL: str = 'https://github.com/doitsujin/dxvk/commit/' 26 | 27 | DXVK_WORKFLOW_NAME: str = 'artifacts' 28 | 29 | def __init__(self, main_window = None): 30 | 31 | super().__init__(main_window) 32 | 33 | self.release_format: str = 'zip' 34 | 35 | def __fetch_workflows(self, count: int = 30) -> list[str]: 36 | 37 | """ 38 | Get all active, successful runs in the DXVK Linux-compatible workflow. 39 | Return Type: list 40 | """ 41 | 42 | workflow_request_url: str = f'{self.CT_WORKFLOW_URL}?per_page={str(count)}' 43 | workflow_response_json: dict = self.rs.get(workflow_request_url).json() 44 | 45 | tags: list[str] = [] 46 | for workflow in workflow_response_json.get('workflows', {}): 47 | if workflow['state'] != "active" or self.DXVK_WORKFLOW_NAME not in workflow['path']: 48 | continue 49 | 50 | page = 1 51 | while page != -1 and page <= 5: # fetch more (up to 5 pages) if first releases all failed 52 | at_least_one_failed = False # ensure the reason that len(tags)=0 is that releases failed 53 | 54 | workflow_runs_request_url: str = f'{workflow["url"]}/runs?per_page={count}&page={page}' 55 | workflow_runs_response_json: dict = self.rs.get(workflow_runs_request_url).json() 56 | 57 | for run in workflow_runs_response_json.get('workflow_runs', {}): 58 | if run['head_branch'] != 'master': 59 | continue 60 | 61 | if run['conclusion'] == "failure": 62 | at_least_one_failed = True 63 | 64 | continue 65 | 66 | # TODO can make this generic so that i.e. this DXVK Ctmod can use commmit SHAs but Proton-tkg can use workflow IDs? 67 | # then this could be a generic function shared between ctmods and could be in a util file, unit tested, etc 68 | commit_hash: str = str(run['head_commit']['id'][:7]) 69 | tags.append(commit_hash) 70 | 71 | if len(tags) == 0 and at_least_one_failed: 72 | page += 1 73 | 74 | continue 75 | 76 | page = -1 77 | 78 | return tags 79 | 80 | def fetch_releases(self, count: int = 30, page: int = 1) -> list[str]: 81 | 82 | """ 83 | List available releases. 84 | Return Type: str[] 85 | """ 86 | 87 | return self.__fetch_workflows(count=count) 88 | 89 | def __get_artifact_from_commit(self, commit): 90 | 91 | """ 92 | Get artifact from commit 93 | Return Type: str 94 | """ 95 | 96 | for artifact in self.rs.get(f'{self.CT_ALL_ARTIFACTS_URL}?per_page=100').json()["artifacts"]: 97 | # DXVK appends '-msvc-output' to Windows builds 98 | # See: https://github.com/doitsujin/dxvk/blob/20a6fae8a7f60e7719724b229552eba1ae6c3427/.github/workflows/test-build-windows.yml#L80 99 | if artifact['workflow_run']['head_sha'][:len(commit)] == commit and not artifact['name'].endswith('-msvc-output'): 100 | artifact['workflow_run']['head_sha'] = commit 101 | return artifact 102 | 103 | return None 104 | 105 | def __fetch_github_data(self, tag: str): 106 | 107 | """ 108 | Fetch GitHub release information 109 | Return Type: dict 110 | Content(s): 111 | 'version', 'date', 'download', 'size' 112 | """ 113 | 114 | # Tag in this case is the commit hash 115 | data = self.__get_artifact_from_commit(tag) 116 | if not data: 117 | return 118 | values = {'version': data['workflow_run']['head_sha'][:7], 'date': data['updated_at'].split('T')[0]} 119 | values['download'] = f'https://nightly.link/doitsujin/dxvk/actions/runs/{data["workflow_run"]["id"]}/{data["name"]}.zip' 120 | 121 | values['size'] = data['size_in_bytes'] 122 | return values 123 | 124 | def __get_data(self, version: str, install_dir: str) -> tuple[dict | None, str | None]: 125 | 126 | """ 127 | Get needed download data and path to extract directory. 128 | Return Type: diple[dict | None, str | None] 129 | """ 130 | 131 | data = self.__fetch_github_data(version) 132 | if not data or not 'download' in data: 133 | return (None, None) 134 | 135 | # TODO This is hardcoded to Lutris as DXVK Nightly currently doesn't support any other launchers -- Could possibly add support for Heroic in future 136 | dxvk_dir = os.path.join(install_dir, '../../runtime/dxvk', 'dxvk-git-' + data['version']) 137 | 138 | return (data, dxvk_dir) 139 | 140 | def __extract(self, archive_path: str, extract_dir: str) -> bool: 141 | 142 | """ 143 | Extract the tool archive at the given path. 144 | Return Type: bool 145 | """ 146 | 147 | if not archive_path or not extract_dir: 148 | return False 149 | 150 | return extract_zip(archive_path, extract_dir) 151 | 152 | def get_info_url(self, version: str) -> str: 153 | 154 | """ 155 | Get link with info about version (eg. GitHub release page) 156 | Return Type: str 157 | """ 158 | 159 | return self.CT_INFO_URL + version 160 | -------------------------------------------------------------------------------- /pupgui2/resources/i18n/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/__init__.py -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_de.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_de.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_el.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_el.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_es.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_es.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_fi.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_fi.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_fr.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_fr.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_hi.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_hi.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_hu.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_hu.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_it.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_it.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_nb_NO.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_nb_NO.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_nl.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_nl.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_pl.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_pl.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_pt.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_pt.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_pt_BR.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_pt_BR.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_ru.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_ru.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_sv.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_sv.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_ta.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_ta.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_tr.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_tr.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_uk.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_uk.qm -------------------------------------------------------------------------------- /pupgui2/resources/i18n/pupgui2_zh_TW.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/i18n/pupgui2_zh_TW.qm -------------------------------------------------------------------------------- /pupgui2/resources/img/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/__init__.py -------------------------------------------------------------------------------- /pupgui2/resources/img/appicon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/appicon256.png -------------------------------------------------------------------------------- /pupgui2/resources/img/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/arrow_down.png -------------------------------------------------------------------------------- /pupgui2/resources/img/awacy_broken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/awacy_broken.png -------------------------------------------------------------------------------- /pupgui2/resources/img/awacy_denied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/awacy_denied.png -------------------------------------------------------------------------------- /pupgui2/resources/img/awacy_planned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/awacy_planned.png -------------------------------------------------------------------------------- /pupgui2/resources/img/awacy_running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/awacy_running.png -------------------------------------------------------------------------------- /pupgui2/resources/img/awacy_supported.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/awacy_supported.png -------------------------------------------------------------------------------- /pupgui2/resources/img/awacy_unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/awacy_unknown.png -------------------------------------------------------------------------------- /pupgui2/resources/img/kofi_button_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/img/kofi_button_blue.png -------------------------------------------------------------------------------- /pupgui2/resources/themes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/themes/__init__.py -------------------------------------------------------------------------------- /pupgui2/resources/themes/steamdeck.qss: -------------------------------------------------------------------------------- 1 | QWidget { 2 | color: #EEEEEE; 3 | background-color: #171D25; 4 | font-weight: 300; 5 | } 6 | 7 | QPushButton, QListWidget, QComboBox, QToolButton, QLineEdit { 8 | background-color: #282D36; 9 | border: none; 10 | border-radius: 2px; 11 | padding: 8px; 12 | } 13 | 14 | QListWidget::item { 15 | padding: 4px; 16 | } 17 | 18 | QPushButton:hover { 19 | background-color: #464D58; 20 | } 21 | 22 | QPushButton::pressed { 23 | background-color: #393F49; 24 | } 25 | 26 | QPushButton::disabled { 27 | background-color: #1D2026; 28 | } 29 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/pupgui2/resources/ui/__init__.py -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_aboutdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PupguiAboutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 513 10 | 301 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 0 27 | 0 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | <html><head/><body><p>GUI for installing/updating Wine- and Proton-based compatibility tools.<br/>Inspired by/partly based on AUNaseef's protonup.</p></body></html> 41 | 42 | 43 | 44 | 45 | 46 | 47 | ProtonUp-Qt by DavidoTek <...> 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 0 65 | 0 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | Qt::Orientation::Horizontal 85 | 86 | 87 | 88 | 40 89 | 20 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Support development on GitHub 98 | 99 | 100 | 101 | 102 | 103 | 104 | Donate 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Qt::Orientation::Vertical 114 | 115 | 116 | 117 | 20 118 | 40 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | Color Theme: 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 0 137 | 0 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | Enable advanced mode (show git-builds for compatibility tools etc.) 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | Edit Git access tokens 158 | 159 | 160 | 161 | 162 | 163 | 164 | Qt::Orientation::Horizontal 165 | 166 | 167 | 168 | 40 169 | 20 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | Check for updates 183 | 184 | 185 | 186 | 187 | 188 | 189 | Qt::Orientation::Horizontal 190 | 191 | 192 | 193 | 40 194 | 20 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | Adds a shortcut to open ProtonUp-Qt from your Steam library. 203 | To remove the shortcut, open Steam and select "remove non-Steam game from your library". 204 | 205 | 206 | Add Steam shortcut 207 | 208 | 209 | 210 | 211 | 212 | 213 | About Qt 214 | 215 | 216 | 217 | 218 | 219 | 220 | Close 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_ctbatchupdatedialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PuiguiCustomInstallDirectoryDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 350 10 | 160 11 | 12 | 13 | 14 | 15 | 350 16 | 160 17 | 18 | 19 | 20 | Batch Update 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | Migrate games using the current compatibility tool to the one specified below. 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | New Version: 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 0 50 | 0 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Old Version: 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Qt::Vertical 75 | 76 | 77 | 78 | 20 79 | 40 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Qt::Horizontal 90 | 91 | 92 | 93 | 40 94 | 20 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | false 103 | 104 | 105 | Batch Update 106 | 107 | 108 | 109 | 110 | 111 | 112 | Close 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_ctinfodialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PupguiCtInfoDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 340 11 | 12 | 13 | 14 | About compatibility tool 15 | 16 | 17 | 18 | .. 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Compatibility tool: 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 37 | 38 | 39 | 40 | 41 | 42 | 43 | Game Launcher: 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 54 | 55 | 56 | 57 | 58 | 59 | 60 | Install directory: 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Qt::Horizontal 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Games using compatibility tool: 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | Qt::Horizontal 103 | 104 | 105 | 106 | 40 107 | 20 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | Refresh Games 116 | 117 | 118 | 119 | 120 | 121 | 122 | .. 123 | 124 | 125 | 126 | 127 | 128 | 129 | Search games... 130 | 131 | 132 | 133 | 134 | 135 | 136 | .. 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 0 146 | 147 | 148 | 149 | 150 | 0 151 | 152 | 153 | 0 154 | 155 | 156 | 0 157 | 158 | 159 | 0 160 | 161 | 162 | 0 163 | 164 | 165 | 166 | 167 | 168 | 0 169 | 0 170 | 171 | 172 | 173 | Qt::ClickFocus 174 | 175 | 176 | QAbstractItemView::NoEditTriggers 177 | 178 | 179 | QAbstractItemView::SelectRows 180 | 181 | 182 | true 183 | 184 | 185 | 0 186 | 187 | 188 | 2 189 | 190 | 191 | true 192 | 193 | 194 | false 195 | 196 | 197 | true 198 | 199 | 200 | false 201 | 202 | 203 | false 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 0 218 | 219 | 220 | 0 221 | 222 | 223 | 0 224 | 225 | 226 | 0 227 | 228 | 229 | 0 230 | 231 | 232 | 233 | 234 | false 235 | 236 | 237 | 238 | 0 239 | 0 240 | 241 | 242 | 243 | 244 | 18 245 | 246 | 247 | 248 | No games 249 | 250 | 251 | Qt::AlignCenter 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | Batch Update 265 | 266 | 267 | 268 | 269 | 270 | 271 | e.g. Half-Life 3 272 | 273 | 274 | Search for a game... 275 | 276 | 277 | 278 | 279 | 280 | 281 | Qt::Horizontal 282 | 283 | 284 | 285 | 40 286 | 20 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | Qt::StrongFocus 295 | 296 | 297 | Close 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | btnClose 307 | btnBatchUpdate 308 | btnSearch 309 | searchBox 310 | btnRefreshGames 311 | 312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_custominstalldirectorydialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PuiguiCustomInstallDirectoryDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 350 10 | 185 11 | 12 | 13 | 14 | 15 | 350 16 | 185 17 | 18 | 19 | 20 | Dialog 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | Specify a custom location for downloading and displaying a launcher's compatibility tools. 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Directory: 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Launcher: 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 0 60 | 0 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Qt::Orientation::Vertical 71 | 72 | 73 | 74 | 20 75 | 40 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | Reset the custom install directory back to default for this launcher 86 | 87 | 88 | Default 89 | 90 | 91 | 92 | 93 | 94 | 95 | Qt::Orientation::Horizontal 96 | 97 | 98 | 99 | 40 100 | 20 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | false 109 | 110 | 111 | Save 112 | 113 | 114 | 115 | 116 | 117 | 118 | Close 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_gamelistdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PupguiGameListDialog 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 800 13 | 325 14 | 15 | 16 | 17 | Game List 18 | 19 | 20 | 21 | 22 | 23 | Qt::NoFocus 24 | 25 | 26 | QAbstractItemView::NoEditTriggers 27 | 28 | 29 | true 30 | 31 | 32 | 5 33 | 34 | 35 | true 36 | 37 | 38 | 160 39 | 40 | 41 | true 42 | 43 | 44 | false 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | QLabel { color: orange; } 59 | 60 | 61 | Warning: Close the Steam client beforehand so that the changes can be applied! 62 | 63 | 64 | 65 | 66 | 67 | 68 | e.g. Team Fortress 2 69 | 70 | 71 | Search for a game... 72 | 73 | 74 | 75 | 76 | 77 | 78 | Qt::Horizontal 79 | 80 | 81 | 82 | 40 83 | 20 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Refresh Games 92 | 93 | 94 | 95 | 96 | 97 | 98 | .. 99 | 100 | 101 | 102 | 103 | 104 | 105 | Shortcut Editor 106 | 107 | 108 | 109 | 110 | 111 | 112 | Search 113 | 114 | 115 | 116 | 117 | 118 | 119 | Apply 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | btnApply 129 | btnSearch 130 | searchBox 131 | btnRefreshGames 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_gitaccesstokendialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | GitAccessTokenDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 200 11 | 12 | 13 | 14 | 15 | 400 16 | 200 17 | 18 | 19 | 20 | Configure Git access tokens 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | GitHub: 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Qt::Orientation::Horizontal 39 | 40 | 41 | 42 | 40 43 | 20 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Save 52 | 53 | 54 | 55 | 56 | 57 | 58 | Close 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | GitLab: 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Qt::Orientation::Vertical 81 | 82 | 83 | 84 | 20 85 | 40 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | This dialog allows you to configure access tokens for the GitHub/GitLab API to prevent the API rate limit warning. 94 | 95 | 96 | true 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_installdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PupguiInstallDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 320 10 | 280 11 | 12 | 13 | 14 | 15 | 320 16 | 280 17 | 18 | 19 | 20 | Install Compatibility Tool 21 | 22 | 23 | true 24 | 25 | 26 | 27 | QLayout::SetDefaultConstraint 28 | 29 | 30 | 31 | 32 | Compatibility tool: 33 | 34 | 35 | 36 | 37 | 38 | 39 | Qt::StrongFocus 40 | 41 | 42 | 43 | 44 | 45 | 46 | Version: 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Description: 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 0 65 | 0 66 | 67 | 68 | 69 | true 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Qt::Horizontal 79 | 80 | 81 | 82 | 40 83 | 20 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Info 92 | 93 | 94 | 95 | 96 | 97 | 98 | Install 99 | 100 | 101 | 102 | 103 | 104 | 105 | Cancel 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 440 10 | 360 11 | 12 | 13 | 14 | 15 | 440 16 | 360 17 | 18 | 19 | 20 | ProtonUp-Qt - Wine/Proton Installer 21 | 22 | 23 | 24 | .. 25 | 26 | 27 | 28 | 29 | 6 30 | 31 | 32 | 9 33 | 34 | 35 | 9 36 | 37 | 38 | 9 39 | 40 | 41 | 9 42 | 43 | 44 | 45 | 46 | 47 | 48 | Install for: 49 | 50 | 51 | 52 | 53 | 54 | 55 | Qt::Horizontal 56 | 57 | 58 | 59 | 40 60 | 20 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Active downloads: 69 | 70 | 71 | 72 | 73 | 74 | 75 | 0 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Add Custom Install Directory... 90 | 91 | 92 | ... 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Installed compatibility tools: 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Qt::Horizontal 118 | 119 | 120 | 121 | 40 122 | 20 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | QAbstractItemView::ExtendedSelection 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Add version 149 | 150 | 151 | 152 | 153 | 154 | 155 | Remove selected 156 | 157 | 158 | 159 | 160 | 161 | 162 | Show info 163 | 164 | 165 | 166 | 167 | 168 | 169 | Qt::Horizontal 170 | 171 | 172 | 173 | 40 174 | 20 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | Qt::Horizontal 187 | 188 | 189 | 190 | 40 191 | 20 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | Get tools from Flathub 200 | 201 | 202 | 203 | 204 | 205 | 206 | Show game list 207 | 208 | 209 | 210 | 211 | 212 | 213 | About 214 | 215 | 216 | 217 | 218 | 219 | 220 | Close 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /pupgui2/resources/ui/pupgui2_shortcutdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PupguiShortcutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 899 10 | 350 11 | 12 | 13 | 14 | Qt::ClickFocus 15 | 16 | 17 | Steam Shortcut Editor 18 | 19 | 20 | 21 | 22 | 23 | Qt::ClickFocus 24 | 25 | 26 | QAbstractItemView::NoEditTriggers 27 | 28 | 29 | true 30 | 31 | 32 | 4 33 | 34 | 35 | true 36 | 37 | 38 | 160 39 | 40 | 41 | true 42 | 43 | 44 | false 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Add a new shortcut 58 | 59 | 60 | Add new 61 | 62 | 63 | 64 | 65 | 66 | 67 | Click on a row, then click here to remove a shortcut 68 | 69 | 70 | Remove selected 71 | 72 | 73 | 74 | 75 | 76 | 77 | e.g. ProtonUp-Qt 78 | 79 | 80 | Search for a game... 81 | 82 | 83 | 84 | 85 | 86 | 87 | Save changes and delete marked shortcuts 88 | 89 | 90 | Save 91 | 92 | 93 | 94 | 95 | 96 | 97 | Close without saving changes or deleting shortcuts 98 | 99 | 100 | Close 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.pytest.ini_options] 6 | pythonpath = [ 7 | "." 8 | ] 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PySide6-Essentials>=6.3.0 2 | requests>=2.27.0 3 | vdf @ git+https://github.com/solsticegamestudios/vdf.git@v4.0 4 | inputs==0.5 5 | pyxdg>=0.27 6 | steam @ git+https://github.com/solsticegamestudios/steam.git@v1.6.1 7 | PyYAML>=6.0 8 | zstandard>=0.19.0 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = ProtonUp-Qt 3 | version = 2.12.1 4 | description = Install Wine- and Proton-based compatibility tools 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/DavidoTek/ProtonUp-Qt 8 | author = DavidoTek 9 | license = GPL-3.0 10 | license_files = 11 | LICENSE 12 | project_urls = 13 | Bug Tracker = https://github.com/DavidoTek/ProtonUp-Qt/issues 14 | classifiers = 15 | Programming Language :: Python :: 3 16 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 17 | Operating System :: POSIX :: Linux 18 | 19 | [options] 20 | include_package_data = True 21 | python_requires = >=3.10 22 | install_requires = 23 | PySide6-Essentials>=6.3.0 24 | requests>=2.27.0 25 | vdf @ git+https://github.com/solsticegamestudios/vdf.git@v4.0 26 | inputs==0.5 27 | pyxdg>=0.27 28 | steam @ git+https://github.com/solsticegamestudios/steam.git@v1.6.1 29 | PyYAML>=6.0 30 | zstandard>=0.19.0 31 | 32 | packages = find: 33 | 34 | [options.entry_points] 35 | gui_scripts = 36 | protonup-qt = pupgui2.pupgui2:main 37 | -------------------------------------------------------------------------------- /share/applications/net.davidotek.pupgui2.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=ProtonUp-Qt 3 | Exec=net.davidotek.pupgui2 4 | Type=Application 5 | Comment=Install Wine and Proton-based Compatibility Tools 6 | Comment[de]=Wine und Proton Kompatibilitätstools installieren 7 | Comment[it]=Installa tool di compatibilità basati su Wine e Proton 8 | Comment[nl]=Installeer op Wine en Proton gebaseerde compatibiliteitshulpmiddelen 9 | Comment[pl]=Zainstaluj narzędzia kompatybilności oparte na Wine i Protonie 10 | Terminal=false 11 | Icon=net.davidotek.pupgui2 12 | Categories=Game;Utility; 13 | -------------------------------------------------------------------------------- /share/icons/hicolor/128x128/apps/net.davidotek.pupgui2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/share/icons/hicolor/128x128/apps/net.davidotek.pupgui2.png -------------------------------------------------------------------------------- /share/icons/hicolor/256x256/apps/net.davidotek.pupgui2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/share/icons/hicolor/256x256/apps/net.davidotek.pupgui2.png -------------------------------------------------------------------------------- /share/icons/hicolor/64x64/apps/net.davidotek.pupgui2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidoTek/ProtonUp-Qt/f50e93a0c686a06e6b2dbbcef637a2e7452fd18b/share/icons/hicolor/64x64/apps/net.davidotek.pupgui2.png -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-md 3 | pytest-emoji 4 | 5 | pytest-responses 6 | -------------------------------------------------------------------------------- /tests/test_ctmods.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QApplication 2 | 3 | from pupgui2.ctloader import CtLoader 4 | 5 | 6 | class DummyMainWindow: 7 | """ Dummy MainWindow object for the CtLoader test. Works thanks to duck typing. """ 8 | 9 | def __init__(self, web_access_tokens: dict[str, str]) -> None: 10 | self.web_access_tokens = web_access_tokens 11 | 12 | 13 | def test_ctmod_loader() -> None: 14 | """ 15 | Test loading of ctmods using CtLoader. 16 | """ 17 | app = QApplication() 18 | 19 | dummy_main_window = DummyMainWindow({}) 20 | 21 | ct_loader = CtLoader(main_window=dummy_main_window) 22 | 23 | # Ensure that ctmods are loaded successfully 24 | assert(ct_loader.load_ctmods() is True) 25 | 26 | # Ensure that ctmods are loaded (assume at least one ctmod is exists) 27 | assert(len(ct_loader.get_ctmods()) > 0) 28 | assert(len(ct_loader.get_ctobjs()) > 0) 29 | 30 | # Ensure that no advanced mode ctmods are loaded when advanced_mode is False 31 | assert(all(["advmode" not in ctmod.CT_LAUNCHERS for ctmod in ct_loader.get_ctmods(launcher=None, advanced_mode=False)])) 32 | 33 | QApplication.shutdown(app) 34 | -------------------------------------------------------------------------------- /tests/test_heroicutil.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pupgui2.constants import POSSIBLE_INSTALL_LOCATIONS 4 | 5 | from pupgui2.heroicutil import * 6 | 7 | KNOWN_HEROIC_LAUNCHERS = ['heroicwine', 'heroicproton'] 8 | 9 | @pytest.mark.parametrize('launcher, expected_heroic_launcher', [ 10 | *[pytest.param(install_loc.get('launcher'), install_loc.get('launcher') in KNOWN_HEROIC_LAUNCHERS, id = install_loc.get('display_name')) for install_loc in POSSIBLE_INSTALL_LOCATIONS] 11 | ]) 12 | def test_is_heroic_launcher(launcher: str, expected_heroic_launcher: bool) -> None: 13 | 14 | result: bool = is_heroic_launcher(launcher) 15 | 16 | assert result == expected_heroic_launcher 17 | -------------------------------------------------------------------------------- /tests/test_steamutil.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pupgui2.steamutil import calc_shortcut_app_id 4 | 5 | 6 | @pytest.mark.parametrize( 7 | 'shortcut_dict, expected_appid', [ 8 | pytest.param({ 'name': 'Half-Life 3', 'exe': 'hl3.exe' }, -758629442, id = 'Half-Life 3 (-758629442)'), 9 | pytest.param({ 'name': 'Twenty One', 'exe': '21.exe' }, -416959852, id = 'Twenty One (-416959852)'), 10 | pytest.param({ 'name': 'ProtonUp-Qt', 'exe': 'pupgui2' }, -1763982845, id = 'ProtonUp-Qt (-1763982845)') 11 | ] 12 | ) 13 | def test_calc_shortcut_app_id(shortcut_dict: dict[str, str], expected_appid: int) -> None: 14 | 15 | result: int = calc_shortcut_app_id(shortcut_dict.get('name', ''), shortcut_dict.get('exe', '')) 16 | 17 | assert result == expected_appid 18 | --------------------------------------------------------------------------------