1
0
Fork 0
mirror of synced 2024-06-02 02:34:40 +12:00

Merge pull request #328 from loathingKernel/entry_points

Separate entry points for the main application and the launcher
This commit is contained in:
Stelios Tsampas 2023-12-11 00:09:19 +02:00 committed by GitHub
commit 5d0940083f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1483 additions and 1219 deletions

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View file

@ -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

View file

@ -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
View 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 }}

View file

@ -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

View file

@ -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.

View file

@ -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,

View file

@ -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"

View file

@ -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
View 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())

View file

@ -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

View file

@ -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

View file

@ -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]]:

View file

@ -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

View file

@ -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):

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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:

View file

@ -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()

View file

@ -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]

View file

@ -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)

View file

@ -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):

View file

@ -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_()

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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
View 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())

View file

@ -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:

View file

@ -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 {

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -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"))

View file

@ -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">

View file

@ -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()

View file

@ -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>

View file

@ -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}"

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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
View file

@ -0,0 +1,5 @@
requests
QtAwesome
setuptools
legendary-gl>=0.20.34
pypresence

View file

@ -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"

View file

@ -1,7 +1,7 @@
typing_extensions
requests
PyQt5
QtAwesome
setuptools
legendary-gl>=0.20.34
orjson
pywin32; platform_system == "Windows"

View file

@ -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,
)