├── .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 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 | [](https://github.com/DavidoTek/ProtonUp-Qt/releases)
2 | [](https://flathub.org/apps/details/net.davidotek.pupgui2)
3 | [](https://github.com/DavidoTek/ProtonUp-Qt/blob/main/LICENSE)
4 | [](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 | 
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 | [ ](https://flathub.org/apps/details/net.davidotek.pupgui2) [ ](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 |
19 | awk (or gawk)
20 | bash
21 | git
22 | pgrep
23 | unzip
24 | wget
25 | xdotool
26 | xprop
27 | xrandr
28 | xwininfo
29 | xxd
30 | Yad >= v7.2
31 |
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 |
--------------------------------------------------------------------------------