Merge pull request #328 from loathingKernel/entry_points
Separate entry points for the main application and the launcher
This commit is contained in:
commit
5d0940083f
10
.github/workflows/checks.yml
vendored
10
.github/workflows/checks.yml
vendored
|
@ -24,14 +24,12 @@ jobs:
|
|||
pylint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install Test Dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
pip3 install astroid
|
||||
|
@ -45,4 +43,4 @@ jobs:
|
|||
pip3 install qstylizer
|
||||
- name: Analysis with pylint
|
||||
run: |
|
||||
python3 -m pylint -E rare --jobs=3 --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5 --generated-members=PyQt5.*
|
||||
python3 -m pylint -E rare --jobs=3 --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5 --generated-members=PyQt5.*,orjson.*
|
||||
|
|
40
.github/workflows/job_appimage.yml
vendored
Normal file
40
.github/workflows/job_appimage.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
name: job_appimage
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install python3 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
|
||||
- name: Install appimage-builder
|
||||
run: |
|
||||
sudo wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
|
||||
sudo chmod +x /usr/local/bin/appimagetool
|
||||
sudo pip3 install appimage-builder
|
||||
- name: Build
|
||||
run: |
|
||||
appimage-builder --skip-test
|
||||
mv Rare-*.AppImage Rare.AppImage
|
||||
mv Rare-*.AppImage.zsync Rare.AppImage.zsync
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ inputs.version }}.AppImage
|
||||
path: Rare.AppImage
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ inputs.version }}.AppImage.zsync
|
||||
path: Rare.AppImage.zsync
|
37
.github/workflows/job_cx-freeze-msi.yml
vendored
Normal file
37
.github/workflows/job_cx-freeze-msi.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
name: job_cx-freeze-msi
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install Build Dependencies
|
||||
run: pip3 install --upgrade cx_freeze wheel
|
||||
- name: Install Target Dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
- name: Build
|
||||
run: |
|
||||
python freeze.py bdist_msi
|
||||
mv dist/*.msi Rare.msi
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ inputs.version }}.msi
|
||||
path: Rare.msi
|
39
.github/workflows/job_cx-freeze-zip.yml
vendored
Normal file
39
.github/workflows/job_cx-freeze-zip.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
name: job_cx-freeze-zip
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install build dependencies
|
||||
run: pip3 install cx_freeze
|
||||
- name: Install target dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
pip3 install .
|
||||
- name: Build
|
||||
run: cxfreeze -c rare/main.py --target-dir dist --target-name rare --icon rare/resources/images/Rare.ico -OO --base-name Win32GUI
|
||||
- name: Compress
|
||||
run: |
|
||||
python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'dist')"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-Windows-${{ inputs.version }}.zip
|
||||
path: Rare-Windows.zip
|
46
.github/workflows/job_macos.yml
vendored
Normal file
46
.github/workflows/job_macos.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: job_macos
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
- name: Install Build Dependencies
|
||||
run: pip install pyinstaller
|
||||
- name: Install Target Dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
- name: Move files
|
||||
run: mv rare/__main__.py __main__.py
|
||||
- name: Build
|
||||
run: >-
|
||||
pyinstaller -F --name Rare
|
||||
--add-data "rare/resources/languages/*:rare/resources/languages"
|
||||
--add-data "rare/resources/images/*:rare/resources/images/"
|
||||
--windowed
|
||||
--icon rare/resources/images/Rare.icns
|
||||
--hidden-import=legendary
|
||||
__main__.py
|
||||
- name: Create dmg
|
||||
run: |
|
||||
git clone https://github.com/create-dmg/create-dmg
|
||||
create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ inputs.version }}.dmg
|
||||
path: Rare.dmg
|
70
.github/workflows/job_nuitka-win.yml
vendored
Normal file
70
.github/workflows/job_nuitka-win.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
name: job_nuitka-win
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.9'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install build dependencies
|
||||
run: pip3 install nuitka ordered-set
|
||||
- name: Install target dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
- name: Build
|
||||
run: >-
|
||||
python -m nuitka
|
||||
--assume-yes-for-downloads
|
||||
--msvc=latest
|
||||
--lto=yes
|
||||
--jobs=2
|
||||
--static-libpython=no
|
||||
--standalone
|
||||
--enable-plugin=anti-bloat
|
||||
--enable-plugin=pyqt5
|
||||
--show-modules
|
||||
--show-anti-bloat-changes
|
||||
--follow-stdlib
|
||||
--follow-imports
|
||||
--nofollow-import-to="*.tests"
|
||||
--nofollow-import-to="*.distutils"
|
||||
--prefer-source-code
|
||||
--include-package=pypresence
|
||||
--include-package-data=qtawesome
|
||||
--include-data-dir=rare\resources\images=rare\resources\images
|
||||
--include-data-files=rare\resources\languages=rare\resources\languages="*.qm"
|
||||
--windows-icon-from-ico=rare\resources\images\Rare.ico
|
||||
--windows-company-name=Rare
|
||||
--windows-product-name=Rare
|
||||
--windows-file-description=rare.exe
|
||||
--windows-file-version=${{ inputs.version }}
|
||||
--windows-product-version=${{ inputs.version }}
|
||||
--disable-console
|
||||
rare
|
||||
- name: Fix QtNetwork SSL
|
||||
run: |
|
||||
Copy-Item -Path "rare.dist\libcrypto-1_1.dll" -Destination "rare.dist\libcrypto-1_1-x64.dll"
|
||||
Copy-Item -Path "rare.dist\libssl-1_1.dll" -Destination "rare.dist\libssl-1_1-x64.dll"
|
||||
- name: Compress
|
||||
run: |
|
||||
python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'rare.dist')"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-Windows-${{ inputs.version }}.zip
|
||||
path: Rare-Windows.zip
|
30
.github/workflows/job_pypi.yml
vendored
Normal file
30
.github/workflows/job_pypi.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: job_pypi
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
pip3 install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
49
.github/workflows/job_release.yml
vendored
Normal file
49
.github/workflows/job_release.yml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
name: job_release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
file1:
|
||||
required: true
|
||||
type: string
|
||||
name1:
|
||||
required: true
|
||||
type: string
|
||||
name2:
|
||||
type: string
|
||||
file2:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Download ${{ inputs.name1 }} artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ inputs.name1 }}
|
||||
- name: Upload ${{ inputs.name1 }} to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ inputs.file1 }}
|
||||
asset_name: ${{ inputs.name1 }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
- name: Download ${{ inputs.name2 }} artifact
|
||||
uses: actions/download-artifact@v3
|
||||
if: ${{ inputs.name2 != '' }}
|
||||
with:
|
||||
name: ${{ inputs.name2 }}
|
||||
- name: Upload ${{ inputs.name2 }} to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: ${{ inputs.name2 != '' }}
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ inputs.file2 }}
|
||||
asset_name: ${{ inputs.name2 }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
39
.github/workflows/job_ubuntu.yml
vendored
Normal file
39
.github/workflows/job_ubuntu.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
name: job_ubuntu
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install makedeb
|
||||
run: |
|
||||
wget -qO - 'https://proget.makedeb.org/debian-feeds/makedeb.pub' | gpg --dearmor | sudo tee /usr/share/keyrings/makedeb-archive-keyring.gpg 1> /dev/null
|
||||
echo 'deb [signed-by=/usr/share/keyrings/makedeb-archive-keyring.gpg arch=all] https://proget.makedeb.org/ makedeb main' | sudo tee /etc/apt/sources.list.d/makedeb.list
|
||||
sudo apt update
|
||||
sudo apt install makedeb
|
||||
- name: Prepare source directory
|
||||
run: |
|
||||
git clone https://github.com/RareDevs/package-mpr.git build
|
||||
sed '/^pkgver=/d' -i build/PKGBUILD
|
||||
sed '/^source=/d' -i build/PKGBUILD
|
||||
echo "pkgver=${{ inputs.version }}" >> build/PKGBUILD
|
||||
echo "source=(\"git+https://github.com/${{ github.repository }}.git#branch=${{ github.ref_name }}\")" >> build/PKGBUILD
|
||||
|
||||
- name: Run makedeb
|
||||
run: |
|
||||
cd build
|
||||
makedeb -d
|
||||
mv *.deb ../Rare.deb
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ inputs.version }}.deb
|
||||
path: Rare.deb
|
247
.github/workflows/release-tests.yml
vendored
247
.github/workflows/release-tests.yml
vendored
|
@ -1,247 +0,0 @@
|
|||
|
||||
name: "Snapshot"
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: "Version"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag_abbrev: ${{ steps.version.outputs.tag_abbrev }}
|
||||
tag_offset: ${{ steps.version.outputs.tag_offset }}
|
||||
sha_short: ${{ steps.version.outputs.sha_short }}
|
||||
full_desc: ${{ steps.version.outputs.full_desc }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
tag_abbrev=$(git tag --sort=v:refname | grep -oE "(^[0-9]+\.[0-9]+(.[0-9]+)?)$" | tail -1)
|
||||
echo "tag_abbrev=$tag_abbrev" >> $GITHUB_OUTPUT
|
||||
echo "tag_offset=$(git rev-list $tag_abbrev..HEAD --count)" >> $GITHUB_OUTPUT
|
||||
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "full_desc=$(git describe --long --tags)" >> $GITHUB_OUTPUT
|
||||
|
||||
deb-package:
|
||||
needs: version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Makedeb
|
||||
run: |
|
||||
wget -qO - 'https://proget.hunterwittenborn.com/debian-feeds/makedeb.pub' | gpg --dearmor | sudo tee /usr/share/keyrings/makedeb-archive-keyring.gpg &> /dev/null
|
||||
echo 'deb [signed-by=/usr/share/keyrings/makedeb-archive-keyring.gpg arch=all] https://proget.hunterwittenborn.com/ makedeb main' | sudo tee /etc/apt/sources.list.d/makedeb.list
|
||||
sudo apt update
|
||||
sudo apt install makedeb
|
||||
- name: Prepare source directory
|
||||
run: |
|
||||
git clone https://mpr.hunterwittenborn.com/rare.git build
|
||||
sed -i 's/source=.*/source=("rare-test::git+$url")/g' build/PKGBUILD
|
||||
sed -i "s/\$pkgver/test/g" build/PKGBUILD
|
||||
|
||||
- name: build deb
|
||||
run: |
|
||||
cd build
|
||||
makedeb -d
|
||||
mv *.deb ../Rare.deb
|
||||
|
||||
- name: Upload to Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.deb
|
||||
path: Rare.deb
|
||||
|
||||
appimage:
|
||||
needs: version
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install python3 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
|
||||
- name: Install appimage-builder
|
||||
run: |
|
||||
sudo wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
|
||||
sudo chmod +x /usr/local/bin/appimagetool
|
||||
sudo pip3 install appimage-builder
|
||||
- name: Build
|
||||
run: |
|
||||
cp rare/__main__.py .
|
||||
appimage-builder --skip-test
|
||||
mv Rare-*.AppImage Rare.AppImage
|
||||
mv Rare-*.AppImage.zsync Rare.AppImage.zsync
|
||||
|
||||
- name: Upload to Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.AppImage
|
||||
path: Rare.AppImage
|
||||
- name: Upload to Artifacts (zsync)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.AppImage.zsync
|
||||
path: Rare.AppImage.zsync
|
||||
|
||||
nuitka:
|
||||
if: ${{ false }}
|
||||
needs: version
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.9'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install build dependencies
|
||||
run: pip3 install nuitka ordered-set
|
||||
- name: Install target dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
- name: Build
|
||||
run: >-
|
||||
python -m nuitka
|
||||
--assume-yes-for-downloads
|
||||
--msvc=latest
|
||||
--lto=yes
|
||||
--jobs=2
|
||||
--static-libpython=no
|
||||
--standalone
|
||||
--enable-plugin=anti-bloat
|
||||
--enable-plugin=pyqt5
|
||||
--show-modules
|
||||
--show-anti-bloat-changes
|
||||
--follow-stdlib
|
||||
--follow-imports
|
||||
--nofollow-import-to="*.tests"
|
||||
--nofollow-import-to="*.distutils"
|
||||
--prefer-source-code
|
||||
--include-package=pypresence
|
||||
--include-package-data=qtawesome
|
||||
--include-data-dir=rare\resources\images=rare\resources\images
|
||||
--include-data-files=rare\resources\languages=rare\resources\languages="*.qm"
|
||||
--windows-icon-from-ico=rare\resources\images\Rare.ico
|
||||
--windows-company-name=Rare
|
||||
--windows-product-name=Rare
|
||||
--windows-file-description=rare.exe
|
||||
--windows-file-version=${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
--windows-product-version=${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
--enable-console
|
||||
rare
|
||||
- name: Fix QtNetwork SSL
|
||||
run: |
|
||||
Copy-Item -Path "rare.dist\libcrypto-1_1.dll" -Destination "rare.dist\libcrypto-1_1-x64.dll"
|
||||
Copy-Item -Path "rare.dist\libssl-1_1.dll" -Destination "rare.dist\libssl-1_1-x64.dll"
|
||||
- name: Compress
|
||||
run: |
|
||||
python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'rare.dist')"
|
||||
|
||||
- name: Upload to Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-Windows-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
path: Rare-Windows.zip
|
||||
|
||||
cx_freeze_msi:
|
||||
needs: version
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install Build Dependencies
|
||||
run: pip3 install --upgrade cx_freeze wheel
|
||||
- name: Install Target Dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
- name: Build
|
||||
run: |
|
||||
python freeze.py bdist_msi
|
||||
mv dist/*.msi Rare.msi
|
||||
|
||||
- name: Upload to Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.msi
|
||||
path: Rare.msi
|
||||
|
||||
cx_freeze_zip:
|
||||
needs: version
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install build dependencies
|
||||
run: pip3 install cx_freeze
|
||||
- name: Install target dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
pip3 install .
|
||||
- name: Build
|
||||
run: cxfreeze -c rare/__main__.py --target-dir dist --target-name rare --icon rare/resources/images/Rare.ico -OO --base-name Win32GUI
|
||||
- name: Compress
|
||||
run: |
|
||||
python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'dist')"
|
||||
|
||||
- name: Upload to Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-Windows-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.zip
|
||||
path: Rare-Windows.zip
|
||||
|
||||
mac_os:
|
||||
needs: version
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
- name: Install Build Dependencies
|
||||
run: pip install pyinstaller
|
||||
- name: Install Target Dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
- name: Move files
|
||||
run: mv rare/__main__.py __main__.py
|
||||
- name: Build
|
||||
run: >-
|
||||
pyinstaller -F --name Rare
|
||||
--add-data "rare/resources/languages/*:rare/resources/languages"
|
||||
--add-data "rare/resources/images/*:rare/resources/images/"
|
||||
--windowed
|
||||
--icon rare/resources/images/Rare.icns
|
||||
--hidden-import=legendary
|
||||
__main__.py
|
||||
- name: Create dmg
|
||||
run: |
|
||||
git clone https://github.com/create-dmg/create-dmg
|
||||
create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
|
||||
|
||||
- name: Upload to Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.dmg
|
||||
path: Rare.dmg
|
327
.github/workflows/release.yml
vendored
327
.github/workflows/release.yml
vendored
|
@ -3,7 +3,6 @@ name: "Release"
|
|||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
|
@ -13,254 +12,98 @@ permissions:
|
|||
|
||||
|
||||
jobs:
|
||||
pypi-deploy:
|
||||
|
||||
describe:
|
||||
name: Version ${{ github.ref_name }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: "true"
|
||||
|
||||
pypi:
|
||||
if: "!github.event.release.prerelease"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
name: PyPI
|
||||
uses: ./.github/workflows/job_pypi.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
deb-package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Makedeb
|
||||
run: |
|
||||
wget -qO - 'https://proget.hunterwittenborn.com/debian-feeds/makedeb.pub' | gpg --dearmor | sudo tee /usr/share/keyrings/makedeb-archive-keyring.gpg &> /dev/null
|
||||
echo 'deb [signed-by=/usr/share/keyrings/makedeb-archive-keyring.gpg arch=all] https://proget.hunterwittenborn.com/ makedeb main' | sudo tee /etc/apt/sources.list.d/makedeb.list
|
||||
sudo apt update
|
||||
sudo apt install makedeb
|
||||
- name: Prepare source directory
|
||||
run: |
|
||||
git clone https://mpr.makedeb.org/rare build
|
||||
sed -i "s/pkgver=.*/pkgver=${{ github.event.release.tag_name }}/g" build/PKGBUILD
|
||||
|
||||
- name: build deb
|
||||
run: |
|
||||
cd build
|
||||
makedeb -d
|
||||
mv *.deb ../Rare.deb
|
||||
|
||||
- name: Upload to Release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: Rare.deb
|
||||
asset_name: Rare-${{ github.event.release.tag_name }}.deb
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
ubuntu:
|
||||
name: Ubuntu
|
||||
uses: ./.github/workflows/job_ubuntu.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
ubuntu-release:
|
||||
needs: ubuntu
|
||||
name: Ubuntu
|
||||
uses: ./.github/workflows/job_release.yml
|
||||
with:
|
||||
file1: Rare.deb
|
||||
name1: Rare-${{ github.ref_name }}.deb
|
||||
|
||||
appimage:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install python3 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
|
||||
- name: Install appimage-builder
|
||||
run: |
|
||||
sudo wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
|
||||
sudo chmod +x /usr/local/bin/appimagetool
|
||||
sudo pip3 install appimage-builder
|
||||
- name: Build
|
||||
run: |
|
||||
cp rare/__main__.py .
|
||||
appimage-builder --skip-test
|
||||
mv Rare-*.AppImage Rare.AppImage
|
||||
mv Rare-*.AppImage.zsync Rare.AppImage.zsync
|
||||
name: AppImage
|
||||
uses: ./.github/workflows/job_appimage.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
appimage-release:
|
||||
needs: appimage
|
||||
name: AppImage
|
||||
uses: ./.github/workflows/job_release.yml
|
||||
with:
|
||||
file1: Rare.AppImage
|
||||
name1: Rare-${{ github.ref_name }}.AppImage
|
||||
file2: Rare.AppImage.zsync
|
||||
name2: Rare-${{ github.ref_name }}.AppImage.zsync
|
||||
|
||||
- name: Upload to Release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: Rare.AppImage
|
||||
asset_name: Rare-${{ github.event.release.tag_name }}.AppImage
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
- name: Upload to Release (zsync)
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: Rare.AppImage.zsync
|
||||
asset_name: Rare-${{ github.event.release.tag_name }}.AppImage.zsync
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
nuitka:
|
||||
nuitka-win:
|
||||
if: ${{ false }}
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.9'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install build dependencies
|
||||
run: pip3 install nuitka ordered-set
|
||||
- name: Install target dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
- name: Build
|
||||
run: >-
|
||||
python -m nuitka
|
||||
--assume-yes-for-downloads
|
||||
--msvc=latest
|
||||
--lto=yes
|
||||
--jobs=2
|
||||
--static-libpython=no
|
||||
--standalone
|
||||
--enable-plugin=anti-bloat
|
||||
--enable-plugin=pyqt5
|
||||
--show-modules
|
||||
--show-anti-bloat-changes
|
||||
--follow-stdlib
|
||||
--follow-imports
|
||||
--nofollow-import-to="*.tests"
|
||||
--nofollow-import-to="*.distutils"
|
||||
--prefer-source-code
|
||||
--include-package=pypresence
|
||||
--include-package-data=qtawesome
|
||||
--include-data-dir=rare\resources\images=rare\resources\images
|
||||
--include-data-files=rare\resources\languages=rare\resources\languages="*.qm"
|
||||
--windows-icon-from-ico=rare\resources\images\Rare.ico
|
||||
--windows-company-name=Rare
|
||||
--windows-product-name=Rare
|
||||
--windows-file-description=rare.exe
|
||||
--windows-file-version=${{ github.event.release.tag_name }}
|
||||
--windows-product-version=${{ github.event.release.tag_name }}
|
||||
--disable-console
|
||||
rare
|
||||
- name: Fix QtNetwork SSL
|
||||
run: |
|
||||
Copy-Item -Path "rare.dist\libcrypto-1_1.dll" -Destination "rare.dist\libcrypto-1_1-x64.dll"
|
||||
Copy-Item -Path "rare.dist\libssl-1_1.dll" -Destination "rare.dist\libssl-1_1-x64.dll"
|
||||
- name: Compress
|
||||
run: |
|
||||
python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'rare.dist')"
|
||||
name: Nuitka Windows
|
||||
uses: ./.github/workflows/job_nuitka-win.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
nuitka-win-release:
|
||||
needs: nuitka-win
|
||||
name: Nuitka Windows
|
||||
uses: ./.github/workflows/job_release.yml
|
||||
with:
|
||||
file1: Rare-Windows.zip
|
||||
name1: Rare-Windows-${{ github.ref_name }}.zip
|
||||
|
||||
- name: Upload to Release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: Rare-Windows.zip
|
||||
asset_name: Rare-Windows-${{ github.event.release.tag_name }}.zip
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
cx-freeze-msi:
|
||||
name: cx-Freeze msi
|
||||
uses: ./.github/workflows/job_cx-freeze-msi.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
cx-freeze-msi-release:
|
||||
needs: cx-freeze-msi
|
||||
name: cx-Freeze msi
|
||||
uses: ./.github/workflows/job_release.yml
|
||||
with:
|
||||
file1: Rare.msi
|
||||
name1: Rare-${{ github.ref_name }}.msi
|
||||
|
||||
cx_freeze_msi:
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install Build Dependencies
|
||||
run: pip3 install --upgrade cx_freeze wheel
|
||||
- name: Install Target Dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
- name: Build
|
||||
run: |
|
||||
python freeze.py bdist_msi
|
||||
mv dist/*.msi Rare.msi
|
||||
cx-freeze-zip:
|
||||
name: cx-Freeze zip
|
||||
uses: ./.github/workflows/job_cx-freeze-zip.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
cx-freeze-zip-release:
|
||||
needs: cx-freeze-zip
|
||||
name: cx-Freeze zip
|
||||
uses: ./.github/workflows/job_release.yml
|
||||
with:
|
||||
file1: Rare-Windows.zip
|
||||
name1: Rare-Windows-${{ github.ref_name }}.zip
|
||||
|
||||
- name: Upload to Release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: Rare.msi
|
||||
asset_name: Rare-${{ github.event.release.tag_name }}.msi
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
macos:
|
||||
name: MacOS
|
||||
uses: ./.github/workflows/job_macos.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
macos-release:
|
||||
needs: macos
|
||||
name: MacOS
|
||||
uses: ./.github/workflows/job_release.yml
|
||||
with:
|
||||
file1: Rare.dmg
|
||||
name1: Rare-${{ github.ref_name }}.dmg
|
||||
|
||||
cx_freeze_zip:
|
||||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install build dependencies
|
||||
run: pip3 install cx_freeze
|
||||
- name: Install target dependencies
|
||||
run: |
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-presence.txt
|
||||
pip3 install .
|
||||
- name: Build
|
||||
run: cxfreeze -c rare/__main__.py --target-dir dist --target-name rare --icon rare/resources/images/Rare.ico -OO --base-name Win32GUI
|
||||
- name: Compress
|
||||
run: |
|
||||
python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'dist')"
|
||||
|
||||
- name: Upload to Release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: Rare-Windows.zip
|
||||
asset_name: Rare-Windows-${{ github.event.release.tag_name }}.zip
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
mac_os:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
check-latest: true
|
||||
- name: Install Build Dependencies
|
||||
run: pip install pyinstaller
|
||||
- name: Install Target Dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
- name: Move files
|
||||
run: mv rare/__main__.py __main__.py
|
||||
- name: Build
|
||||
run: >-
|
||||
pyinstaller -F --name Rare
|
||||
--add-data "rare/resources/languages/*:rare/resources/languages"
|
||||
--add-data "rare/resources/images/*:rare/resources/images/"
|
||||
--windowed
|
||||
--icon rare/resources/images/Rare.icns
|
||||
--hidden-import=legendary
|
||||
__main__.py
|
||||
- name: Create dmg
|
||||
run: |
|
||||
git clone https://github.com/create-dmg/create-dmg
|
||||
create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
|
||||
|
||||
- name: Upload to Release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: Rare.dmg
|
||||
asset_name: Rare-${{ github.event.release.tag_name }}.dmg
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
|
88
.github/workflows/snapshot.yml
vendored
Normal file
88
.github/workflows/snapshot.yml
vendored
Normal file
|
@ -0,0 +1,88 @@
|
|||
|
||||
name: "Snapshot"
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types: [closed]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
version:
|
||||
name: Describe
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag_abbrev: ${{ steps.describe.outputs.tag_abbrev }}
|
||||
tag_offset: ${{ steps.describe.outputs.tag_offset }}
|
||||
sha_short: ${{ steps.describe.outputs.sha_short }}
|
||||
full_desc: ${{ steps.describe.outputs.full_desc }}
|
||||
branch: ${{ steps.describe.outputs.branch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Describe version
|
||||
id: describe
|
||||
shell: bash
|
||||
run: |
|
||||
tag_abbrev=$(git tag --sort=v:refname | grep -oE "(^[0-9]+\.[0-9]+(.[0-9]+)?)$" | tail -1)
|
||||
echo "tag_abbrev=$tag_abbrev" >> $GITHUB_OUTPUT
|
||||
echo "tag_offset=$(git rev-list $tag_abbrev..HEAD --count)" >> $GITHUB_OUTPUT
|
||||
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "full_desc=$(git describe --long --tags)" >> $GITHUB_OUTPUT
|
||||
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
|
||||
describe:
|
||||
needs: version
|
||||
name: Version ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: "true"
|
||||
|
||||
ubuntu:
|
||||
needs: version
|
||||
name: Ubuntu
|
||||
uses: ./.github/workflows/job_ubuntu.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
|
||||
appimage:
|
||||
needs: version
|
||||
name: AppImage
|
||||
uses: ./.github/workflows/job_appimage.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
|
||||
nuitka-win:
|
||||
if: ${{ false }}
|
||||
needs: version
|
||||
name: Nuitka Windows
|
||||
uses: ./.github/workflows/job_nuitka-win.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
|
||||
cx-freeze-msi:
|
||||
needs: version
|
||||
name: cx-Freeze msi
|
||||
uses: ./.github/workflows/job_cx-freeze-msi.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
|
||||
cx-freeze-zip:
|
||||
needs: version
|
||||
name: cx-Freeze zip
|
||||
uses: ./.github/workflows/job_cx-freeze-zip.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
|
||||
macos:
|
||||
needs: version
|
||||
name: MacOS
|
||||
uses: ./.github/workflows/job_macos.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
|
||||
|
|
@ -3,17 +3,16 @@ version: 1
|
|||
|
||||
script:
|
||||
# Remove any previous build
|
||||
- rm -rf AppDir Rare | true
|
||||
- rm -rf AppDir | true
|
||||
# Make usr and icons dirs
|
||||
- mkdir -p AppDir/usr/src
|
||||
- mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps/
|
||||
# Copy source files
|
||||
- cp -r rare AppDir/usr/src/rare
|
||||
- mv AppDir/usr/src/rare/__main__.py AppDir/usr/src/
|
||||
# copy Logo
|
||||
- cp AppDir/usr/src/rare/resources/images/Rare.png AppDir/usr/share/icons/hicolor/256x256/apps/
|
||||
# Install application dependencies
|
||||
- python3 -m pip install --ignore-installed --prefix=/usr --root=AppDir pypresence qtawesome legendary-gl typing_extensions
|
||||
- python3 -m pip install --ignore-installed --prefix=/usr --root=AppDir pypresence qtawesome legendary-gl orjson
|
||||
|
||||
AppDir:
|
||||
path: AppDir
|
||||
|
@ -23,7 +22,7 @@ AppDir:
|
|||
icon: Rare
|
||||
version: 1.10.7
|
||||
exec: usr/bin/python3
|
||||
exec_args: $APPDIR/usr/src/__main__.py $@
|
||||
exec_args: $APPDIR/usr/src/rare/main.py $@
|
||||
apt:
|
||||
arch: amd64
|
||||
allow_unauthenticated: true
|
||||
|
|
59
README.md
59
README.md
|
@ -45,32 +45,36 @@ Run it via:
|
|||
There are some AUR packages available:
|
||||
|
||||
- [rare](https://aur.archlinux.org/packages/rare) - for stable releases
|
||||
- [rare-git](https://aur.archlinux.org/packages/rare-git) - for the latest features, which are not in a stable release
|
||||
- [rare-git](https://aur.archlinux.org/packages/rare-git) - for the latest development version
|
||||
|
||||
#### Debian based
|
||||
|
||||
- DUR package: [rare](https://mpr.hunterwittenborn.com/packages/rare)
|
||||
- .deb file in [releases page](https://github.com/Dummerle/Rare/releases)
|
||||
- `.deb` file in [releases page](https://github.com/Dummerle/Rare/releases)
|
||||
|
||||
**Note**:
|
||||
|
||||
- pypresence is an optional package. You can install it
|
||||
from [DUR](https://mpr.hunterwittenborn.com/packages/python3-pypresence) or with pip.
|
||||
- Do not wonder if some icons look strange, because the official python3-qtawesome package is too old. Many icons were
|
||||
replaced.
|
||||
- pypresence is an optional package. You can install it from [DUR](https://mpr.hunterwittenborn.com/packages/python3-pypresence) or with pip.
|
||||
- Some icons might look strange on Debian based distributions. The official python3-qtawesome package is too old.
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
There is a .dmg file available in [releases page](https://github.com/Dummerle/Rare/releases).
|
||||
There is a `.dmg` file available in [releases page](https://github.com/Dummerle/Rare/releases).
|
||||
|
||||
**Note**: When you launch it, you will see an error, that the package is from an unknown source. You have to enable it
|
||||
manually in `Settings -> Security and Privacy`. Otherwise, Gatekeeper will block Rare from running.
|
||||
**Note**: When you launch it, you will see an error, that the package is from an unknown source. You have to enable it manually in `Settings -> Security and Privacy`. Otherwise, Gatekeeper will block Rare from running.
|
||||
|
||||
You can also use `pip`.
|
||||
|
||||
### Windows
|
||||
|
||||
There is an `.msi` installer available in [releases page](https://github.com/Dummerle/Rare/releases).
|
||||
|
||||
There is also a semi-portable `.zip` archive in [releases page](https://github.com/Dummerle/Rare/releases) that lets you run Rare without installing it.
|
||||
|
||||
**Important**: On recent version of Windows you should have MSVC 2015 installed, you can get it from [here](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022)
|
||||
|
||||
#### Packages
|
||||
|
||||
- Rare is available as a [Winget package](https://github.com/microsoft/winget-pkgs/tree/master/manifests/d/Dummerle/Rare)
|
||||
You can install Rare with the following one-liner:
|
||||
|
||||
|
@ -81,10 +85,7 @@ You can install Rare with the following one-liner:
|
|||
|
||||
`choco install rare`
|
||||
|
||||
- There is a small beta tool for Windows: [Rare Updater](https://github.com/Dummerle/RareUpdater), which installs and updates rare with a single click
|
||||
|
||||
|
||||
*NOTE*: On recent Windows you should have MSVC 2015 installed, you can get it from [here](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022)
|
||||
- We also have a beta tool for Windows: [Rare Updater](https://github.com/Dummerle/RareUpdater), which installs and updates rare with a single click
|
||||
|
||||
### Packages
|
||||
|
||||
|
@ -95,7 +96,8 @@ file for macOS.
|
|||
|
||||
In the [actions](https://github.com/Dummerle/Rare/actions) tab you can find packages for the latest commits.
|
||||
|
||||
**Note**: They might be unstable.
|
||||
**Note**: They might be unstable and likely broken.
|
||||
|
||||
|
||||
### Installation via pip (platform independent)
|
||||
|
||||
|
@ -105,22 +107,13 @@ Linux, Mac and FreeBSD: execute `rare` in your terminal.
|
|||
|
||||
Windows: execute `pythonw -m rare` in cmd
|
||||
|
||||
It is possible to create a desktop link, or a start menu link. Execute the command above with `--desktop-shortcut`
|
||||
or `--startmenu-shortcut` option, alternatively you can create them in the settings.
|
||||
It is possible to create a desktop link, or a start menu link. Execute the command above with `--desktop-shortcut` or `--startmenu-shortcut` option, alternatively you can create them in the settings.
|
||||
|
||||
**Note about $PATH**:
|
||||
|
||||
On Linux:
|
||||
|
||||
`/home/user/.local/bin` must be in your PATH.
|
||||
|
||||
On Windows:
|
||||
|
||||
`PythonInstallationDirectory\Scripts` must be in your PATH.
|
||||
|
||||
On Mac:
|
||||
|
||||
`/Users/user/Library/Python/3.x/bin` must be in your PATH.
|
||||
* On Linux `/home/user/.local/bin` must be in your PATH.
|
||||
* On Windows `PythonInstallationDirectory\Scripts` must be in your PATH.
|
||||
* On Mac `/Users/user/Library/Python/3.x/bin` must be in your PATH.
|
||||
|
||||
|
||||
### Run from source
|
||||
|
@ -128,10 +121,9 @@ On Mac:
|
|||
1. Clone the repo: `git clone https://github.com/Dummerle/Rare
|
||||
2. Change your working directory to the project folder: `cd Rare`
|
||||
3. Run `pip install -r requirements.txt` to install all required dependencies.
|
||||
If you want to be able to use the automatic login and Discord pypresence, run `pip install -r requirements-full.txt`
|
||||
If you are on Arch you can
|
||||
run `sudo pacman --needed -S python-wheel python-setuptools python-pyqt5 python-qtawesome python-requests python-typing_extensions` and `yay -S legendary`
|
||||
If you are on FreeBSD you have to install py39-qt5 from the packages: `sudo pkg install py39-qt5`
|
||||
* If you want to be able to use the automatic login and Discord pypresence, run `pip install -r requirements-full.txt`
|
||||
* If you are on Arch you can run `sudo pacman --needed -S python-wheel python-setuptools python-pyqt5 python-qtawesome python-requests python-orjson` and `yay -S legendary`
|
||||
* If you are on FreeBSD you have to install py39-qt5 from the packages: `sudo pkg install py39-qt5`
|
||||
4. Run `python3 -m rare`
|
||||
|
||||
## Contributing
|
||||
|
@ -139,8 +131,7 @@ On Mac:
|
|||
There are several options to contribute.
|
||||
|
||||
- If you know Python and PyQt, you can implement new features (Some ideas are in the projects tab).
|
||||
- You can translate the application in your language: Check our [transifex](https://www.transifex.com/rare-1/rare) page
|
||||
for that.
|
||||
- You can translate the application in your language: Check our [transifex](https://www.transifex.com/rare-1/rare) page for that.
|
||||
|
||||
More information is available in CONTRIBUTING.md.
|
||||
|
||||
|
|
|
@ -31,9 +31,11 @@ bdist_msi_options = {
|
|||
base = "Win32GUI"
|
||||
|
||||
exe = Executable(
|
||||
"rare/__main__.py",
|
||||
base=base, icon="rare/resources/images/Rare.ico",
|
||||
target_name="Rare")
|
||||
"rare/main.py",
|
||||
base=base,
|
||||
icon="rare/resources/images/Rare.ico",
|
||||
target_name=name
|
||||
)
|
||||
|
||||
setup(
|
||||
name=name,
|
||||
|
|
|
@ -17,7 +17,7 @@ description = "A GUI for Legendary"
|
|||
authors = ["Dummerle"]
|
||||
license = "GPL3"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/Dummerle/Rare"
|
||||
repository = "https://github.com/RareDevs/Rare"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
|
@ -32,10 +32,11 @@ pywebview = [
|
|||
{ version = "^3.6.3", extras = ["gtk"], platform = "freebsd", optional = true },
|
||||
]
|
||||
legendary-gl = "^0.20.34"
|
||||
orjson = "^3.8.0"
|
||||
typing-extensions = "^4.3.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
start = "rare.__main__:main"
|
||||
start = "rare.main:main"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
Nuitka = "^1.0.6"
|
||||
|
|
|
@ -1,2 +1,8 @@
|
|||
__version__ = "1.10.7"
|
||||
__codename__ = "Garlic Crab"
|
||||
|
||||
# For PyCharm profiler
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from rare.main import main
|
||||
sys.exit(main())
|
||||
|
|
135
rare/__main__.py
Executable file → Normal file
135
rare/__main__.py
Executable file → Normal file
|
@ -1,133 +1,4 @@
|
|||
#!/usr/bin/env python3
|
||||
import multiprocessing
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
def main():
|
||||
# fix cx_freeze
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
# insert legendary for installed via pip/setup.py submodule to path
|
||||
# if not __name__ == "__main__":
|
||||
# sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary"))
|
||||
|
||||
# CLI Options
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-V", "--version", action="store_true", help="Shows version and exits"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-S",
|
||||
"--silent",
|
||||
action="store_true",
|
||||
help="Launch Rare in background. Open it from System Tray Icon",
|
||||
)
|
||||
parser.add_argument("--debug", action="store_true", help="Launch in debug mode")
|
||||
parser.add_argument(
|
||||
"--offline", action="store_true", help="Launch Rare in offline mode"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-start", action="store_true", help="Quit immediately after launch"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--desktop-shortcut",
|
||||
action="store_true",
|
||||
dest="desktop_shortcut",
|
||||
help="Use this, if there is no link on desktop to start Rare",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--startmenu-shortcut",
|
||||
action="store_true",
|
||||
dest="startmenu_shortcut",
|
||||
help="Use this, if there is no link in start menu to launch Rare",
|
||||
)
|
||||
subparsers = parser.add_subparsers(title="Commands", dest="subparser")
|
||||
|
||||
launch_minimal_parser = subparsers.add_parser("start", aliases=["launch"])
|
||||
launch_minimal_parser.add_argument("app_name", help="AppName of the game to launch",
|
||||
metavar="<App Name>", action="store")
|
||||
launch_minimal_parser.add_argument("--dry-run", help="Print arguments and exit", action="store_true")
|
||||
launch_minimal_parser.add_argument("--offline", help="Launch game offline",
|
||||
action="store_true")
|
||||
launch_minimal_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='<wine binary>',
|
||||
default=os.environ.get('LGDRY_WINE_BINARY', None),
|
||||
help='Set WINE binary to use to launch the app')
|
||||
launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='<wine pfx path>',
|
||||
default=os.environ.get('LGDRY_WINE_PREFIX', None),
|
||||
help='Set WINE prefix to use')
|
||||
launch_minimal_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves",
|
||||
action="store_true")
|
||||
launch_minimal_parser.add_argument("--skip-update-check", help="Do not check for updates",
|
||||
action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.desktop_shortcut or args.startmenu_shortcut:
|
||||
from rare.utils.paths import create_desktop_link
|
||||
|
||||
if args.desktop_shortcut:
|
||||
create_desktop_link(app_name="rare_shortcut", link_type="desktop")
|
||||
|
||||
if args.startmenu_shortcut:
|
||||
create_desktop_link(app_name="rare_shortcut", link_type="start_menu")
|
||||
|
||||
print("Link created")
|
||||
return
|
||||
|
||||
if args.version:
|
||||
from rare import __version__, code_name
|
||||
print(f"Rare {__version__} Codename: {code_name}")
|
||||
return
|
||||
|
||||
if args.subparser == "start" or args.subparser == "launch":
|
||||
from rare import game_launch_helper as helper
|
||||
helper.start_game(args)
|
||||
return
|
||||
|
||||
from rare.utils import singleton
|
||||
|
||||
try:
|
||||
# this object only allows one instance per machine
|
||||
|
||||
me = singleton.SingleInstance()
|
||||
except singleton.SingleInstanceException:
|
||||
print("Rare is already running")
|
||||
from rare.utils.paths import lock_file
|
||||
|
||||
with open(lock_file(), "w") as file:
|
||||
file.write("show")
|
||||
file.close()
|
||||
return
|
||||
|
||||
from rare.app import start
|
||||
|
||||
start(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# run from source
|
||||
# insert raw legendary submodule
|
||||
# sys.path.insert(
|
||||
# 0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
|
||||
# )
|
||||
|
||||
# insert source directory
|
||||
if "__compiled__" not in globals():
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
|
||||
|
||||
# If we are on Windows, and we are in a "compiled" GUI application form
|
||||
# stdout (and stderr?) will be None. So to avoid `'NoneType' object has no attribute 'write'`
|
||||
# errors, redirect both of them to devnull
|
||||
if os.name == "nt" and (getattr(sys, "frozen", False) or ("__compiled__" in globals())):
|
||||
# Check if stdout and stderr are None before redirecting
|
||||
# This is useful in the case of test builds that enable console
|
||||
if sys.stdout is None:
|
||||
sys.stdout = open(os.devnull, 'w')
|
||||
if sys.stderr is None:
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
|
||||
main()
|
||||
import sys
|
||||
from rare.main import main
|
||||
sys.exit(main())
|
||||
|
|
132
rare/app.py
132
rare/app.py
|
@ -1,132 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from argparse import Namespace
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import requests.exceptions
|
||||
from PyQt5.QtCore import QThreadPool, QTimer, pyqtSlot, Qt
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
from requests import HTTPError
|
||||
|
||||
from rare.components.dialogs.launch_dialog import LaunchDialog
|
||||
from rare.components.main_window import MainWindow
|
||||
from rare.shared import RareCore
|
||||
from rare.utils import config_helper, paths
|
||||
from rare.widgets.rare_app import RareApp, RareAppException
|
||||
|
||||
logger = logging.getLogger("Rare")
|
||||
|
||||
|
||||
class RareException(RareAppException):
|
||||
def __init__(self, parent=None):
|
||||
super(RareException, self).__init__(parent=parent)
|
||||
|
||||
def _handler(self, exc_type, exc_value, exc_tb) -> bool:
|
||||
if exc_type == HTTPError:
|
||||
try:
|
||||
if RareCore.instance() is not None:
|
||||
if RareCore.instance().core().login():
|
||||
return True
|
||||
raise ValueError
|
||||
except Exception as e:
|
||||
logger.fatal(str(e))
|
||||
QMessageBox.warning(None, "Error", self.tr("Failed to login"))
|
||||
QApplication.exit(1)
|
||||
return False
|
||||
|
||||
|
||||
class Rare(RareApp):
|
||||
def __init__(self, args: Namespace):
|
||||
log_file = "Rare_{0}.log"
|
||||
super(Rare, self).__init__(args, log_file)
|
||||
self._hook.deleteLater()
|
||||
self._hook = RareException(self)
|
||||
self.rcore = RareCore(args=args)
|
||||
self.args = RareCore.instance().args()
|
||||
self.signals = RareCore.instance().signals()
|
||||
self.core = RareCore.instance().core()
|
||||
|
||||
config_helper.init_config_handler(self.core)
|
||||
|
||||
lang = self.settings.value("language", self.core.language_code, type=str)
|
||||
self.load_translator(lang)
|
||||
|
||||
# set Application name for settings
|
||||
self.main_window: Optional[MainWindow] = None
|
||||
self.launch_dialog: Optional[LaunchDialog] = None
|
||||
self.timer: Optional[QTimer] = None
|
||||
|
||||
# launch app
|
||||
self.launch_dialog = LaunchDialog(parent=None)
|
||||
self.launch_dialog.quit_app.connect(self.launch_dialog.close)
|
||||
self.launch_dialog.quit_app.connect(lambda x: sys.exit(x))
|
||||
self.launch_dialog.start_app.connect(self.start_app)
|
||||
self.launch_dialog.start_app.connect(self.launch_dialog.close)
|
||||
|
||||
self.launch_dialog.login()
|
||||
|
||||
def poke_timer(self):
|
||||
dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]).replace(tzinfo=timezone.utc)
|
||||
dt_now = datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||
td = abs(dt_exp - dt_now)
|
||||
self.timer.start(int(td.total_seconds() - 60) * 1000)
|
||||
logger.info(f"Renewed session expires at {self.core.lgd.userdata['expires_at']}")
|
||||
|
||||
def re_login(self):
|
||||
logger.info("Session expires shortly. Renew session")
|
||||
try:
|
||||
self.core.login()
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.timer.start(3000) # try again if no connection
|
||||
return
|
||||
self.poke_timer()
|
||||
|
||||
def start_app(self):
|
||||
self.timer = QTimer()
|
||||
self.timer.timeout.connect(self.re_login)
|
||||
self.poke_timer()
|
||||
|
||||
self.main_window = MainWindow()
|
||||
self.main_window.exit_app.connect(self.on_exit_app)
|
||||
|
||||
if not self.args.silent:
|
||||
self.main_window.show()
|
||||
|
||||
if self.args.test_start:
|
||||
self.main_window.close()
|
||||
self.main_window = None
|
||||
self.on_exit_app(0)
|
||||
|
||||
@pyqtSlot()
|
||||
@pyqtSlot(int)
|
||||
def on_exit_app(self, exit_code=0):
|
||||
threadpool = QThreadPool.globalInstance()
|
||||
threadpool.waitForDone()
|
||||
if self.timer is not None:
|
||||
self.timer.stop()
|
||||
self.timer.deleteLater()
|
||||
self.timer = None
|
||||
self.rcore.deleteLater()
|
||||
del self.rcore
|
||||
self.processEvents()
|
||||
shutil.rmtree(paths.tmp_dir())
|
||||
os.makedirs(paths.tmp_dir())
|
||||
|
||||
self.exit(exit_code)
|
||||
|
||||
|
||||
def start(args):
|
||||
while True:
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
app = Rare(args)
|
||||
exit_code = app.exec_()
|
||||
# if not restart
|
||||
# restart app
|
||||
del app
|
||||
if exit_code != -133742:
|
||||
break
|
|
@ -0,0 +1,131 @@
|
|||
import os
|
||||
import shutil
|
||||
from argparse import Namespace
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import requests.exceptions
|
||||
from PyQt5.QtCore import QThreadPool, QTimer, pyqtSlot, Qt
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
from requests import HTTPError
|
||||
|
||||
from rare.components.dialogs.launch_dialog import LaunchDialog
|
||||
from rare.components.main_window import MainWindow
|
||||
from rare.shared import RareCore
|
||||
from rare.utils import config_helper, paths
|
||||
from rare.utils.misc import ExitCodes
|
||||
from rare.widgets.rare_app import RareApp, RareAppException
|
||||
|
||||
|
||||
class RareException(RareAppException):
|
||||
def __init__(self, parent=None):
|
||||
super(RareException, self).__init__(parent=parent)
|
||||
|
||||
def _handler(self, exc_type, exc_value, exc_tb) -> bool:
|
||||
if exc_type == HTTPError:
|
||||
try:
|
||||
if RareCore.instance() is not None:
|
||||
if RareCore.instance().core().login():
|
||||
return True
|
||||
raise ValueError
|
||||
except Exception as e:
|
||||
self.logger.fatal(str(e))
|
||||
QMessageBox.warning(None, "Error", self.tr("Failed to login"))
|
||||
QApplication.exit(1)
|
||||
return False
|
||||
|
||||
|
||||
class Rare(RareApp):
|
||||
def __init__(self, args: Namespace):
|
||||
super(Rare, self).__init__(args, f"{type(self).__name__}_{{0}}.log")
|
||||
self._hook.deleteLater()
|
||||
self._hook = RareException(self)
|
||||
self.rcore = RareCore(args=args)
|
||||
self.args = RareCore.instance().args()
|
||||
self.signals = RareCore.instance().signals()
|
||||
self.core = RareCore.instance().core()
|
||||
|
||||
config_helper.init_config_handler(self.core)
|
||||
|
||||
lang = self.settings.value("language", self.core.language_code, type=str)
|
||||
self.load_translator(lang)
|
||||
|
||||
# set Application name for settings
|
||||
self.main_window: Optional[MainWindow] = None
|
||||
self.launch_dialog: Optional[LaunchDialog] = None
|
||||
self.timer: Optional[QTimer] = None
|
||||
|
||||
# This launches the application after it has been instantiated.
|
||||
# The timer's signal will be serviced once we call `exec()` on the application
|
||||
QTimer.singleShot(0, self.launch_app)
|
||||
|
||||
def poke_timer(self):
|
||||
dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]).replace(tzinfo=timezone.utc)
|
||||
dt_now = datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||
td = abs(dt_exp - dt_now)
|
||||
self.timer.start(int(td.total_seconds() - 60) * 1000)
|
||||
self.logger.info(f"Renewed session expires at {self.core.lgd.userdata['expires_at']}")
|
||||
|
||||
def re_login(self):
|
||||
self.logger.info("Session expires shortly. Renew session")
|
||||
try:
|
||||
self.core.login()
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.timer.start(3000) # try again if no connection
|
||||
return
|
||||
self.poke_timer()
|
||||
|
||||
@pyqtSlot()
|
||||
def launch_app(self):
|
||||
self.launch_dialog = LaunchDialog(parent=None)
|
||||
self.launch_dialog.exit_app.connect(self.launch_dialog.close)
|
||||
self.launch_dialog.exit_app.connect(self.__on_exit_app)
|
||||
self.launch_dialog.start_app.connect(self.start_app)
|
||||
self.launch_dialog.start_app.connect(self.launch_dialog.close)
|
||||
self.launch_dialog.login()
|
||||
|
||||
@pyqtSlot()
|
||||
def start_app(self):
|
||||
self.timer = QTimer()
|
||||
self.timer.timeout.connect(self.re_login)
|
||||
self.poke_timer()
|
||||
|
||||
self.main_window = MainWindow()
|
||||
self.main_window.exit_app.connect(self.__on_exit_app)
|
||||
|
||||
if not self.args.silent:
|
||||
self.main_window.show()
|
||||
|
||||
if self.args.test_start:
|
||||
self.main_window.close()
|
||||
self.main_window = None
|
||||
self.__on_exit_app(0)
|
||||
|
||||
@pyqtSlot()
|
||||
@pyqtSlot(int)
|
||||
def __on_exit_app(self, exit_code=0):
|
||||
threadpool = QThreadPool.globalInstance()
|
||||
threadpool.waitForDone()
|
||||
if self.timer is not None:
|
||||
self.timer.stop()
|
||||
self.timer.deleteLater()
|
||||
self.timer = None
|
||||
self.rcore.deleteLater()
|
||||
del self.rcore
|
||||
self.processEvents()
|
||||
shutil.rmtree(paths.tmp_dir())
|
||||
os.makedirs(paths.tmp_dir())
|
||||
|
||||
self.exit(exit_code)
|
||||
|
||||
|
||||
def start(args) -> int:
|
||||
while True:
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
app = Rare(args)
|
||||
exit_code = app.exec()
|
||||
del app
|
||||
if exit_code != ExitCodes.LOGOUT:
|
||||
break
|
||||
return exit_code
|
|
@ -164,9 +164,9 @@ class InstallDialog(QDialog):
|
|||
|
||||
self.non_reload_option_changed("shortcut")
|
||||
|
||||
self.ui.cancel_button.clicked.connect(self.cancel_clicked)
|
||||
self.ui.verify_button.clicked.connect(self.verify_clicked)
|
||||
self.ui.install_button.clicked.connect(self.install_clicked)
|
||||
self.ui.cancel_button.clicked.connect(self.__on_cancel)
|
||||
self.ui.verify_button.clicked.connect(self.__on_verify)
|
||||
self.ui.install_button.clicked.connect(self.__on_install)
|
||||
|
||||
self.advanced.ui.install_prereqs_check.setChecked(self.options.install_prereqs)
|
||||
|
||||
|
@ -184,7 +184,7 @@ class InstallDialog(QDialog):
|
|||
self.get_download_info()
|
||||
else:
|
||||
self.setModal(True)
|
||||
self.verify_clicked()
|
||||
self.__on_verify()
|
||||
self.show()
|
||||
|
||||
@pyqtSlot(str)
|
||||
|
@ -205,7 +205,7 @@ class InstallDialog(QDialog):
|
|||
layout = QVBoxLayout(widget)
|
||||
layout.setSpacing(0)
|
||||
for tag, info in sdl_data.items():
|
||||
cb = TagCheckBox(info["name"], info["tags"])
|
||||
cb = TagCheckBox(info["name"], info["description"], info["tags"])
|
||||
if tag == "__required":
|
||||
cb.setChecked(True)
|
||||
cb.setDisabled(True)
|
||||
|
@ -247,7 +247,7 @@ class InstallDialog(QDialog):
|
|||
self.worker_running = True
|
||||
self.threadpool.start(info_worker)
|
||||
|
||||
def verify_clicked(self):
|
||||
def __on_verify(self):
|
||||
self.error_box()
|
||||
message = self.tr("Updating...")
|
||||
self.ui.download_size_text.setText(message)
|
||||
|
@ -282,7 +282,7 @@ class InstallDialog(QDialog):
|
|||
elif option == "install_prereqs":
|
||||
self.options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked()
|
||||
|
||||
def cancel_clicked(self):
|
||||
def __on_cancel(self):
|
||||
if self.config_tags is not None:
|
||||
config_helper.add_option(self.rgame.app_name, 'install_tags', ','.join(self.config_tags))
|
||||
else:
|
||||
|
@ -293,10 +293,20 @@ class InstallDialog(QDialog):
|
|||
self.reject_close = False
|
||||
self.close()
|
||||
|
||||
def install_clicked(self):
|
||||
def __on_install(self):
|
||||
self.reject_close = False
|
||||
self.close()
|
||||
|
||||
@staticmethod
|
||||
def same_platform(download: InstallDownloadModel) -> bool:
|
||||
platform = download.igame.platform
|
||||
if pf.system() == "Windows":
|
||||
return platform == "Windows" or platform == "Win32"
|
||||
elif pf.system() == "Darwin":
|
||||
return platform == "Mac"
|
||||
else:
|
||||
return False
|
||||
|
||||
@pyqtSlot(InstallDownloadModel)
|
||||
def on_worker_result(self, download: InstallDownloadModel):
|
||||
self.__download = download
|
||||
|
@ -314,17 +324,18 @@ class InstallDialog(QDialog):
|
|||
self.ui.install_size_text.setStyleSheet("font-style: normal; font-weight: bold")
|
||||
self.ui.verify_button.setEnabled(self.options_changed)
|
||||
self.ui.cancel_button.setEnabled(True)
|
||||
if pf.system() == "Windows" or ArgumentsSingleton().debug:
|
||||
if download.igame.prereq_info and not download.igame.prereq_info.get("installed", False):
|
||||
self.advanced.ui.install_prereqs_check.setEnabled(True)
|
||||
self.advanced.ui.install_prereqs_label.setEnabled(True)
|
||||
self.advanced.ui.install_prereqs_check.setChecked(True)
|
||||
prereq_name = download.igame.prereq_info.get("name", "")
|
||||
prereq_path = os.path.split(download.igame.prereq_info.get("path", ""))[-1]
|
||||
prereq_desc = prereq_name if prereq_name else prereq_path
|
||||
self.advanced.ui.install_prereqs_check.setText(
|
||||
self.tr("Also install: {}").format(prereq_desc)
|
||||
)
|
||||
has_prereqs = bool(download.igame.prereq_info) and not download.igame.prereq_info.get("installed", False)
|
||||
if has_prereqs:
|
||||
prereq_name = download.igame.prereq_info.get("name", "")
|
||||
prereq_path = os.path.split(download.igame.prereq_info.get("path", ""))[-1]
|
||||
prereq_desc = prereq_name if prereq_name else prereq_path
|
||||
self.advanced.ui.install_prereqs_check.setText(self.tr("Also install: {}").format(prereq_desc))
|
||||
else:
|
||||
self.advanced.ui.install_prereqs_check.setText("")
|
||||
# Offer to install prerequisites only on same platforms
|
||||
self.advanced.ui.install_prereqs_label.setEnabled(has_prereqs)
|
||||
self.advanced.ui.install_prereqs_check.setEnabled(has_prereqs)
|
||||
self.advanced.ui.install_prereqs_check.setChecked(has_prereqs and self.same_platform(download))
|
||||
if self.options.silent:
|
||||
self.close()
|
||||
|
||||
|
@ -361,13 +372,15 @@ class InstallDialog(QDialog):
|
|||
|
||||
def keyPressEvent(self, e: QKeyEvent) -> None:
|
||||
if e.key() == Qt.Key_Escape:
|
||||
self.cancel_clicked()
|
||||
e.accept()
|
||||
self.__on_cancel()
|
||||
|
||||
|
||||
class TagCheckBox(QCheckBox):
|
||||
def __init__(self, text, tags: List[str], parent=None):
|
||||
def __init__(self, text, desc, tags: List[str], parent=None):
|
||||
super(TagCheckBox, self).__init__(parent)
|
||||
self.setText(text)
|
||||
self.setToolTip(desc)
|
||||
self.tags = tags
|
||||
|
||||
def isChecked(self) -> Union[bool, List[str]]:
|
||||
|
|
|
@ -14,7 +14,7 @@ logger = getLogger("LaunchDialog")
|
|||
|
||||
|
||||
class LaunchDialog(QDialog):
|
||||
quit_app = pyqtSignal(int)
|
||||
exit_app = pyqtSignal(int)
|
||||
start_app = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
@ -33,7 +33,7 @@ class LaunchDialog(QDialog):
|
|||
self.ui = Ui_LaunchDialog()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.accept_close = False
|
||||
self.reject_close = True
|
||||
|
||||
self.progress_info = ElideLabel(parent=self)
|
||||
self.progress_info.setFixedHeight(False)
|
||||
|
@ -74,7 +74,7 @@ class LaunchDialog(QDialog):
|
|||
self.show()
|
||||
self.launch()
|
||||
else:
|
||||
self.quit_app.emit(0)
|
||||
self.exit_app.emit(0)
|
||||
|
||||
def launch(self):
|
||||
self.progress_info.setText(self.tr("Preparing Rare"))
|
||||
|
@ -87,11 +87,9 @@ class LaunchDialog(QDialog):
|
|||
|
||||
def __on_completed(self):
|
||||
logger.info("App starting")
|
||||
self.accept_close = True
|
||||
self.reject_close = False
|
||||
self.start_app.emit()
|
||||
|
||||
def reject(self) -> None:
|
||||
if self.accept_close:
|
||||
if not self.reject_close:
|
||||
super(LaunchDialog, self).reject()
|
||||
else:
|
||||
pass
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtWidgets import QLayout, QDialog, QMessageBox, QFrame
|
||||
from legendary.core import LegendaryCore
|
||||
|
||||
|
@ -15,13 +14,6 @@ from .import_login import ImportLogin
|
|||
logger = getLogger("LoginDialog")
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoginPages:
|
||||
landing: int
|
||||
browser: int
|
||||
import_egl: int
|
||||
|
||||
|
||||
class LandingPage(QFrame):
|
||||
def __init__(self, parent=None):
|
||||
super(LandingPage, self).__init__(parent=parent)
|
||||
|
@ -31,8 +23,7 @@ class LandingPage(QFrame):
|
|||
|
||||
|
||||
class LoginDialog(QDialog):
|
||||
logged_in: bool = False
|
||||
pages = LoginPages(landing=0, browser=1, import_egl=2)
|
||||
exit_app: pyqtSignal = pyqtSignal(int)
|
||||
|
||||
def __init__(self, core: LegendaryCore, parent=None):
|
||||
super(LoginDialog, self).__init__(parent=parent)
|
||||
|
@ -51,6 +42,8 @@ class LoginDialog(QDialog):
|
|||
self.ui = Ui_LoginDialog()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.logged_in: bool = False
|
||||
|
||||
self.core = core
|
||||
self.args = ArgumentsSingleton()
|
||||
|
||||
|
@ -59,16 +52,16 @@ class LoginDialog(QDialog):
|
|||
self.ui.login_stack_layout.addWidget(self.login_stack)
|
||||
|
||||
self.landing_page = LandingPage(self.login_stack)
|
||||
self.login_stack.insertWidget(self.pages.landing, self.landing_page)
|
||||
self.login_stack.insertWidget(0, self.landing_page)
|
||||
|
||||
self.browser_page = BrowserLogin(self.core, self.login_stack)
|
||||
self.login_stack.insertWidget(self.pages.browser, self.browser_page)
|
||||
self.login_stack.insertWidget(1, self.browser_page)
|
||||
self.browser_page.success.connect(self.login_successful)
|
||||
self.browser_page.changed.connect(
|
||||
lambda: self.ui.next_button.setEnabled(self.browser_page.is_valid())
|
||||
)
|
||||
self.import_page = ImportLogin(self.core, self.login_stack)
|
||||
self.login_stack.insertWidget(self.pages.import_egl, self.import_page)
|
||||
self.login_stack.insertWidget(2, self.import_page)
|
||||
self.import_page.success.connect(self.login_successful)
|
||||
self.import_page.changed.connect(lambda: self.ui.next_button.setEnabled(self.import_page.is_valid()))
|
||||
|
||||
|
@ -84,34 +77,34 @@ class LoginDialog(QDialog):
|
|||
self.ui.back_button.clicked.connect(self.back_clicked)
|
||||
self.ui.next_button.clicked.connect(self.next_clicked)
|
||||
|
||||
self.login_stack.setCurrentIndex(self.pages.landing)
|
||||
self.login_stack.setCurrentWidget(self.landing_page)
|
||||
|
||||
self.layout().setSizeConstraint(QLayout.SetFixedSize)
|
||||
|
||||
def back_clicked(self):
|
||||
self.ui.back_button.setEnabled(False)
|
||||
self.ui.next_button.setEnabled(True)
|
||||
self.login_stack.slideInIndex(self.pages.landing)
|
||||
self.login_stack.slideInWidget(self.landing_page)
|
||||
|
||||
def browser_radio_clicked(self):
|
||||
self.login_stack.slideInIndex(self.pages.browser)
|
||||
self.login_stack.slideInWidget(self.browser_page)
|
||||
self.ui.back_button.setEnabled(True)
|
||||
self.ui.next_button.setEnabled(False)
|
||||
|
||||
def import_radio_clicked(self):
|
||||
self.login_stack.slideInIndex(self.pages.import_egl)
|
||||
self.login_stack.slideInWidget(self.import_page)
|
||||
self.ui.back_button.setEnabled(True)
|
||||
self.ui.next_button.setEnabled(self.import_page.is_valid())
|
||||
|
||||
def next_clicked(self):
|
||||
if self.login_stack.currentIndex() == self.pages.landing:
|
||||
if self.login_stack.currentWidget() is self.landing_page:
|
||||
if self.landing_page.ui.login_browser_radio.isChecked():
|
||||
self.browser_radio_clicked()
|
||||
if self.landing_page.ui.login_import_radio.isChecked():
|
||||
self.import_radio_clicked()
|
||||
elif self.login_stack.currentIndex() == self.pages.browser:
|
||||
elif self.login_stack.currentWidget() is self.browser_page:
|
||||
self.browser_page.do_login()
|
||||
elif self.login_stack.currentIndex() == self.pages.import_egl:
|
||||
elif self.login_stack.currentWidget() is self.import_page:
|
||||
self.import_page.do_login()
|
||||
|
||||
def login(self):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from PyQt5.QtCore import Qt, pyqtSignal, QCoreApplication
|
||||
from PyQt5.QtGui import QCloseEvent
|
||||
from PyQt5.QtGui import QCloseEvent, QKeyEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog,
|
||||
QLabel,
|
||||
|
@ -75,3 +75,8 @@ class UninstallDialog(QDialog):
|
|||
def __on_cancel(self):
|
||||
self.options.values = (None, None, None)
|
||||
self.close()
|
||||
|
||||
def keyPressEvent(self, e: QKeyEvent) -> None:
|
||||
if e.key() == Qt.Key_Escape:
|
||||
e.accept()
|
||||
self.__on_cancel()
|
||||
|
|
|
@ -44,7 +44,7 @@ class MainWindow(QMainWindow):
|
|||
|
||||
self.setWindowTitle("Rare - GUI for legendary")
|
||||
self.tab_widget = MainTabWidget(self)
|
||||
self.tab_widget.exit_app.connect(self.on_exit_app)
|
||||
self.tab_widget.exit_app.connect(self.__on_exit_app)
|
||||
self.setCentralWidget(self.tab_widget)
|
||||
|
||||
# Set up status bar stuff (jumping through a lot of hoops)
|
||||
|
@ -112,7 +112,7 @@ class MainWindow(QMainWindow):
|
|||
self.timer.start()
|
||||
|
||||
self.tray_icon: TrayIcon = TrayIcon(self)
|
||||
self.tray_icon.exit_app.connect(self.on_exit_app)
|
||||
self.tray_icon.exit_app.connect(self.__on_exit_app)
|
||||
self.tray_icon.show_app.connect(self.show)
|
||||
self.tray_icon.activated.connect(lambda r: self.toggle() if r == self.tray_icon.DoubleClick else None)
|
||||
|
||||
|
@ -203,7 +203,7 @@ class MainWindow(QMainWindow):
|
|||
|
||||
@pyqtSlot()
|
||||
@pyqtSlot(int)
|
||||
def on_exit_app(self, exit_code=0) -> None:
|
||||
def __on_exit_app(self, exit_code=0) -> None:
|
||||
self.__exit_code = exit_code
|
||||
self.close()
|
||||
|
||||
|
@ -221,43 +221,42 @@ class MainWindow(QMainWindow):
|
|||
return
|
||||
|
||||
# FIXME: Move this to RareCore once the download manager is implemented
|
||||
if not self.args.offline:
|
||||
if self.rcore.queue_threadpool.activeThreadCount():
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
self.tr("Quit {}?").format(QApplication.applicationName()),
|
||||
self.tr(
|
||||
"There are currently running operations. "
|
||||
"Rare cannot exit until they are completed.\n\n"
|
||||
"Do you want to clear the queue?"
|
||||
),
|
||||
buttons=(QMessageBox.Yes | QMessageBox.No),
|
||||
defaultButton=QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.rcore.queue_threadpool.clear()
|
||||
for qw in self.rcore.queued_workers():
|
||||
self.rcore.dequeue_worker(qw)
|
||||
self.update_statusbar()
|
||||
if self.rcore.queue_threadpool.activeThreadCount():
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
self.tr("Quit {}?").format(QApplication.applicationName()),
|
||||
self.tr(
|
||||
"There are currently running operations. "
|
||||
"Rare cannot exit until they are completed.\n\n"
|
||||
"Do you want to clear the queue?"
|
||||
),
|
||||
buttons=(QMessageBox.Yes | QMessageBox.No),
|
||||
defaultButton=QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.rcore.queue_threadpool.clear()
|
||||
for qw in self.rcore.queued_workers():
|
||||
self.rcore.dequeue_worker(qw)
|
||||
self.update_statusbar()
|
||||
e.ignore()
|
||||
return
|
||||
elif self.tab_widget.downloads_tab.is_download_active:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
self.tr("Quit {}?").format(QApplication.applicationName()),
|
||||
self.tr(
|
||||
"There is an active download. "
|
||||
"Quitting Rare now will stop the download.\n\n"
|
||||
"Are you sure you want to quit?"
|
||||
),
|
||||
buttons=(QMessageBox.Yes | QMessageBox.No),
|
||||
defaultButton=QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.tab_widget.downloads_tab.stop_download(omit_queue=True)
|
||||
else:
|
||||
e.ignore()
|
||||
return
|
||||
elif self.tab_widget.downloads_tab.is_download_active:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
self.tr("Quit {}?").format(QApplication.applicationName()),
|
||||
self.tr(
|
||||
"There is an active download. "
|
||||
"Quitting Rare now will stop the download.\n\n"
|
||||
"Are you sure you want to quit?"
|
||||
),
|
||||
buttons=(QMessageBox.Yes | QMessageBox.No),
|
||||
defaultButton=QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.tab_widget.downloads_tab.stop_download(omit_queue=True)
|
||||
else:
|
||||
e.ignore()
|
||||
return
|
||||
# FIXME: End of FIXME
|
||||
self.timer.stop()
|
||||
self.tray_icon.deleteLater()
|
||||
|
|
|
@ -2,7 +2,7 @@ from PyQt5.QtCore import QSize, pyqtSignal, pyqtSlot
|
|||
from PyQt5.QtWidgets import QMenu, QTabWidget, QWidget, QWidgetAction, QShortcut, QMessageBox
|
||||
|
||||
from rare.shared import RareCore, LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import icon, ExitCodes
|
||||
from .account import AccountWidget
|
||||
from .downloads import DownloadsTab
|
||||
from .games import GamesTab
|
||||
|
@ -51,7 +51,7 @@ class MainTabWidget(QTabWidget):
|
|||
self.setTabEnabled(button_index, False)
|
||||
|
||||
self.account_widget = AccountWidget(self)
|
||||
self.account_widget.logout.connect(self.logout)
|
||||
self.account_widget.exit_app.connect(self.__on_exit_app)
|
||||
account_action = QWidgetAction(self)
|
||||
account_action.setDefaultWidget(self.account_widget)
|
||||
account_button = TabButtonWidget("mdi.account-circle", "Account", fallback_icon="fa.user")
|
||||
|
@ -93,25 +93,28 @@ class MainTabWidget(QTabWidget):
|
|||
self.tab_bar.setMinimumWidth(self.width())
|
||||
super(MainTabWidget, self).resizeEvent(event)
|
||||
|
||||
@pyqtSlot()
|
||||
def logout(self):
|
||||
@pyqtSlot(int)
|
||||
def __on_exit_app(self, exit_code: int):
|
||||
# FIXME: Don't allow logging out if there are active downloads
|
||||
if self.downloads_tab.is_download_active:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
self.tr("Logout"),
|
||||
self.tr("There are active downloads. Stop them before logging out."),
|
||||
self.tr("Quit") if exit_code == ExitCodes.EXIT else self.tr("Logout"),
|
||||
self.tr("There are active downloads. Stop them before trying to quit."),
|
||||
)
|
||||
return
|
||||
# FIXME: End of FIXME
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
self.tr("Logout"),
|
||||
self.tr("Do you really want to logout <b>{}</b>?").format(self.core.lgd.userdata.get("display_name")),
|
||||
buttons=(QMessageBox.Yes | QMessageBox.No),
|
||||
defaultButton=QMessageBox.No,
|
||||
)
|
||||
if exit_code == ExitCodes.LOGOUT:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
self.tr("Logout"),
|
||||
self.tr("Do you really want to logout <b>{}</b>?").format(self.core.lgd.userdata.get("display_name")),
|
||||
buttons=(QMessageBox.Yes | QMessageBox.No),
|
||||
defaultButton=QMessageBox.No,
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
self.core.lgd.invalidate_userdata()
|
||||
self.exit_app.emit(-133742) # restart exit code
|
||||
if reply == QMessageBox.Yes:
|
||||
self.core.lgd.invalidate_userdata()
|
||||
else:
|
||||
return
|
||||
self.exit_app.emit(exit_code) # restart exit code
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import webbrowser
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox, QLabel, QPushButton
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import icon, ExitCodes
|
||||
|
||||
|
||||
class AccountWidget(QWidget):
|
||||
exit_app: pyqtSignal = pyqtSignal(int)
|
||||
logout: pyqtSignal = pyqtSignal()
|
||||
|
||||
def __init__(self, parent):
|
||||
|
@ -25,11 +26,22 @@ class AccountWidget(QWidget):
|
|||
"https://www.epicgames.com/account/personal?productName=epicgames"
|
||||
)
|
||||
)
|
||||
self.logout_button = QPushButton(self.tr("Logout"))
|
||||
self.logout_button.clicked.connect(self.logout)
|
||||
self.logout_button = QPushButton(self.tr("Logout"), parent=self)
|
||||
self.logout_button.clicked.connect(self.__on_logout)
|
||||
self.quit_button = QPushButton(self.tr("Quit"), parent=self)
|
||||
self.quit_button.clicked.connect(self.__on_quit)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(QLabel(self.tr("Account")))
|
||||
layout.addWidget(QLabel(self.tr("Logged in as <b>{}</b>").format(username)))
|
||||
layout.addWidget(self.open_browser)
|
||||
layout.addWidget(self.logout_button)
|
||||
layout.addWidget(self.quit_button)
|
||||
|
||||
@pyqtSlot()
|
||||
def __on_quit(self):
|
||||
self.exit_app.emit(ExitCodes.EXIT)
|
||||
|
||||
@pyqtSlot()
|
||||
def __on_logout(self):
|
||||
self.exit_app.emit(ExitCodes.LOGOUT)
|
||||
|
|
|
@ -58,6 +58,10 @@ class DlThread(QThread):
|
|||
self.rgame.signals.progress.finish.emit(not result.code == DlResultCode.FINISHED)
|
||||
self.result.emit(result)
|
||||
|
||||
def __status_callback(self, status: UIUpdate):
|
||||
self.progress.emit(status, self.dl_size)
|
||||
self.rgame.signals.progress.update.emit(int(status.progress))
|
||||
|
||||
def run(self):
|
||||
cli = LegendaryCLI(self.core)
|
||||
self.item.download.dlm.logging_queue = cli.logging_queue
|
||||
|
@ -71,9 +75,7 @@ class DlThread(QThread):
|
|||
time.sleep(1)
|
||||
while self.item.download.dlm.is_alive():
|
||||
try:
|
||||
status = self.item.download.dlm.status_queue.get(timeout=1.0)
|
||||
self.rgame.signals.progress.update.emit(int(status.progress))
|
||||
self.progress.emit(status, self.dl_size)
|
||||
self.__status_callback(self.item.download.dlm.status_queue.get(timeout=1.0))
|
||||
except queue.Empty:
|
||||
pass
|
||||
if self.dlm_signals.update:
|
||||
|
|
|
@ -117,6 +117,7 @@ class CloudSaves(QWidget, SideTabContents):
|
|||
logger.warning(str(e))
|
||||
resolver = WineResolver(self.core, self.rgame.raw_save_path, self.rgame.app_name)
|
||||
if not resolver.wine_env.get("WINEPREFIX"):
|
||||
del resolver
|
||||
self.cloud_save_path_edit.setText("")
|
||||
QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
|
||||
return
|
||||
|
@ -145,7 +146,7 @@ class CloudSaves(QWidget, SideTabContents):
|
|||
self,
|
||||
self.tr("Error - {}").format(self.rgame.title),
|
||||
self.tr(
|
||||
"Error while calculating path for <b>{}</b>. Insufficient permisions to create <b>{}</b>"
|
||||
"Error while calculating path for <b>{}</b>. Insufficient permissions to create <b>{}</b>"
|
||||
).format(self.rgame.title, path),
|
||||
)
|
||||
return
|
||||
|
@ -205,7 +206,10 @@ class CloudSaves(QWidget, SideTabContents):
|
|||
self.cloud_ui.cloud_sync.blockSignals(True)
|
||||
self.cloud_ui.cloud_sync.setChecked(self.rgame.auto_sync_saves)
|
||||
self.cloud_ui.cloud_sync.blockSignals(False)
|
||||
|
||||
self.cloud_save_path_edit.setText(self.rgame.save_path if self.rgame.save_path else "")
|
||||
if platform.system() == "Windows" and not self.rgame.save_path:
|
||||
self.compute_save_path()
|
||||
|
||||
def update_game(self, rgame: RareGame):
|
||||
if self.rgame:
|
||||
|
|
|
@ -90,6 +90,7 @@ class GameInfo(QWidget, SideTabContents):
|
|||
}
|
||||
|
||||
# lk: hide unfinished things
|
||||
self.ui.tags_group.setVisible(False)
|
||||
self.ui.requirements_group.setVisible(False)
|
||||
|
||||
@pyqtSlot()
|
||||
|
|
|
@ -4,10 +4,10 @@ from dataclasses import dataclass
|
|||
from enum import IntEnum
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Optional
|
||||
from typing import List, Tuple, Optional, Set
|
||||
|
||||
from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThreadPool, pyqtSlot
|
||||
from PyQt5.QtGui import QStandardItemModel
|
||||
from PyQt5.QtGui import QStandardItemModel, QShowEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QFileDialog,
|
||||
QGroupBox,
|
||||
|
@ -18,12 +18,13 @@ from PyQt5.QtWidgets import (
|
|||
QStackedWidget,
|
||||
QProgressBar,
|
||||
QSizePolicy,
|
||||
QFormLayout,
|
||||
)
|
||||
|
||||
from rare.lgndr.cli import LegendaryCLI
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.lgndr.glue.arguments import LgndrImportGameArgs
|
||||
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
|
||||
from rare.lgndr.glue.monkeys import LgndrIndirectStatus, get_boolean_choice_factory
|
||||
from rare.shared import RareCore
|
||||
from rare.ui.components.tabs.games.integrations.import_group import Ui_ImportGroup
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
|
@ -78,6 +79,7 @@ class ImportWorker(QRunnable):
|
|||
import_force: bool = False
|
||||
):
|
||||
super(ImportWorker, self).__init__()
|
||||
self.setAutoDelete(True)
|
||||
self.signals = ImportWorker.Signals()
|
||||
self.core = core
|
||||
|
||||
|
@ -128,7 +130,7 @@ class ImportWorker(QRunnable):
|
|||
skip_dlcs=not self.import_dlcs,
|
||||
with_dlcs=self.import_dlcs,
|
||||
indirect_status=status,
|
||||
get_boolean_choice=lambda prompt, default=True: self.import_dlcs
|
||||
get_boolean_choice=get_boolean_choice_factory(self.import_dlcs)
|
||||
)
|
||||
cli.import_game(args)
|
||||
return status.success, status.message
|
||||
|
@ -137,7 +139,7 @@ class ImportWorker(QRunnable):
|
|||
class AppNameCompleter(QCompleter):
|
||||
activated = pyqtSignal(str)
|
||||
|
||||
def __init__(self, app_names: List, parent=None):
|
||||
def __init__(self, app_names: Set[Tuple[str, str]], parent=None):
|
||||
super(AppNameCompleter, self).__init__(parent)
|
||||
# pylint: disable=E1136
|
||||
super(AppNameCompleter, self).activated[QModelIndex].connect(self.__activated_idx)
|
||||
|
@ -183,8 +185,11 @@ class ImportGroup(QGroupBox):
|
|||
self.rcore = RareCore.instance()
|
||||
self.core = RareCore.instance().core()
|
||||
|
||||
self.app_name_list = [rgame.app_name for rgame in self.rcore.games]
|
||||
self.install_dir_list = [rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc]
|
||||
self.worker: Optional[ImportWorker] = None
|
||||
self.threadpool = QThreadPool.globalInstance()
|
||||
|
||||
self.__app_names: Set[str] = set()
|
||||
self.__install_dirs: Set[str] = set()
|
||||
|
||||
self.path_edit = PathEdit(
|
||||
self.core.get_default_install_dir(),
|
||||
|
@ -193,23 +198,25 @@ class ImportGroup(QGroupBox):
|
|||
parent=self,
|
||||
)
|
||||
self.path_edit.textChanged.connect(self.path_changed)
|
||||
self.ui.path_edit_layout.addWidget(self.path_edit)
|
||||
self.ui.import_layout.setWidget(
|
||||
self.ui.import_layout.indexOf(self.ui.path_edit_label), QFormLayout.FieldRole, self.path_edit
|
||||
)
|
||||
|
||||
self.app_name_edit = IndicatorLineEdit(
|
||||
placeholder=self.tr("Use in case the app name was not found automatically"),
|
||||
completer=AppNameCompleter(
|
||||
app_names=[(rgame.app_name, rgame.app_title) for rgame in self.rcore.games],
|
||||
),
|
||||
edit_func=self.app_name_edit_callback,
|
||||
parent=self,
|
||||
)
|
||||
self.app_name_edit.textChanged.connect(self.app_name_changed)
|
||||
self.ui.app_name_layout.addWidget(self.app_name_edit)
|
||||
self.ui.import_layout.setWidget(
|
||||
self.ui.import_layout.indexOf(self.ui.app_name_label), QFormLayout.FieldRole, self.app_name_edit
|
||||
)
|
||||
|
||||
self.ui.import_folder_check.stateChanged.connect(self.import_folder_changed)
|
||||
self.ui.import_dlcs_check.setEnabled(False)
|
||||
self.ui.import_dlcs_check.stateChanged.connect(self.import_dlcs_changed)
|
||||
|
||||
self.ui.import_button_label.setText("")
|
||||
self.ui.import_button.setEnabled(False)
|
||||
self.ui.import_button.clicked.connect(
|
||||
lambda: self.__import(self.path_edit.text())
|
||||
|
@ -224,9 +231,17 @@ class ImportGroup(QGroupBox):
|
|||
self.info_progress = QProgressBar(self.button_info_stack)
|
||||
self.button_info_stack.addWidget(self.info_label)
|
||||
self.button_info_stack.addWidget(self.info_progress)
|
||||
self.ui.button_info_layout.addWidget(self.button_info_stack)
|
||||
self.ui.button_info_layout.insertWidget(0, self.button_info_stack)
|
||||
|
||||
self.threadpool = QThreadPool.globalInstance()
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.__app_names = {rgame.app_name for rgame in self.rcore.games}
|
||||
self.__install_dirs = {rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc}
|
||||
self.app_name_edit.setCompleter(
|
||||
AppNameCompleter(app_names={(rgame.app_name, rgame.app_title) for rgame in self.rcore.games})
|
||||
)
|
||||
super().showEvent(a0)
|
||||
|
||||
def set_game(self, app_name: str):
|
||||
if app_name:
|
||||
|
@ -238,7 +253,7 @@ class ImportGroup(QGroupBox):
|
|||
if os.path.exists(path):
|
||||
if os.path.exists(os.path.join(path, ".egstore")):
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
elif os.path.basename(path) in self.install_dir_list:
|
||||
elif os.path.basename(path) in self.__install_dirs:
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
else:
|
||||
return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS
|
||||
|
@ -257,7 +272,7 @@ class ImportGroup(QGroupBox):
|
|||
def app_name_edit_callback(self, text) -> Tuple[bool, str, int]:
|
||||
if not text:
|
||||
return False, text, IndicatorReasonsCommon.UNDEFINED
|
||||
if text in self.app_name_list:
|
||||
if text in self.__app_names:
|
||||
return True, text, IndicatorReasonsCommon.VALID
|
||||
else:
|
||||
return False, text, IndicatorReasonsCommon.NOT_INSTALLED
|
||||
|
@ -267,14 +282,12 @@ class ImportGroup(QGroupBox):
|
|||
self.info_label.setText("")
|
||||
self.ui.import_dlcs_check.setCheckState(Qt.Unchecked)
|
||||
self.ui.import_force_check.setCheckState(Qt.Unchecked)
|
||||
if self.app_name_edit.is_valid:
|
||||
self.ui.import_dlcs_check.setEnabled(
|
||||
bool(self.core.get_dlc_for_game(app_name))
|
||||
)
|
||||
self.ui.import_button.setEnabled(self.path_edit.is_valid)
|
||||
else:
|
||||
self.ui.import_dlcs_check.setEnabled(False)
|
||||
self.ui.import_button.setEnabled(False)
|
||||
self.ui.import_dlcs_check.setEnabled(
|
||||
self.app_name_edit.is_valid and bool(self.core.get_dlc_for_game(app_name))
|
||||
)
|
||||
self.ui.import_button.setEnabled(
|
||||
not bool(self.worker) and (self.app_name_edit.is_valid and self.path_edit.is_valid)
|
||||
)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def import_folder_changed(self, state: Qt.CheckState):
|
||||
|
@ -285,17 +298,26 @@ class ImportGroup(QGroupBox):
|
|||
state
|
||||
or (self.app_name_edit.is_valid and bool(self.core.get_dlc_for_game(self.app_name_edit.text())))
|
||||
)
|
||||
self.ui.import_button.setEnabled(state or (not state and self.app_name_edit.is_valid))
|
||||
self.ui.import_button.setEnabled(
|
||||
not bool(self.worker) and (state or (not state and self.app_name_edit.is_valid))
|
||||
)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def import_dlcs_changed(self, state: Qt.CheckState):
|
||||
self.ui.import_button.setEnabled(self.ui.import_folder_check.isChecked() or self.app_name_edit.is_valid)
|
||||
self.ui.import_button.setEnabled(
|
||||
not bool(self.worker) and (self.ui.import_folder_check.isChecked() or self.app_name_edit.is_valid)
|
||||
)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def __import(self, path: Optional[str] = None):
|
||||
self.ui.import_button.setDisabled(True)
|
||||
self.info_label.setText(self.tr("Status: Importing games"))
|
||||
self.info_progress.setValue(0)
|
||||
self.button_info_stack.setCurrentWidget(self.info_progress)
|
||||
|
||||
if not path:
|
||||
path = self.path_edit.text()
|
||||
worker = ImportWorker(
|
||||
self.worker = ImportWorker(
|
||||
self.core,
|
||||
path,
|
||||
self.app_name_edit.text(),
|
||||
|
@ -303,12 +325,9 @@ class ImportGroup(QGroupBox):
|
|||
self.ui.import_dlcs_check.isChecked(),
|
||||
self.ui.import_force_check.isChecked()
|
||||
)
|
||||
worker.signals.result.connect(self.__on_import_result)
|
||||
worker.signals.progress.connect(self.__on_import_progress)
|
||||
self.threadpool.start(worker)
|
||||
self.button_info_stack.setCurrentWidget(self.info_progress)
|
||||
self.info_label.setText(self.tr("Importing games"))
|
||||
self.ui.import_button.setDisabled(True)
|
||||
self.worker.signals.progress.connect(self.__on_import_progress)
|
||||
self.worker.signals.result.connect(self.__on_import_result)
|
||||
self.threadpool.start(self.worker)
|
||||
|
||||
@pyqtSlot(ImportedGame, int)
|
||||
def __on_import_progress(self, imported: ImportedGame, progress: int):
|
||||
|
@ -322,6 +341,7 @@ class ImportGroup(QGroupBox):
|
|||
|
||||
@pyqtSlot(list)
|
||||
def __on_import_result(self, result: List[ImportedGame]):
|
||||
self.worker = None
|
||||
self.button_info_stack.setCurrentWidget(self.info_label)
|
||||
if len(result) == 1:
|
||||
res = result[0]
|
||||
|
@ -338,7 +358,7 @@ class ImportGroup(QGroupBox):
|
|||
self.tr("Error: Could not find AppName for <b>{}</b>").format(res.path)
|
||||
)
|
||||
else:
|
||||
self.info_label.setText("")
|
||||
self.info_label.setText(self.tr("Status: Finished importing games"))
|
||||
success = [r for r in result if r.result == ImportResult.SUCCESS]
|
||||
failure = [r for r in result if r.result == ImportResult.FAILED]
|
||||
errored = [r for r in result if r.result == ImportResult.ERROR]
|
||||
|
|
|
@ -19,19 +19,20 @@ def versiontuple(v):
|
|||
return tuple((9, 9, 9)) # It is a beta version and newer
|
||||
|
||||
|
||||
class About(QWidget, Ui_About):
|
||||
class About(QWidget):
|
||||
update_available_ready = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(About, self).__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
self.ui = Ui_About()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.version.setText(f"{__version__} {__codename__}")
|
||||
self.ui.version.setText(f"{__version__} {__codename__}")
|
||||
|
||||
self.update_label.setEnabled(False)
|
||||
self.update_lbl.setEnabled(False)
|
||||
self.open_browser.setVisible(False)
|
||||
self.open_browser.setEnabled(False)
|
||||
self.ui.update_label.setEnabled(False)
|
||||
self.ui.update_lbl.setEnabled(False)
|
||||
self.ui.open_browser.setVisible(False)
|
||||
self.ui.open_browser.setEnabled(False)
|
||||
|
||||
self.manager = QtRequestManager("json")
|
||||
self.manager.get(
|
||||
|
@ -39,7 +40,7 @@ class About(QWidget, Ui_About):
|
|||
self.update_available_finished,
|
||||
)
|
||||
|
||||
self.open_browser.clicked.connect(
|
||||
self.ui.open_browser.clicked.connect(
|
||||
lambda: webbrowser.open("https://github.com/RareDevs/Rare/releases/latest")
|
||||
)
|
||||
|
||||
|
@ -62,11 +63,11 @@ class About(QWidget, Ui_About):
|
|||
|
||||
if self.update_available:
|
||||
logger.info(f"Update available: {__version__} -> {latest_tag}")
|
||||
self.update_lbl.setText("{} -> {}".format(__version__, latest_tag))
|
||||
self.ui.update_lbl.setText("{} -> {}".format(__version__, latest_tag))
|
||||
self.update_available_ready.emit()
|
||||
else:
|
||||
self.update_lbl.setText(self.tr("You have the latest version"))
|
||||
self.update_label.setEnabled(self.update_available)
|
||||
self.update_lbl.setEnabled(self.update_available)
|
||||
self.open_browser.setVisible(self.update_available)
|
||||
self.open_browser.setEnabled(self.update_available)
|
||||
self.ui.update_lbl.setText(self.tr("You have the latest version"))
|
||||
self.ui.update_label.setEnabled(self.update_available)
|
||||
self.ui.update_lbl.setEnabled(self.update_available)
|
||||
self.ui.open_browser.setVisible(self.update_available)
|
||||
self.ui.open_browser.setEnabled(self.update_available)
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton
|
||||
|
||||
from rare.shared import GlobalSignalsSingleton
|
||||
from rare.utils.misc import ExitCodes
|
||||
|
||||
|
||||
class DebugSettings(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(DebugSettings, self).__init__(parent=parent)
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.raise_runtime_exception_button = QPushButton("Raise Exception")
|
||||
layout.addWidget(self.raise_runtime_exception_button)
|
||||
self.raise_runtime_exception_button.clicked.connect(self.raise_exception)
|
||||
self.restart_button = QPushButton("Restart")
|
||||
layout.addWidget(self.restart_button)
|
||||
self.restart_button.clicked.connect(lambda: GlobalSignalsSingleton().application.quit.emit(-133742))
|
||||
self.restart_button.clicked.connect(
|
||||
lambda: GlobalSignalsSingleton().application.quit.emit(ExitCodes.LOGOUT)
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.raise_runtime_exception_button)
|
||||
layout.addWidget(self.restart_button)
|
||||
layout.addStretch(1)
|
||||
|
||||
def raise_exception(self):
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import json
|
||||
import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from argparse import Namespace
|
||||
from logging import getLogger
|
||||
from signal import signal, SIGINT, SIGTERM, strsignal
|
||||
from typing import Union, Optional
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
@ -24,10 +22,9 @@ from rare.widgets.rare_app import RareApp, RareAppException
|
|||
from .console import Console
|
||||
from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError
|
||||
|
||||
logger = logging.getLogger("RareLauncher")
|
||||
|
||||
DETACHED_APP_NAMES = [
|
||||
"0a2d9f6403244d12969e11da6713137b"
|
||||
"0a2d9f6403244d12969e11da6713137b" # Fall Guys
|
||||
"Fortnite"
|
||||
]
|
||||
|
||||
|
||||
|
@ -40,6 +37,7 @@ class PreLaunchThread(QRunnable):
|
|||
|
||||
def __init__(self, core: LegendaryCore, args: InitArgs, rgame: RareGameSlim, sync_action=None):
|
||||
super(PreLaunchThread, self).__init__()
|
||||
self.logger = getLogger(type(self).__name__)
|
||||
self.core = core
|
||||
self.signals = self.Signals()
|
||||
self.args = args
|
||||
|
@ -47,36 +45,36 @@ class PreLaunchThread(QRunnable):
|
|||
self.sync_action = sync_action
|
||||
|
||||
def run(self) -> None:
|
||||
logger.info(f"Sync action: {self.sync_action}")
|
||||
self.logger.info(f"Sync action: {self.sync_action}")
|
||||
if self.sync_action == CloudSaveDialog.UPLOAD:
|
||||
self.rgame.upload_saves(False)
|
||||
elif self.sync_action == CloudSaveDialog.DOWNLOAD:
|
||||
self.rgame.download_saves(False)
|
||||
else:
|
||||
logger.info("No sync action")
|
||||
self.logger.info("No sync action")
|
||||
|
||||
args = self.prepare_launch(self.args)
|
||||
if not args:
|
||||
return
|
||||
self.signals.ready_to_launch.emit(args)
|
||||
|
||||
def prepare_launch(self, args: InitArgs) -> Union[LaunchArgs, None]:
|
||||
def prepare_launch(self, args: InitArgs) -> Optional[LaunchArgs]:
|
||||
try:
|
||||
args = get_launch_args(self.core, args)
|
||||
launch_args = get_launch_args(self.core, args)
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(str(e))
|
||||
return None
|
||||
if not args:
|
||||
if not launch_args:
|
||||
return None
|
||||
|
||||
if args.pre_launch_command:
|
||||
if launch_args.pre_launch_command:
|
||||
proc = get_configured_process()
|
||||
proc.setProcessEnvironment(args.env)
|
||||
proc.setProcessEnvironment(launch_args.environment)
|
||||
self.signals.started_pre_launch_command.emit()
|
||||
proc.start(args.pre_launch_command[0], args.pre_launch_command[1:])
|
||||
if args.pre_launch_wait:
|
||||
proc.start(launch_args.pre_launch_command[0], launch_args.pre_launch_command[1:])
|
||||
if launch_args.pre_launch_wait:
|
||||
proc.waitForFinished(-1)
|
||||
return args
|
||||
return launch_args
|
||||
|
||||
|
||||
class SyncCheckWorker(QRunnable):
|
||||
|
@ -121,57 +119,63 @@ class RareLauncher(RareApp):
|
|||
exit_app = pyqtSignal()
|
||||
|
||||
def __init__(self, args: InitArgs):
|
||||
log_file = f"Rare_Launcher_{args.app_name}" + "_{0}.log"
|
||||
super(RareLauncher, self).__init__(args, log_file)
|
||||
super(RareLauncher, self).__init__(args, f"{type(self).__name__}_{args.app_name}_{{0}}.log")
|
||||
self.socket: Optional[QLocalSocket] = None
|
||||
self.console: Optional[Console] = None
|
||||
self.game_process: QProcess = QProcess(self)
|
||||
self.server: QLocalServer = QLocalServer(self)
|
||||
|
||||
self._hook.deleteLater()
|
||||
self._hook = RareLauncherException(self, args, self)
|
||||
self.logger = getLogger(f"Launcher_{args.app_name}")
|
||||
|
||||
self.success: bool = True
|
||||
self.success: bool = False
|
||||
self.no_sync_on_exit = False
|
||||
self.args = args
|
||||
self.core = LegendaryCore()
|
||||
self.rgame = RareGameSlim(self.core, self.core.get_game(args.app_name))
|
||||
game = self.core.get_game(args.app_name)
|
||||
if not game:
|
||||
self.logger.error(f"Game {args.app_name} not found. Exiting")
|
||||
return
|
||||
self.rgame = RareGameSlim(self.core, game)
|
||||
|
||||
lang = self.settings.value("language", self.core.language_code, type=str)
|
||||
self.load_translator(lang)
|
||||
|
||||
self.console: Optional[Console] = None
|
||||
if QSettings().value("show_console", False, bool):
|
||||
self.console = Console()
|
||||
self.console.show()
|
||||
|
||||
self.game_process: QProcess = QProcess(self)
|
||||
self.game_process.finished.connect(self.game_finished)
|
||||
self.game_process.errorOccurred.connect(
|
||||
lambda err: self.error_occurred(self.game_process.errorString()))
|
||||
self.game_process.finished.connect(self.__process_finished)
|
||||
self.game_process.errorOccurred.connect(self.__process_errored)
|
||||
if self.console:
|
||||
self.game_process.readyReadStandardOutput.connect(
|
||||
lambda: self.console.log(
|
||||
self.game_process.readAllStandardOutput().data().decode("utf-8", "ignore")
|
||||
)
|
||||
)
|
||||
self.game_process.readyReadStandardError.connect(
|
||||
lambda: self.console.error(
|
||||
self.game_process.readAllStandardError().data().decode("utf-8", "ignore")
|
||||
)
|
||||
)
|
||||
self.game_process.readyReadStandardOutput.connect(self.__proc_log_stdout)
|
||||
self.game_process.readyReadStandardError.connect(self.__proc_log_stderr)
|
||||
self.console.term.connect(self.__proc_term)
|
||||
self.console.kill.connect(self.__proc_kill)
|
||||
|
||||
self.socket: Optional[QLocalSocket] = None
|
||||
self.server: QLocalServer = QLocalServer(self)
|
||||
ret = self.server.listen(f"rare_{args.app_name}")
|
||||
if not ret:
|
||||
self.logger.error(self.server.errorString())
|
||||
self.logger.info("Server is running")
|
||||
self.server.close()
|
||||
self.success = False
|
||||
return
|
||||
self.server.newConnection.connect(self.new_server_connection)
|
||||
|
||||
self.success = True
|
||||
self.start_time = time.time()
|
||||
|
||||
@pyqtSlot()
|
||||
def __proc_log_stdout(self):
|
||||
self.console.log(
|
||||
self.game_process.readAllStandardOutput().data().decode("utf-8", "ignore")
|
||||
)
|
||||
|
||||
@pyqtSlot()
|
||||
def __proc_log_stderr(self):
|
||||
self.console.error(
|
||||
self.game_process.readAllStandardError().data().decode("utf-8", "ignore")
|
||||
)
|
||||
|
||||
@pyqtSlot()
|
||||
def __proc_term(self):
|
||||
self.game_process.terminate()
|
||||
|
@ -215,16 +219,17 @@ class RareLauncher(RareApp):
|
|||
|
||||
if action == CloudSaveDialog.UPLOAD:
|
||||
if self.console:
|
||||
self.console.log("upload saves...")
|
||||
self.console.log("Uploading saves...")
|
||||
self.rgame.upload_saves()
|
||||
elif action == CloudSaveDialog.DOWNLOAD:
|
||||
if self.console:
|
||||
self.console.log("Download saves...")
|
||||
self.console.log("Downloading saves...")
|
||||
self.rgame.download_saves()
|
||||
else:
|
||||
self.on_exit(exit_code)
|
||||
|
||||
def game_finished(self, exit_code):
|
||||
@pyqtSlot(int, QProcess.ExitStatus)
|
||||
def __process_finished(self, exit_code: int, exit_status: QProcess.ExitStatus):
|
||||
self.logger.info("Game finished")
|
||||
|
||||
if self.rgame.auto_sync_saves:
|
||||
|
@ -232,6 +237,10 @@ class RareLauncher(RareApp):
|
|||
else:
|
||||
self.on_exit(exit_code)
|
||||
|
||||
@pyqtSlot(QProcess.ProcessError)
|
||||
def __process_errored(self, error: QProcess.ProcessError):
|
||||
self.error_occurred(self.game_process.errorString())
|
||||
|
||||
def on_exit(self, exit_code: int):
|
||||
if self.console:
|
||||
self.console.on_process_exit(self.core.get_game(self.rgame.app_name).app_title, exit_code)
|
||||
|
@ -247,13 +256,14 @@ class RareLauncher(RareApp):
|
|||
)
|
||||
self.stop()
|
||||
|
||||
@pyqtSlot(object)
|
||||
def launch_game(self, args: LaunchArgs):
|
||||
# should never happen
|
||||
if not args:
|
||||
self.stop()
|
||||
return
|
||||
if self.console:
|
||||
self.console.set_env(args.env)
|
||||
self.console.set_env(args.environment)
|
||||
self.start_time = time.time()
|
||||
|
||||
if args.is_origin_game:
|
||||
|
@ -261,9 +271,9 @@ class RareLauncher(RareApp):
|
|||
self.stop() # stop because it is no subprocess
|
||||
return
|
||||
|
||||
if args.cwd:
|
||||
self.game_process.setWorkingDirectory(args.cwd)
|
||||
self.game_process.setProcessEnvironment(args.env)
|
||||
if args.working_directory:
|
||||
self.game_process.setWorkingDirectory(args.working_directory)
|
||||
self.game_process.setProcessEnvironment(args.environment)
|
||||
# send start message after process started
|
||||
self.game_process.started.connect(lambda: self.send_message(
|
||||
StateChangedModel(
|
||||
|
@ -273,22 +283,22 @@ class RareLauncher(RareApp):
|
|||
))
|
||||
if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
|
||||
self.game_process.deleteLater()
|
||||
subprocess.Popen([args.executable] + args.args, cwd=args.cwd,
|
||||
env={i: args.env.value(i) for i in args.env.keys()})
|
||||
subprocess.Popen([args.executable] + args.arguments, cwd=args.working_directory,
|
||||
env={i: args.environment.value(i) for i in args.environment.keys()})
|
||||
if self.console:
|
||||
self.console.log("Launching game detached")
|
||||
self.console.log("Launching game as a detached process")
|
||||
self.stop()
|
||||
return
|
||||
if self.args.dry_run:
|
||||
logger.info("Dry run activated")
|
||||
self.logger.info("Dry run activated")
|
||||
if self.console:
|
||||
self.console.log(f"{args.executable} {' '.join(args.args)}")
|
||||
self.console.log(f"{args.executable} {' '.join(args.arguments)}")
|
||||
self.console.log(f"Do not start {self.rgame.app_name}")
|
||||
self.console.accept_close = True
|
||||
print(args.executable, " ".join(args.args))
|
||||
print(args.executable, " ".join(args.arguments))
|
||||
self.stop()
|
||||
return
|
||||
self.game_process.start(args.executable, args.args)
|
||||
self.game_process.start(args.executable, args.arguments)
|
||||
|
||||
def error_occurred(self, error_str: str):
|
||||
self.logger.warning(error_str)
|
||||
|
@ -322,9 +332,9 @@ class RareLauncher(RareApp):
|
|||
self.no_sync_on_exit = True
|
||||
if self.console:
|
||||
if action == CloudSaveDialog.DOWNLOAD:
|
||||
self.console.log("Downloading saves")
|
||||
self.console.log("Downloading saves...")
|
||||
elif action == CloudSaveDialog.UPLOAD:
|
||||
self.console.log("Uloading saves")
|
||||
self.console.log("Uploading saves...")
|
||||
self.start_prepare(action)
|
||||
|
||||
def start(self, args: InitArgs):
|
||||
|
@ -334,11 +344,11 @@ class RareLauncher(RareApp):
|
|||
raise ValueError("You are not logged in")
|
||||
except ValueError:
|
||||
# automatically launch offline if available
|
||||
self.logger.error("Not logged in. Try to launch game offline")
|
||||
self.logger.error("Not logged in. Trying to launch the game in offline mode")
|
||||
args.offline = True
|
||||
|
||||
if not args.offline and self.rgame.auto_sync_saves:
|
||||
logger.info("Start sync worker")
|
||||
self.logger.info("Start sync worker")
|
||||
worker = SyncCheckWorker(self.core, self.rgame)
|
||||
worker.signals.error_occurred.connect(self.error_occurred)
|
||||
worker.signals.sync_state_ready.connect(self.sync_ready)
|
||||
|
@ -352,10 +362,13 @@ class RareLauncher(RareApp):
|
|||
if self.console:
|
||||
self.game_process.readyReadStandardOutput.disconnect()
|
||||
self.game_process.readyReadStandardError.disconnect()
|
||||
self.game_process.finished.disconnect()
|
||||
self.game_process.errorOccurred.disconnect()
|
||||
if self.game_process.receivers(self.game_process.finished):
|
||||
self.game_process.finished.disconnect()
|
||||
if self.game_process.receivers(self.game_process.errorOccurred):
|
||||
self.game_process.errorOccurred.disconnect()
|
||||
except TypeError as e:
|
||||
logger.error(f"Failed to disconnect signals: {e}")
|
||||
self.logger.error(f"Failed to disconnect process signals: {e}")
|
||||
|
||||
self.logger.info("Stopping server")
|
||||
try:
|
||||
self.server.close()
|
||||
|
@ -369,7 +382,7 @@ class RareLauncher(RareApp):
|
|||
self.console.on_process_exit(self.rgame.app_name, 0)
|
||||
|
||||
|
||||
def start_game(args: Namespace):
|
||||
def launch(args: Namespace) -> int:
|
||||
args = InitArgs.from_argparse(args)
|
||||
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
|
@ -381,15 +394,17 @@ def start_game(args: Namespace):
|
|||
# This prevents ghost QLocalSockets, which block the name, which makes it unable to start
|
||||
# No handling for SIGKILL
|
||||
def sighandler(s, frame):
|
||||
logger.info(f"{strsignal(s)} received. Stopping")
|
||||
app.logger.info(f"{strsignal(s)} received. Stopping")
|
||||
app.stop()
|
||||
app.exit(1)
|
||||
signal(SIGINT, sighandler)
|
||||
signal(SIGTERM, sighandler)
|
||||
|
||||
if not app.success:
|
||||
return
|
||||
app.stop()
|
||||
app.exit(1)
|
||||
return 1
|
||||
app.start(args)
|
||||
# app.exit_app.connect(lambda: app.exit(0))
|
||||
|
||||
sys.exit(app.exec_())
|
||||
return app.exec_()
|
|
@ -48,15 +48,15 @@ class Console(QDialog):
|
|||
|
||||
button_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed))
|
||||
|
||||
# self.terminate_button = QPushButton(self.tr("Terminate"))
|
||||
self.terminate_button = QPushButton(self.tr("Terminate"))
|
||||
# self.terminate_button.setVisible(platform.system() == "Windows")
|
||||
# button_layout.addWidget(self.terminate_button)
|
||||
# self.terminate_button.clicked.connect(lambda: self.term.emit())
|
||||
#
|
||||
# self.kill_button = QPushButton(self.tr("Kill"))
|
||||
button_layout.addWidget(self.terminate_button)
|
||||
self.terminate_button.clicked.connect(lambda: self.term.emit())
|
||||
|
||||
self.kill_button = QPushButton(self.tr("Kill"))
|
||||
# self.kill_button.setVisible(platform.system() == "Windows")
|
||||
# button_layout.addWidget(self.kill_button)
|
||||
# self.kill_button.clicked.connect(lambda: self.kill.emit())
|
||||
button_layout.addWidget(self.kill_button)
|
||||
self.kill_button.clicked.connect(lambda: self.kill.emit())
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
|
@ -44,9 +44,9 @@ class InitArgs(Namespace):
|
|||
@dataclass
|
||||
class LaunchArgs:
|
||||
executable: str = ""
|
||||
args: List[str] = None
|
||||
cwd: str = None
|
||||
env: QProcessEnvironment = None
|
||||
arguments: List[str] = None
|
||||
working_directory: str = None
|
||||
environment: QProcessEnvironment = None
|
||||
pre_launch_command: str = ""
|
||||
pre_launch_wait: bool = False
|
||||
is_origin_game: bool = False # only for windows to launch as url
|
||||
|
@ -60,7 +60,7 @@ def get_origin_params(core: LegendaryCore, app_name, offline: bool,
|
|||
origin_uri = core.get_origin_uri(app_name, offline)
|
||||
if platform.system() == "Windows":
|
||||
launch_args.executable = origin_uri
|
||||
launch_args.args = []
|
||||
launch_args.arguments = []
|
||||
# only set it here true, because on linux it is a launch command like every other game
|
||||
launch_args.is_origin_game = True
|
||||
return launch_args
|
||||
|
@ -71,12 +71,20 @@ def get_origin_params(core: LegendaryCore, app_name, offline: bool,
|
|||
command.append(origin_uri)
|
||||
|
||||
env = core.get_app_environment(app_name)
|
||||
launch_args.env = QProcessEnvironment.systemEnvironment()
|
||||
for name, value in env.items():
|
||||
launch_args.env.insert(name, value)
|
||||
launch_args.environment = QProcessEnvironment.systemEnvironment()
|
||||
|
||||
if os.environ.get("container") == "flatpak":
|
||||
flatpak_command = ["flatpak-spawn", "--host"]
|
||||
for name, value in env.items():
|
||||
flatpak_command.append(f"--env={name}={value}")
|
||||
command = flatpak_command + command
|
||||
else:
|
||||
for name, value in env.items():
|
||||
launch_args.environment.insert(name, value)
|
||||
|
||||
launch_args.executable = command[0]
|
||||
launch_args.args = command[1:]
|
||||
launch_args.arguments = command[1:]
|
||||
|
||||
return launch_args
|
||||
|
||||
|
||||
|
@ -100,10 +108,16 @@ def get_game_params(core: LegendaryCore, igame: InstalledGame, args: InitArgs,
|
|||
app_name=igame.app_name, offline=args.offline
|
||||
)
|
||||
|
||||
full_params = list()
|
||||
full_params = []
|
||||
launch_args.environment = QProcessEnvironment.systemEnvironment()
|
||||
|
||||
if os.environ.get("container") == "flatpak":
|
||||
full_params.extend(["flatpak-spawn", "--host"])
|
||||
for name, value in params.environment.items():
|
||||
full_params.append(f"--env={name}={value}")
|
||||
else:
|
||||
for name, value in params.environment.items():
|
||||
launch_args.environment.insert(name, value)
|
||||
|
||||
full_params.extend(params.launch_command)
|
||||
full_params.append(
|
||||
|
@ -114,12 +128,9 @@ def get_game_params(core: LegendaryCore, igame: InstalledGame, args: InitArgs,
|
|||
full_params.extend(params.user_parameters)
|
||||
|
||||
launch_args.executable = full_params[0]
|
||||
launch_args.args = full_params[1:]
|
||||
launch_args.arguments = full_params[1:]
|
||||
launch_args.working_directory = params.working_directory
|
||||
|
||||
launch_args.env = QProcessEnvironment.systemEnvironment()
|
||||
for name, value in params.environment.items():
|
||||
launch_args.env.insert(name, value)
|
||||
launch_args.cwd = params.working_directory
|
||||
return launch_args
|
||||
|
||||
|
|
@ -43,6 +43,9 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
def unlock_installed(func):
|
||||
@functools.wraps(func)
|
||||
def unlock(self, *args, **kwargs):
|
||||
if not self.core.lgd.lock_installed():
|
||||
self.logger.debug("Data is locked, trying to forcufully release it")
|
||||
# self.core.lgd._installed_lock.release(force=True)
|
||||
try:
|
||||
ret = func(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
|
@ -388,7 +391,6 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
|
||||
get_boolean_choice = args.get_boolean_choice_main
|
||||
# def get_boolean_choice(x, default): return True
|
||||
if not self.core.lgd.lock_installed():
|
||||
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
|
||||
'install/import/move applications at a time.')
|
||||
|
@ -429,7 +431,6 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
|
||||
get_boolean_choice = args.get_boolean_choice_handler
|
||||
# def get_boolean_choice(x, default): return True
|
||||
# noinspection PyShadowingBuiltins
|
||||
def print(x): self.logger.info(x) if x else None
|
||||
|
||||
|
|
|
@ -34,6 +34,9 @@ class LegendaryCore(LegendaryCoreReal):
|
|||
def unlock_installed(func):
|
||||
@functools.wraps(func)
|
||||
def unlock(self, *args, **kwargs):
|
||||
if not self.lgd.lock_installed():
|
||||
self.log.debug("Data is locked, trying to forcufully release it")
|
||||
# self.lgd._installed_lock.release(force=True)
|
||||
try:
|
||||
ret = func(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
|
|
|
@ -4,9 +4,10 @@ from typing import Callable, List, Optional, Dict
|
|||
|
||||
from rare.lgndr.glue.monkeys import (
|
||||
LgndrIndirectStatus,
|
||||
GetBooleanChoiceProtocol,
|
||||
get_boolean_choice,
|
||||
verify_stdout,
|
||||
get_boolean_choice_factory,
|
||||
sdl_prompt_factory,
|
||||
verify_stdout_factory,
|
||||
ui_update_factory,
|
||||
DLManagerSignals,
|
||||
)
|
||||
from rare.lgndr.models.downloading import UIUpdate
|
||||
|
@ -33,7 +34,7 @@ class LgndrImportGameArgs:
|
|||
yes: bool = False
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
|
||||
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
get_boolean_choice: Callable[[str, bool], bool] = get_boolean_choice_factory(True)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -44,8 +45,8 @@ class LgndrUninstallGameArgs:
|
|||
yes: bool = False
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
|
||||
get_boolean_choice_main: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
get_boolean_choice_handler: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
get_boolean_choice_main: Callable[[str, bool], bool] = get_boolean_choice_factory(True)
|
||||
get_boolean_choice_handler: Callable[[str, bool], bool] = get_boolean_choice_factory(True)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -53,7 +54,7 @@ class LgndrVerifyGameArgs:
|
|||
app_name: str
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
|
||||
verify_stdout: Callable[[int, int, float, float], None] = verify_stdout
|
||||
verify_stdout: Callable[[int, int, float, float], None] = verify_stdout_factory(None)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -94,14 +95,11 @@ class LgndrInstallGameArgs:
|
|||
yes: bool = True
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
|
||||
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
sdl_prompt: Callable[[str, str], List[str]] = lambda app_name, title: [""]
|
||||
verify_stdout: Callable[[int, int, float, float], None] = verify_stdout
|
||||
get_boolean_choice: Callable[[str, bool], bool] = get_boolean_choice_factory(True)
|
||||
verify_stdout: Callable[[int, int, float, float], None] = verify_stdout_factory(None)
|
||||
|
||||
# def __post_init__(self):
|
||||
# if self.sdl_prompt is None:
|
||||
# self.sdl_prompt: Callable[[str, str], list] = \
|
||||
# lambda app_name, title: self.install_tag if self.install_tag is not None else [""]
|
||||
def __post_init__(self):
|
||||
self.sdl_prompt: Callable[[Dict, str], List[str]] = sdl_prompt_factory(self.install_tag)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -119,8 +117,8 @@ class LgndrInstallGameRealArgs:
|
|||
# Rare: Extra arguments
|
||||
install_prereqs: bool = False
|
||||
indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
|
||||
ui_update: Callable[[UIUpdate], None] = lambda ui: None
|
||||
dlm_signals: DLManagerSignals = DLManagerSignals()
|
||||
ui_update: Callable[[UIUpdate], None] = ui_update_factory(None)
|
||||
dlm_signals: DLManagerSignals = field(default_factory=DLManagerSignals)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
@ -1,20 +1,48 @@
|
|||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, List, Optional, Dict
|
||||
|
||||
from typing_extensions import Protocol
|
||||
from rare.lgndr.models.downloading import UIUpdate
|
||||
|
||||
logger = logging.getLogger("LgndrMonkeys")
|
||||
|
||||
|
||||
class GetBooleanChoiceProtocol(Protocol):
|
||||
def __call__(self, prompt: str, default: bool = ...) -> bool:
|
||||
...
|
||||
def get_boolean_choice_factory(value: bool = True) -> Callable[[str, bool], bool]:
|
||||
def get_boolean_choice(prompt: str, default: bool) -> bool:
|
||||
logger.debug("get_boolean_choice: %s, default: %s, choice: %s", prompt, default, value)
|
||||
return value
|
||||
return get_boolean_choice
|
||||
|
||||
|
||||
def get_boolean_choice(prompt: str, default: bool = True) -> bool:
|
||||
return default
|
||||
def sdl_prompt_factory(install_tag: Optional[List[str]] = None) -> Callable[[Dict, str], List[str]]:
|
||||
def sdl_prompt(sdl_data: Dict, title: str) -> List[str]:
|
||||
logger.debug("sdl_prompt: %s", title)
|
||||
for key in sdl_data.keys():
|
||||
logger.debug("%s: %s %s", key, sdl_data[key]["tags"], sdl_data[key]["name"])
|
||||
tags = install_tag if install_tag is not None else [""]
|
||||
logger.debug("choice: %s, tags: %s", install_tag, tags)
|
||||
return tags
|
||||
return sdl_prompt
|
||||
|
||||
|
||||
def verify_stdout(a0: int, a1: int, a2: float, a3: float) -> None:
|
||||
print(f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r")
|
||||
def verify_stdout_factory(
|
||||
callback: Callable[[int, int, float, float], None] = None
|
||||
) -> Callable[[int, int, float, float], None]:
|
||||
def verify_stdout(a0: int, a1: int, a2: float, a3: float) -> None:
|
||||
if callback is not None and callable(callback):
|
||||
callback(a0, a1, a2, a3)
|
||||
else:
|
||||
logger.info("Verification progress: %d/%d (%.01f%%) [%.1f MiB/s]", a0, a1, a2, a3)
|
||||
return verify_stdout
|
||||
|
||||
|
||||
def ui_update_factory(callback: Callable[[UIUpdate], None] = None) -> Callable[[UIUpdate], None]:
|
||||
def ui_update(status: UIUpdate) -> None:
|
||||
if callback is not None and callable(callback):
|
||||
callback(status)
|
||||
else:
|
||||
logger.info("Installation progress: %s", status)
|
||||
return ui_update
|
||||
|
||||
|
||||
class DLManagerSignals:
|
||||
|
|
133
rare/main.py
Executable file
133
rare/main.py
Executable file
|
@ -0,0 +1,133 @@
|
|||
import multiprocessing
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# If we are on Windows, and we are in a "compiled" GUI application form
|
||||
# stdout (and stderr?) will be None. So to avoid `'NoneType' object has no attribute 'write'`
|
||||
# errors, redirect both of them to devnull
|
||||
if os.name == "nt" and (getattr(sys, "frozen", False) or ("__compiled__" in globals())):
|
||||
# Check if stdout and stderr are None before redirecting
|
||||
# This is useful in the case of test builds that enable console
|
||||
if sys.stdout is None:
|
||||
sys.stdout = open(os.devnull, 'w')
|
||||
if sys.stderr is None:
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
|
||||
os.environ["QT_QPA_PLATFORMTHEME"] = ""
|
||||
|
||||
# fix cx_freeze
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
# insert legendary for installed via pip/setup.py submodule to path
|
||||
# if not __name__ == "__main__":
|
||||
# sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary"))
|
||||
|
||||
# CLI Options
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-V", "--version", action="store_true", help="Shows version and exits"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-S",
|
||||
"--silent",
|
||||
action="store_true",
|
||||
help="Launch Rare in background. Open it from System Tray Icon",
|
||||
)
|
||||
parser.add_argument("--debug", action="store_true", help="Launch in debug mode")
|
||||
parser.add_argument(
|
||||
"--offline", action="store_true", help="Launch Rare in offline mode"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-start", action="store_true", help="Quit immediately after launch"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--desktop-shortcut",
|
||||
action="store_true",
|
||||
dest="desktop_shortcut",
|
||||
help="Use this, if there is no link on desktop to start Rare",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--startmenu-shortcut",
|
||||
action="store_true",
|
||||
dest="startmenu_shortcut",
|
||||
help="Use this, if there is no link in start menu to launch Rare",
|
||||
)
|
||||
subparsers = parser.add_subparsers(title="Commands", dest="subparser")
|
||||
|
||||
launch_minimal_parser = subparsers.add_parser("start", aliases=["launch"])
|
||||
launch_minimal_parser.add_argument("app_name", help="AppName of the game to launch",
|
||||
metavar="<App Name>", action="store")
|
||||
launch_minimal_parser.add_argument("--dry-run", help="Print arguments and exit", action="store_true")
|
||||
launch_minimal_parser.add_argument("--offline", help="Launch game offline",
|
||||
action="store_true")
|
||||
launch_minimal_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='<wine binary>',
|
||||
default=os.environ.get('LGDRY_WINE_BINARY', None),
|
||||
help='Set WINE binary to use to launch the app')
|
||||
launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='<wine pfx path>',
|
||||
default=os.environ.get('LGDRY_WINE_PREFIX', None),
|
||||
help='Set WINE prefix to use')
|
||||
launch_minimal_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves",
|
||||
action="store_true")
|
||||
launch_minimal_parser.add_argument("--skip-update-check", help="Do not check for updates",
|
||||
action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.desktop_shortcut or args.startmenu_shortcut:
|
||||
from rare.utils.paths import create_desktop_link
|
||||
|
||||
if args.desktop_shortcut:
|
||||
create_desktop_link(app_name="rare_shortcut", link_type="desktop")
|
||||
|
||||
if args.startmenu_shortcut:
|
||||
create_desktop_link(app_name="rare_shortcut", link_type="start_menu")
|
||||
|
||||
print("Link created")
|
||||
return 0
|
||||
|
||||
if args.version:
|
||||
from rare import __version__, __codename__
|
||||
print(f"Rare {__version__} Codename: {__codename__}")
|
||||
return 0
|
||||
|
||||
if args.subparser == "start" or args.subparser == "launch":
|
||||
from rare.launcher import launch
|
||||
return launch(args)
|
||||
|
||||
from rare.utils import singleton
|
||||
|
||||
try:
|
||||
# this object only allows one instance per machine
|
||||
|
||||
me = singleton.SingleInstance()
|
||||
except singleton.SingleInstanceException:
|
||||
print("Rare is already running")
|
||||
from rare.utils.paths import lock_file
|
||||
|
||||
with open(lock_file(), "w") as file:
|
||||
file.write("show")
|
||||
file.close()
|
||||
return -1
|
||||
|
||||
from rare.components import start
|
||||
return start(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# run from source
|
||||
# insert raw legendary submodule
|
||||
# sys.path.insert(
|
||||
# 0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
|
||||
# )
|
||||
|
||||
# insert source directory if running `main.py` as python script
|
||||
# Required by AppImage
|
||||
if "__compiled__" not in globals():
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
|
||||
|
||||
sys.exit(main())
|
|
@ -550,7 +550,7 @@ class RareGame(RareGameSlim):
|
|||
|
||||
cmd_line = get_rare_executable()
|
||||
executable, args = cmd_line[0], cmd_line[1:]
|
||||
args.extend(["start", self.app_name])
|
||||
args.extend(["launch", self.app_name])
|
||||
if offline:
|
||||
args.append("--offline")
|
||||
if skip_update_check:
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import os
|
||||
import platform as pf
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Callable, Dict, Tuple
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
|
||||
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
|
@ -33,11 +32,7 @@ class InstallOptionsModel:
|
|||
overlay: bool = False
|
||||
update: bool = False
|
||||
silent: bool = False
|
||||
install_prereqs: bool = pf.system() == "Windows"
|
||||
|
||||
def __post_init__(self):
|
||||
self.sdl_prompt: Callable[[str, str], list] = \
|
||||
lambda app_name, title: self.install_tag if self.install_tag is not None else [""]
|
||||
install_prereqs: bool = False
|
||||
|
||||
def as_install_kwargs(self) -> Dict:
|
||||
return {
|
||||
|
|
|
@ -176,8 +176,8 @@ class RareCore(QObject):
|
|||
del self.__args
|
||||
self.__args = None
|
||||
|
||||
del self.__eos_overlay
|
||||
RareCore.__instance = None
|
||||
|
||||
super(RareCore, self).deleteLater()
|
||||
|
||||
def __validate_install(self, rgame: RareGame):
|
||||
|
|
|
@ -7,7 +7,7 @@ from PyQt5.QtCore import pyqtSignal, QObject
|
|||
from rare.lgndr.cli import LegendaryCLI
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.lgndr.glue.arguments import LgndrVerifyGameArgs
|
||||
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
|
||||
from rare.lgndr.glue.monkeys import LgndrIndirectStatus, verify_stdout_factory
|
||||
from rare.models.game import RareGame
|
||||
from .worker import QueueWorker, QueueWorkerInfo
|
||||
|
||||
|
@ -30,7 +30,7 @@ class VerifyWorker(QueueWorker):
|
|||
self.args = args
|
||||
self.rgame = rgame
|
||||
|
||||
def status_callback(self, num: int, total: int, percentage: float, speed: float):
|
||||
def __status_callback(self, num: int, total: int, percentage: float, speed: float):
|
||||
self.rgame.signals.progress.update.emit(num * 100 // total)
|
||||
self.signals.progress.emit(self.rgame, num, total, percentage, speed)
|
||||
|
||||
|
@ -45,7 +45,9 @@ class VerifyWorker(QueueWorker):
|
|||
cli = LegendaryCLI(self.core)
|
||||
status = LgndrIndirectStatus()
|
||||
args = LgndrVerifyGameArgs(
|
||||
app_name=self.rgame.app_name, indirect_status=status, verify_stdout=self.status_callback
|
||||
app_name=self.rgame.app_name,
|
||||
indirect_status=status,
|
||||
verify_stdout=verify_stdout_factory(self.__status_callback)
|
||||
)
|
||||
|
||||
# lk: first pass, verify with the current manifest
|
||||
|
|
|
@ -38,7 +38,7 @@ class WineResolver(Worker):
|
|||
# pylint: disable=E1136
|
||||
self.signals.result_ready[str].emit("")
|
||||
return
|
||||
if not os.path.exists(self.wine_exec) or not os.path.exists(wine.winepath(self.wine_exec)):
|
||||
if not os.path.exists(self.wine_exec):
|
||||
# pylint: disable=E1136
|
||||
self.signals.result_ready[str].emit("")
|
||||
return
|
||||
|
@ -82,28 +82,39 @@ class OriginWineWorker(QRunnable):
|
|||
wine_env = wine.environ(self.core, rgame.app_name)
|
||||
wine_exec = wine.wine(self.core, rgame.app_name)
|
||||
|
||||
# lk: this is the original way of gettijng the path by parsing "system.reg"
|
||||
wine_prefix = wine.prefix(self.core, rgame.app_name)
|
||||
reg = self.__cache.get(wine_prefix, None) or wine.read_registry("system.reg", wine_prefix)
|
||||
self.__cache[wine_prefix] = reg
|
||||
use_wine = False
|
||||
if not use_wine:
|
||||
# lk: this is the original way of getting the path by parsing "system.reg"
|
||||
wine_prefix = wine.prefix(self.core, rgame.app_name)
|
||||
reg = self.__cache.get(wine_prefix, None) or wine.read_registry("system.reg", wine_prefix)
|
||||
self.__cache[wine_prefix] = reg
|
||||
|
||||
reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node")
|
||||
# lk: split and rejoin the registry path to avoid slash expansion
|
||||
reg_path = "\\\\".join([x for x in reg_path.split("\\") if bool(x)])
|
||||
reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node")
|
||||
# lk: split and rejoin the registry path to avoid slash expansion
|
||||
reg_path = "\\\\".join([x for x in reg_path.split("\\") if bool(x)])
|
||||
|
||||
install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None)
|
||||
|
||||
# lk: this is the alternative way of getting the path by using wine itself
|
||||
# install_dir = wine.query_reg_key(wine_exec, wine_env, f"HKLM\\{reg_path}", reg_key)
|
||||
install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None)
|
||||
else:
|
||||
# lk: this is the alternative way of getting the path by using wine itself
|
||||
install_dir = wine.query_reg_key(wine_exec, wine_env, f"HKLM\\{reg_path}", reg_key)
|
||||
|
||||
if install_dir:
|
||||
logger.debug("Found Wine install directory %s", install_dir)
|
||||
install_dir = wine.convert_to_unix_path(wine_exec, wine_env, install_dir)
|
||||
if install_dir:
|
||||
logger.debug("Found Unix install directory %s", install_dir)
|
||||
else:
|
||||
logger.info("Could not find Unix install directory for %s", rgame.title)
|
||||
else:
|
||||
logger.info("Could not find Wine install directory for %s", rgame.title)
|
||||
|
||||
if install_dir:
|
||||
if os.path.isdir(install_dir):
|
||||
install_size = path_size(install_dir)
|
||||
rgame.set_origin_attributes(install_dir, install_size)
|
||||
logger.debug(f"Found Origin game {rgame.title} ({install_dir}, {format_size(install_size)})")
|
||||
logger.info("Origin game %s (%s, %s)", rgame.title, install_dir, format_size(install_size))
|
||||
else:
|
||||
logger.warning(f"Found Origin game {rgame.title} ({install_dir} does not exist)")
|
||||
logger.info(f"Origin registry worker finished in {time.time() - t}s")
|
||||
logger.warning("Origin game %s (%s does not exist)", rgame.title, install_dir)
|
||||
else:
|
||||
logger.info("Origin game %s is not installed", rgame.title)
|
||||
logger.info("Origin worker finished in %ss", time.time() - t)
|
||||
|
|
|
@ -14,11 +14,48 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
|||
class Ui_GameInfo(object):
|
||||
def setupUi(self, GameInfo):
|
||||
GameInfo.setObjectName("GameInfo")
|
||||
GameInfo.resize(419, 404)
|
||||
GameInfo.resize(600, 404)
|
||||
self.main_layout = QtWidgets.QHBoxLayout(GameInfo)
|
||||
self.main_layout.setObjectName("main_layout")
|
||||
self.left_layout = QtWidgets.QVBoxLayout()
|
||||
self.left_layout.setObjectName("left_layout")
|
||||
self.tags_group = QtWidgets.QGroupBox(GameInfo)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.tags_group.sizePolicy().hasHeightForWidth())
|
||||
self.tags_group.setSizePolicy(sizePolicy)
|
||||
self.tags_group.setObjectName("tags_group")
|
||||
self.tags_layout = QtWidgets.QGridLayout(self.tags_group)
|
||||
self.tags_layout.setHorizontalSpacing(0)
|
||||
self.tags_layout.setObjectName("tags_layout")
|
||||
self.completed_check = QtWidgets.QCheckBox(self.tags_group)
|
||||
self.completed_check.setObjectName("completed_check")
|
||||
self.tags_layout.addWidget(self.completed_check, 3, 0, 1, 2)
|
||||
self.hidden_check = QtWidgets.QCheckBox(self.tags_group)
|
||||
self.hidden_check.setObjectName("hidden_check")
|
||||
self.tags_layout.addWidget(self.hidden_check, 0, 0, 1, 2)
|
||||
self.custom1_edit = QtWidgets.QLineEdit(self.tags_group)
|
||||
self.custom1_edit.setObjectName("custom1_edit")
|
||||
self.tags_layout.addWidget(self.custom1_edit, 4, 1, 1, 1)
|
||||
self.favorites_check = QtWidgets.QCheckBox(self.tags_group)
|
||||
self.favorites_check.setObjectName("favorites_check")
|
||||
self.tags_layout.addWidget(self.favorites_check, 1, 0, 1, 2)
|
||||
self.custom1_check = QtWidgets.QCheckBox(self.tags_group)
|
||||
self.custom1_check.setText("")
|
||||
self.custom1_check.setObjectName("custom1_check")
|
||||
self.tags_layout.addWidget(self.custom1_check, 4, 0, 1, 1)
|
||||
self.backlog_check = QtWidgets.QCheckBox(self.tags_group)
|
||||
self.backlog_check.setObjectName("backlog_check")
|
||||
self.tags_layout.addWidget(self.backlog_check, 2, 0, 1, 2)
|
||||
self.custom2_check = QtWidgets.QCheckBox(self.tags_group)
|
||||
self.custom2_check.setText("")
|
||||
self.custom2_check.setObjectName("custom2_check")
|
||||
self.tags_layout.addWidget(self.custom2_check, 5, 0, 1, 1)
|
||||
self.custom2_edit = QtWidgets.QLineEdit(self.tags_group)
|
||||
self.custom2_edit.setObjectName("custom2_edit")
|
||||
self.tags_layout.addWidget(self.custom2_edit, 5, 1, 1, 1)
|
||||
self.left_layout.addWidget(self.tags_group)
|
||||
self.main_layout.addLayout(self.left_layout)
|
||||
self.right_layout = QtWidgets.QVBoxLayout()
|
||||
self.right_layout.setObjectName("right_layout")
|
||||
|
@ -286,6 +323,11 @@ class Ui_GameInfo(object):
|
|||
def retranslateUi(self, GameInfo):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
GameInfo.setWindowTitle(_translate("GameInfo", "Game Info"))
|
||||
self.tags_group.setTitle(_translate("GameInfo", "Tags"))
|
||||
self.completed_check.setText(_translate("GameInfo", "Completed"))
|
||||
self.hidden_check.setText(_translate("GameInfo", "Hidden"))
|
||||
self.favorites_check.setText(_translate("GameInfo", "Favorites"))
|
||||
self.backlog_check.setText(_translate("GameInfo", "Backlog"))
|
||||
self.lbl_dev.setText(_translate("GameInfo", "Developer"))
|
||||
self.lbl_app_name.setText(_translate("GameInfo", "Application Name"))
|
||||
self.lbl_version.setText(_translate("GameInfo", "Version"))
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>419</width>
|
||||
<width>600</width>
|
||||
<height>404</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
@ -15,7 +15,74 @@
|
|||
</property>
|
||||
<layout class="QHBoxLayout" name="main_layout" stretch="0,1">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="left_layout"/>
|
||||
<layout class="QVBoxLayout" name="left_layout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="tags_group">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Tags</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="tags_layout">
|
||||
<property name="horizontalSpacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="completed_check">
|
||||
<property name="text">
|
||||
<string>Completed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="hidden_check">
|
||||
<property name="text">
|
||||
<string>Hidden</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="custom1_edit"/>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="favorites_check">
|
||||
<property name="text">
|
||||
<string>Favorites</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="custom1_check">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="backlog_check">
|
||||
<property name="text">
|
||||
<string>Backlog</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="custom2_check">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="custom2_edit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="right_layout">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/integrations/import_group.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.15.9
|
||||
# Created by: PyQt5 UI code generator 5.15.10
|
||||
#
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
|||
class Ui_ImportGroup(object):
|
||||
def setupUi(self, ImportGroup):
|
||||
ImportGroup.setObjectName("ImportGroup")
|
||||
ImportGroup.resize(501, 184)
|
||||
ImportGroup.resize(506, 184)
|
||||
ImportGroup.setWindowTitle("ImportGroup")
|
||||
ImportGroup.setWindowFilePath("")
|
||||
self.import_layout = QtWidgets.QFormLayout(ImportGroup)
|
||||
|
@ -23,15 +23,9 @@ class Ui_ImportGroup(object):
|
|||
self.path_edit_label = QtWidgets.QLabel(ImportGroup)
|
||||
self.path_edit_label.setObjectName("path_edit_label")
|
||||
self.import_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.path_edit_label)
|
||||
self.path_edit_layout = QtWidgets.QHBoxLayout()
|
||||
self.path_edit_layout.setObjectName("path_edit_layout")
|
||||
self.import_layout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.path_edit_layout)
|
||||
self.app_name_label = QtWidgets.QLabel(ImportGroup)
|
||||
self.app_name_label.setObjectName("app_name_label")
|
||||
self.import_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.app_name_label)
|
||||
self.app_name_layout = QtWidgets.QHBoxLayout()
|
||||
self.app_name_layout.setObjectName("app_name_layout")
|
||||
self.import_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.app_name_layout)
|
||||
self.import_folder_label = QtWidgets.QLabel(ImportGroup)
|
||||
self.import_folder_label.setObjectName("import_folder_label")
|
||||
self.import_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.import_folder_label)
|
||||
|
@ -51,7 +45,7 @@ class Ui_ImportGroup(object):
|
|||
self.import_dlcs_check.setObjectName("import_dlcs_check")
|
||||
self.import_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.import_dlcs_check)
|
||||
self.import_button_label = QtWidgets.QLabel(ImportGroup)
|
||||
self.import_button_label.setText("")
|
||||
self.import_button_label.setText("Error")
|
||||
self.import_button_label.setObjectName("import_button_label")
|
||||
self.import_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.import_button_label)
|
||||
self.button_info_layout = QtWidgets.QHBoxLayout()
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>501</width>
|
||||
<width>506</width>
|
||||
<height>184</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
@ -30,9 +30,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="path_edit_layout"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="app_name_label">
|
||||
<property name="text">
|
||||
|
@ -40,9 +37,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="app_name_layout"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="import_folder_label">
|
||||
<property name="text">
|
||||
|
@ -84,7 +78,7 @@
|
|||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="import_button_label">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
<string notr="true">Error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import os
|
||||
from enum import IntEnum
|
||||
from logging import getLogger
|
||||
from typing import List, Union, Type
|
||||
from typing import List, Union, Type, Dict
|
||||
|
||||
import qtawesome
|
||||
import requests
|
||||
from PyQt5.QtCore import (
|
||||
pyqtSignal,
|
||||
QObject,
|
||||
QRunnable,
|
||||
QSettings,
|
||||
QFile,
|
||||
QDir,
|
||||
|
@ -16,15 +15,18 @@ from PyQt5.QtCore import (
|
|||
from PyQt5.QtGui import QPalette, QColor, QFontMetrics
|
||||
from PyQt5.QtWidgets import qApp, QStyleFactory, QLabel
|
||||
from PyQt5.sip import wrappertype
|
||||
from legendary.core import LegendaryCore
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from rare.utils.paths import resources_path
|
||||
|
||||
logger = getLogger("Utils")
|
||||
settings = QSettings("Rare", "Rare")
|
||||
|
||||
color_role_map = {
|
||||
|
||||
class ExitCodes(IntEnum):
|
||||
EXIT = 0
|
||||
LOGOUT = -133742
|
||||
|
||||
|
||||
color_role_map: Dict[int, str] = {
|
||||
0: "WindowText",
|
||||
1: "Button",
|
||||
2: "Light",
|
||||
|
@ -49,7 +51,7 @@ color_role_map = {
|
|||
# 21: "NColorRoles",
|
||||
}
|
||||
|
||||
color_group_map = {
|
||||
color_group_map: Dict[int, str] = {
|
||||
0: "Active",
|
||||
1: "Disabled",
|
||||
2: "Inactive",
|
||||
|
@ -148,7 +150,8 @@ def get_translations():
|
|||
def get_latest_version():
|
||||
try:
|
||||
resp = requests.get(
|
||||
"https://api.github.com/repos/RareDevs/Rare/releases/latest", timeout=2,
|
||||
"https://api.github.com/repos/RareDevs/Rare/releases/latest",
|
||||
timeout=2,
|
||||
)
|
||||
tag = resp.json()["tag_name"]
|
||||
return tag
|
||||
|
@ -160,7 +163,8 @@ def path_size(path: Union[str, os.PathLike]) -> int:
|
|||
return sum(
|
||||
os.stat(os.path.join(dp, f)).st_size
|
||||
for dp, dn, filenames in os.walk(path)
|
||||
for f in filenames if os.path.isfile(os.path.join(dp,f ))
|
||||
for f in filenames
|
||||
if os.path.isfile(os.path.join(dp, f))
|
||||
)
|
||||
|
||||
|
||||
|
@ -171,40 +175,6 @@ def format_size(b: Union[int, float]) -> str:
|
|||
b /= 1024
|
||||
|
||||
|
||||
class CloudWorker(QRunnable):
|
||||
class Signals(QObject):
|
||||
# List[SaveGameFile]
|
||||
result_ready = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, core: LegendaryCore):
|
||||
super(CloudWorker, self).__init__()
|
||||
self.core = core
|
||||
self.signals = CloudWorker.Signals()
|
||||
self.setAutoDelete(True)
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
saves = self.core.get_save_games()
|
||||
except HTTPError:
|
||||
self.signals.result_ready.emit(None)
|
||||
return
|
||||
|
||||
save_games = set()
|
||||
for igame in self.core.get_installed_list():
|
||||
game = self.core.get_game(igame.app_name)
|
||||
if self.core.is_installed(igame.app_name) and game.supports_cloud_saves:
|
||||
save_games.add(igame.app_name)
|
||||
|
||||
latest_saves = dict()
|
||||
for s in sorted(saves, key=lambda a: a.datetime):
|
||||
if s.app_name in save_games:
|
||||
if not latest_saves.get(s.app_name):
|
||||
latest_saves[s.app_name] = []
|
||||
latest_saves[s.app_name].append(s)
|
||||
|
||||
self.signals.result_ready.emit(latest_saves)
|
||||
|
||||
|
||||
def icon(icn_str: str, fallback: str = None, **kwargs):
|
||||
try:
|
||||
return qtawesome.icon(icn_str, **kwargs)
|
||||
|
@ -221,7 +191,7 @@ def icon(icn_str: str, fallback: str = None, **kwargs):
|
|||
return qtawesome.icon("ei.error", **kwargs)
|
||||
|
||||
|
||||
def widget_object_name(widget: Union[QObject,wrappertype,Type], suffix: str) -> str:
|
||||
def widget_object_name(widget: Union[QObject, wrappertype, Type], suffix: str) -> str:
|
||||
suffix = f"_{suffix}" if suffix else ""
|
||||
if isinstance(widget, QObject):
|
||||
return f"{type(widget).__name__}{suffix}"
|
||||
|
|
|
@ -36,6 +36,12 @@ def lock_file() -> Path:
|
|||
return Path(QStandardPaths.writableLocation(QStandardPaths.TempLocation), "Rare.lock")
|
||||
|
||||
|
||||
def config_dir() -> Path:
|
||||
# FIXME: This returns ~/.config/Rare/Rare/ for some reason while the settings are in ~/.config/Rare/Rare.conf
|
||||
# Take the parent for now, but this should be investigated
|
||||
return Path(QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)).parent
|
||||
|
||||
|
||||
def data_dir() -> Path:
|
||||
return Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation))
|
||||
|
||||
|
@ -129,6 +135,7 @@ def desktop_link_path(link_name: str, link_type: str) -> Path:
|
|||
|
||||
|
||||
def get_rare_executable() -> List[str]:
|
||||
logger.debug(f"Trying to find executable: {sys.executable}, {sys.argv}")
|
||||
# lk: detect if nuitka
|
||||
if "__compiled__" in globals():
|
||||
executable = [sys.executable]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import difflib
|
||||
import json
|
||||
import orjson
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
|
@ -20,7 +20,7 @@ def get_rating(core: LegendaryCore, app_name: str):
|
|||
global __grades_json
|
||||
if __grades_json is None:
|
||||
if os.path.exists(p := os.path.join(data_dir(), "steam_ids.json")):
|
||||
grades = json.loads(open(p).read())
|
||||
grades = orjson.loads(open(p).read())
|
||||
__grades_json = grades
|
||||
else:
|
||||
grades = {}
|
||||
|
@ -37,8 +37,7 @@ def get_rating(core: LegendaryCore, app_name: str):
|
|||
return "fail"
|
||||
grades[app_name] = {"steam_id": steam_id, "grade": grade}
|
||||
with open(os.path.join(data_dir(), "steam_ids.json"), "w") as f:
|
||||
f.write(json.dumps(grades))
|
||||
f.close()
|
||||
f.write(orjson.dumps(grades).decode("utf-8"))
|
||||
return grade
|
||||
else:
|
||||
return grades[app_name].get("grade")
|
||||
|
@ -52,8 +51,8 @@ def get_grade(steam_code):
|
|||
url = "https://www.protondb.com/api/v1/reports/summaries/"
|
||||
res = requests.get(f"{url}{steam_code}.json")
|
||||
try:
|
||||
lista = json.loads(res.text)
|
||||
except json.decoder.JSONDecodeError:
|
||||
lista = orjson.loads(res.text)
|
||||
except orjson.JSONDecodeError:
|
||||
return "fail"
|
||||
|
||||
return lista["tier"]
|
||||
|
@ -63,17 +62,16 @@ def load_json() -> dict:
|
|||
file = os.path.join(cache_dir(), "game_list.json")
|
||||
if not os.path.exists(file):
|
||||
response = requests.get(url)
|
||||
steam_ids = json.loads(response.text)["applist"]["apps"]
|
||||
steam_ids = orjson.loads(response.text)["applist"]["apps"]
|
||||
ids = {}
|
||||
for game in steam_ids:
|
||||
ids[game["name"]] = game["appid"]
|
||||
|
||||
with open(file, "w") as f:
|
||||
f.write(json.dumps(ids))
|
||||
f.close()
|
||||
f.write(orjson.dumps(ids).decode("utf-8"))
|
||||
return ids
|
||||
else:
|
||||
return json.loads(open(file, "r").read())
|
||||
return orjson.loads(open(file, "r").read())
|
||||
|
||||
|
||||
def get_steam_id(title: str):
|
||||
|
@ -85,16 +83,15 @@ def get_steam_id(title: str):
|
|||
if not os.path.exists(file):
|
||||
response = requests.get(url)
|
||||
ids = {}
|
||||
steam_ids = json.loads(response.text)["applist"]["apps"]
|
||||
steam_ids = orjson.loads(response.text)["applist"]["apps"]
|
||||
for game in steam_ids:
|
||||
ids[game["name"]] = game["appid"]
|
||||
__steam_ids_json = ids
|
||||
|
||||
with open(file, "w") as f:
|
||||
f.write(json.dumps(ids))
|
||||
f.close()
|
||||
f.write(orjson.dumps(ids).decode("utf-8"))
|
||||
else:
|
||||
ids = json.loads(open(file, "r").read())
|
||||
ids = orjson.loads(open(file, "r").read())
|
||||
__steam_ids_json = ids
|
||||
else:
|
||||
ids = __steam_ids_json
|
||||
|
@ -112,7 +109,7 @@ def get_steam_id(title: str):
|
|||
|
||||
def check_time(): # this function check if it's time to update
|
||||
file = os.path.join(cache_dir(), "game_list.json")
|
||||
json_table = json.loads(open(file, "r").read())
|
||||
json_table = orjson.loads(open(file, "r").read())
|
||||
|
||||
today = date.today()
|
||||
day = 0 # it controls how many days it's necessary for an update
|
||||
|
|
|
@ -2,10 +2,13 @@ import os
|
|||
import shutil
|
||||
import subprocess
|
||||
from configparser import ConfigParser
|
||||
from logging import getLogger
|
||||
from typing import Mapping, Dict, List, Tuple
|
||||
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
|
||||
logger = getLogger("Wine")
|
||||
|
||||
|
||||
# this is a copied function from legendary.utils.wine_helpers, but registry file can be specified
|
||||
def read_registry(registry: str, wine_pfx: str) -> ConfigParser:
|
||||
|
@ -19,16 +22,27 @@ def read_registry(registry: str, wine_pfx: str) -> ConfigParser:
|
|||
return reg
|
||||
|
||||
|
||||
def execute(cmd: List, wine_env: Mapping) -> Tuple[str, str]:
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=wine_env,
|
||||
shell=False,
|
||||
text=True,
|
||||
)
|
||||
return proc.communicate()
|
||||
def execute(command: List, wine_env: Mapping) -> Tuple[str, str]:
|
||||
if os.environ.get("container") == "flatpak":
|
||||
flatpak_command = ["flatpak-spawn", "--host"]
|
||||
for name, value in wine_env.items():
|
||||
flatpak_command.append(f"--env={name}={value}")
|
||||
command = flatpak_command + command
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
# Use the current environment if we are in flatpak or our own if we are on host
|
||||
# In flatpak our environment is passed through `flatpak-spawn` arguments
|
||||
env=os.environ.copy() if os.environ.get("container") == "flatpak" else wine_env,
|
||||
shell=False,
|
||||
text=True,
|
||||
)
|
||||
res = proc.communicate()
|
||||
except (FileNotFoundError, PermissionError) as e:
|
||||
res = ("", str(e))
|
||||
return res
|
||||
|
||||
|
||||
def resolve_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
|
||||
|
@ -38,7 +52,11 @@ def resolve_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
|
|||
# lk: if path exists and needs a case-sensitive interpretation form
|
||||
# cmd = [wine_cmd, 'cmd', '/c', f'cd {path} & cd']
|
||||
out, err = execute(cmd, wine_env)
|
||||
return out.strip().strip('"')
|
||||
out, err = out.strip(), err.strip()
|
||||
if not out:
|
||||
logger.error("Failed to resolve wine path due to \"%s\"", err)
|
||||
return out
|
||||
return out.strip('"')
|
||||
|
||||
|
||||
def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str):
|
||||
|
@ -48,6 +66,10 @@ def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str):
|
|||
def query_reg_key(wine_exec: str, wine_env: Mapping, reg_path: str, reg_key) -> str:
|
||||
cmd = [wine_exec, "reg", "query", reg_path, "/v", reg_key]
|
||||
out, err = execute(cmd, wine_env)
|
||||
out, err = out.strip(), err.strip()
|
||||
if not out:
|
||||
logger.error("Failed to query registry key due to \"%s\"", err)
|
||||
return out
|
||||
lines = out.split("\n")
|
||||
keys: Dict = {}
|
||||
for line in lines:
|
||||
|
@ -63,16 +85,12 @@ def convert_to_windows_path(wine_exec: str, wine_env: Mapping, path: str) -> str
|
|||
|
||||
def convert_to_unix_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
|
||||
path = path.strip().strip('"')
|
||||
cmd = [winepath(wine_exec), "-u", path]
|
||||
cmd = [wine_exec, "winepath.exe", "-u", path]
|
||||
out, err = execute(cmd, wine_env)
|
||||
return os.path.realpath(out.strip())
|
||||
|
||||
|
||||
def winepath(wine_exec: str) -> str:
|
||||
_winepath = os.path.join(os.path.dirname(wine_exec), "winepath")
|
||||
if not os.path.isfile(_winepath):
|
||||
return ""
|
||||
return _winepath
|
||||
out, err = out.strip(), err.strip()
|
||||
if not out:
|
||||
logger.error("Failed to convert to unix path due to \"%s\"", err)
|
||||
return os.path.realpath(out) if (out := out.strip()) else out
|
||||
|
||||
|
||||
def wine(core: LegendaryCore, app_name: str = "default") -> str:
|
||||
|
@ -85,8 +103,11 @@ def wine(core: LegendaryCore, app_name: str = "default") -> str:
|
|||
|
||||
|
||||
def environ(core: LegendaryCore, app_name: str = "default") -> Dict:
|
||||
_environ = os.environ.copy()
|
||||
# Get a clean environment if we are in flatpak, this environment will be pass
|
||||
# to `flatpak-spawn`, otherwise use the system's.
|
||||
_environ = {} if os.environ.get("container") == "flatpak" else os.environ.copy()
|
||||
_environ.update(core.get_app_environment(app_name))
|
||||
_environ["WINEDEBUG"] = "-all"
|
||||
_environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
|
||||
_environ["DISPLAY"] = ""
|
||||
return _environ
|
||||
|
|
|
@ -145,10 +145,7 @@ class IndicatorLineEdit(QWidget):
|
|||
self.line_edit.layout().setContentsMargins(0, 0, 10, 0)
|
||||
self.line_edit.layout().addWidget(self.hint_label)
|
||||
# Add completer
|
||||
if completer is not None:
|
||||
completer.popup().setItemDelegate(QStyledItemDelegate(self))
|
||||
completer.popup().setAlternatingRowColors(True)
|
||||
self.line_edit.setCompleter(completer)
|
||||
self.setCompleter(completer)
|
||||
layout.addWidget(self.line_edit)
|
||||
if edit_func is not None:
|
||||
self.indicator_label = QLabel()
|
||||
|
@ -195,6 +192,16 @@ class IndicatorLineEdit(QWidget):
|
|||
self.hint_label.setFrameRect(self.line_edit.rect())
|
||||
self.hint_label.setText(text)
|
||||
|
||||
def setCompleter(self, completer: Optional[QCompleter]):
|
||||
if old := self.line_edit.completer():
|
||||
old.deleteLater()
|
||||
if not completer:
|
||||
self.line_edit.setCompleter(None)
|
||||
return
|
||||
completer.popup().setItemDelegate(QStyledItemDelegate(self))
|
||||
completer.popup().setAlternatingRowColors(True)
|
||||
self.line_edit.setCompleter(completer)
|
||||
|
||||
def extend_reasons(self, reasons: Dict):
|
||||
self.__reasons.extend(reasons)
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ class RareAppException(QObject):
|
|||
exception = pyqtSignal(object, object, object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self.logger = logging.getLogger(type(self).__name__)
|
||||
super(RareAppException, self).__init__(parent=parent)
|
||||
self.logger = logging.getLogger(type(self).__name__)
|
||||
sys.excepthook = self._excepthook
|
||||
self.exception.connect(self._on_exception)
|
||||
|
||||
|
@ -37,14 +37,19 @@ class RareAppException(QObject):
|
|||
if self._handler(exc_type, exc_value, exc_tb):
|
||||
return
|
||||
self.logger.fatal(message)
|
||||
QMessageBox.warning(None, exc_type.__name__, message)
|
||||
QApplication.exit(1)
|
||||
action = QMessageBox.warning(
|
||||
None, exc_type.__name__, message,
|
||||
buttons=QMessageBox.Ignore | QMessageBox.Close,
|
||||
defaultButton=QMessageBox.Ignore
|
||||
)
|
||||
if action == QMessageBox.RejectRole:
|
||||
QApplication.exit(1)
|
||||
|
||||
|
||||
class RareApp(QApplication):
|
||||
def __init__(self, args: Namespace, log_file: str):
|
||||
self.logger = logging.getLogger(type(self).__name__)
|
||||
super(RareApp, self).__init__(sys.argv)
|
||||
self.logger = logging.getLogger(type(self).__name__)
|
||||
self._hook = RareAppException(self)
|
||||
self.setQuitOnLastWindowClosed(False)
|
||||
self.setAttribute(Qt.AA_DontUseNativeDialogs, True)
|
||||
|
|
5
requirements-flatpak.txt
Normal file
5
requirements-flatpak.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
requests
|
||||
QtAwesome
|
||||
setuptools
|
||||
legendary-gl>=0.20.34
|
||||
pypresence
|
|
@ -1,9 +1,9 @@
|
|||
typing_extensions
|
||||
requests
|
||||
PyQt5
|
||||
QtAwesome
|
||||
setuptools
|
||||
legendary-gl
|
||||
legendary-gl>=0.20.33
|
||||
orjson
|
||||
pywin32; platform_system == "Windows"
|
||||
pywebview[qt]; platform_system == "Linux"
|
||||
pywebview[qt]; platform_system == "FreeBSD"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
typing_extensions
|
||||
requests
|
||||
PyQt5
|
||||
QtAwesome
|
||||
setuptools
|
||||
legendary-gl>=0.20.34
|
||||
orjson
|
||||
pywin32; platform_system == "Windows"
|
||||
|
|
7
setup.py
7
setup.py
|
@ -8,12 +8,12 @@ with open("README.md", "r") as fh:
|
|||
requirements = [
|
||||
"requests<3.0",
|
||||
"legendary-gl>=0.20.34",
|
||||
"orjson",
|
||||
"setuptools",
|
||||
"wheel",
|
||||
"PyQt5",
|
||||
"QtAwesome",
|
||||
'pywin32; platform_system == "Windows"',
|
||||
"typing_extensions"
|
||||
]
|
||||
|
||||
optional_reqs = dict(
|
||||
|
@ -45,7 +45,10 @@ setuptools.setup(
|
|||
],
|
||||
include_package_data=True,
|
||||
python_requires=">=3.9",
|
||||
entry_points=dict(console_scripts=["rare=rare.__main__:main"]),
|
||||
entry_points={
|
||||
# 'console_scripts': ["rare = rare.main:main"],
|
||||
'gui_scripts': ["rare = rare.main:main"],
|
||||
},
|
||||
install_requires=requirements,
|
||||
extras_require=optional_reqs,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue