Compare commits
147 commits
1.10.11.86
...
main
Author | SHA1 | Date | |
---|---|---|---|
94c2f765f3 | |||
71348dbf23 | |||
2396f0fb83 | |||
6dd5d2f546 | |||
a4db64082f | |||
50ff2ae4fd | |||
0ac4cf5a7c | |||
259bc98eb9 | |||
6594d65be1 | |||
42341d5c81 | |||
4dea963154 | |||
87b0ae0540 | |||
9b476afe8c | |||
95e760791b | |||
fec4e3c0e1 | |||
fb736fa47b | |||
3b721fdd13 | |||
f9cc1b48f1 | |||
9017826b16 | |||
4bb1fb10ee | |||
beb4f6c310 | |||
337d753d0c | |||
4497a1c712 | |||
07fa7890cf | |||
1d8ae15ea9 | |||
b9d034eef7 | |||
9f67f930a9 | |||
8ebcc3a700 | |||
0ddd5a0674 | |||
0c848c41b0 | |||
8f018cb162 | |||
34abca2a4e | |||
cb919ed69f | |||
1c886535a5 | |||
a35e430539 | |||
14bde0a23c | |||
89340f331b | |||
fb0d5bbe10 | |||
766557924a | |||
1fab13fd92 | |||
7a2a6458ed | |||
9ec349e2d1 | |||
2a2458bacb | |||
816c5f3de9 | |||
b4a26b5932 | |||
fda82b17cf | |||
91af16b76d | |||
f6396f488a | |||
7246078df3 | |||
247b2c947a | |||
b6458b1bfc | |||
d76fc2b68b | |||
2db34324af | |||
b812e38fb8 | |||
d3b591952f | |||
6b15c0f2cf | |||
784fadb2da | |||
c768b6ac3b | |||
c1b92c3ae5 | |||
1c578e354e | |||
f94ab70287 | |||
5b6df91be9 | |||
7b810173da | |||
f3d870cebb | |||
e50015c25c | |||
6cfec6c718 | |||
570261395a | |||
2e8dcc49ca | |||
379cbd2f89 | |||
ae69413ddb | |||
8a421b08f8 | |||
e8e39fa391 | |||
7dff47c5ac | |||
49c06aef79 | |||
1c027fc14a | |||
980bac5c4e | |||
d397912247 | |||
4548a8b1e0 | |||
243b92248e | |||
715ac06719 | |||
5bf353ec37 | |||
8dbce8e9f2 | |||
f542e11b25 | |||
04868fcf25 | |||
ddbd94354c | |||
91d8cb336d | |||
f2c63aa3b4 | |||
557189f41b | |||
5b217e0b15 | |||
d16b3d5d68 | |||
68ea7b9ca1 | |||
db3cf68d19 | |||
5359b73c35 | |||
6db35d1f1e | |||
e776ed457a | |||
582b83c12b | |||
3fe02e5026 | |||
6a747ce0f7 | |||
2f84a501d5 | |||
b7b1bc6406 | |||
6be9eec3ef | |||
b1e537af43 | |||
7c3d5dc9e8 | |||
4f4689e82b | |||
3313f15c9f | |||
52d2ca7cc7 | |||
b84686aba6 | |||
2d3a8deec1 | |||
8a3bdbdd91 | |||
f088fc95b6 | |||
284543a6d9 | |||
c5c581eb6e | |||
94030055cf | |||
fb91a55f30 | |||
e8e4ed739b | |||
8df9b08e7e | |||
a104cf4518 | |||
1cfcb783c2 | |||
a15a2fbbe2 | |||
f33c89a411 | |||
aadf795d21 | |||
bb5b0f1585 | |||
98213d1ce5 | |||
49ad79e871 | |||
17066f9a67 | |||
8bde2c2c6d | |||
0ea29bc941 | |||
0ea4b1a824 | |||
cd1743cb92 | |||
af6d7c5055 | |||
7a5bb0b732 | |||
36ad33b8f3 | |||
07b5d381f0 | |||
b67c391a26 | |||
9eb5f2c51e | |||
88b6e91530 | |||
9181641d70 | |||
03b9e44b13 | |||
f321736dde | |||
58574c1977 | |||
fe709f5702 | |||
1269abf1f7 | |||
5d2cfbf71a | |||
7cdf7996b2 | |||
f089703eb5 | |||
da2e1c0d27 | |||
7ef5172d62 |
1
.gitattributes
vendored
|
@ -1,3 +1,4 @@
|
|||
rare/resources/resources.py binary
|
||||
rare/resources/static_css/__init__.py binary
|
||||
rare/resources/stylesheets/ChildOfMetropolis/__init__.py binary
|
||||
rare/resources/stylesheets/RareStyle/__init__.py binary
|
||||
|
|
3
.github/workflows/checks.yml
vendored
|
@ -23,6 +23,7 @@ on:
|
|||
jobs:
|
||||
pylint:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ['macos-latest', 'windows-latest', 'ubuntu-latest']
|
||||
version: ['3.9', '3.10', '3.11', '3.12']
|
||||
|
@ -47,4 +48,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.*,orjson.*
|
||||
python3 -m pylint rare
|
||||
|
|
93
.github/workflows/codeql.yml
vendored
Normal file
|
@ -0,0 +1,93 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '37 11 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: python
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
2
.github/workflows/job_cx-freeze-msi.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
python-version: '3.12'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install Build Dependencies
|
||||
|
|
2
.github/workflows/job_cx-freeze-zip.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
python-version: '3.11'
|
||||
python-version: '3.12'
|
||||
check-latest: true
|
||||
architecture: x64
|
||||
- name: Install build dependencies
|
||||
|
|
37
.github/workflows/job_release.yml
vendored
|
@ -14,39 +14,34 @@ on:
|
|||
type: string
|
||||
name2:
|
||||
type: string
|
||||
default: ""
|
||||
file2:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Upload
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: ${{ inputs.name1 }}
|
||||
file: ${{ inputs.file1 }}
|
||||
- name: ${{ inputs.name2 }}
|
||||
file: ${{ inputs.file2 }}
|
||||
steps:
|
||||
|
||||
- name: Download ${{ inputs.name1 }} artifact
|
||||
- name: Download ${{ matrix.name }} from artifact
|
||||
uses: actions/download-artifact@v3
|
||||
if: ${{ matrix.name != '' }}
|
||||
with:
|
||||
name: ${{ inputs.name1 }}
|
||||
- name: Upload ${{ inputs.name1 }} to release
|
||||
name: ${{ matrix.name }}
|
||||
- name: Upload ${{ matrix.name }} to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: ${{ matrix.name != '' }}
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ inputs.file1 }}
|
||||
asset_name: ${{ inputs.name1 }}
|
||||
file: ${{ matrix.file }}
|
||||
asset_name: ${{ matrix.name }}
|
||||
tag: ${{ inputs.version }}
|
||||
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: ${{ inputs.version }}
|
||||
overwrite: true
|
4
.github/workflows/job_version.yml
vendored
|
@ -4,9 +4,9 @@ on:
|
|||
workflow_call:
|
||||
outputs:
|
||||
version:
|
||||
value: ${{ jobs.version.outputs.tag_abbrev }}.${{ jobs.version.outputs.tag_offset }}
|
||||
value: "${{ jobs.version.outputs.tag_abbrev }}.${{ jobs.version.outputs.tag_offset }}"
|
||||
branch:
|
||||
value: ${{ jobs.version.outputs.branch }}
|
||||
value: "${{ jobs.version.outputs.branch }}"
|
||||
|
||||
|
||||
jobs:
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
host = https://www.transifex.com
|
||||
|
||||
[o:rare-1:p:rare:r:placeholder-ts]
|
||||
file_filter = rare/resources/languages/<lang>.ts
|
||||
source_file = rare/resources/languages/translation_source.ts
|
||||
source_lang = en_US
|
||||
file_filter = rare/resources/languages/rare_<lang>.ts
|
||||
source_file = rare/resources/languages/source.ts
|
||||
source_lang = en
|
||||
type = QT
|
||||
minimum_perc = 50
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ AppDir:
|
|||
# Path to the site-packages dir or other modules dirs
|
||||
# See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH
|
||||
PYTHONPATH: '${APPDIR}/usr/lib/python3.8/site-packages:${APPDIR}/usr/lib/python3.9/site-packages'
|
||||
PYTHONNOUSERSITE: 1
|
||||
|
||||
test:
|
||||
fedora:
|
||||
|
|
40
README.md
|
@ -23,7 +23,7 @@ Rare is a graphical interface for Legendary, a command line alternative to Epic
|
|||
- Packages, packages everywhere
|
||||
|
||||
|
||||
## Reporing issues
|
||||
## Reporting issues
|
||||
|
||||
If you run into any issues, you can report them by creating an issue on GitHub: https://github.com/RareDevs/Rare/issues/new/choose
|
||||
|
||||
|
@ -147,12 +147,24 @@ Depending on your operating system and the `python` distribution, the following
|
|||
|
||||
### Run from source
|
||||
|
||||
1. Clone the repo: `git clone https://github.com/Dummerle/Rare
|
||||
1. Clone the repo: `git clone https://github.com/RareDevs/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-orjson` 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
|
||||
```shell
|
||||
pip install -r requirements-full.txt
|
||||
```
|
||||
* If you are on Arch you can run
|
||||
```shell
|
||||
sudo pacman --needed -S python-wheel python-setuptools python-pyqt5 python-qtawesome python-requests python-orjson
|
||||
```
|
||||
```
|
||||
yay -S legendary
|
||||
```
|
||||
* If you are on FreeBSD you have to install py39-qt5 from the packages
|
||||
```shell
|
||||
sudo pkg install py39-qt5
|
||||
```
|
||||
4. Run `python3 -m rare`
|
||||
|
||||
|
||||
|
@ -165,10 +177,16 @@ There are several options to contribute.
|
|||
|
||||
More information is available in CONTRIBUTING.md.
|
||||
|
||||
## Images
|
||||
## Screenshots
|
||||
| Game covers | Vertical list |
|
||||
|----------------------------------------------|----------------------------------------------|
|
||||
| ![alt text](Screenshots/RareLibraryIcon.png) | ![alt text](Screenshots/RareLibraryList.png) |
|
||||
|
||||
| Game details | Game settings |
|
||||
|-------------------------------------------|-----------------------------------------------|
|
||||
| ![alt text](Screenshots/RareGameInfo.png) | ![alt text](Screenshots/RareGameSettings.png) |
|
||||
|
||||
| Downloads | Application settings |
|
||||
|--------------------------------------------|-------------------------------------------|
|
||||
| ![alt text](Screenshots/RareDownloads.png) | ![alt text](Screenshots/RareSettings.png) |
|
||||
|
||||
![alt text](https://github.com/Dummerle/Rare/blob/main/Screenshots/Rare.png?raw=true)
|
||||
![alt text](https://github.com/Dummerle/Rare/blob/main/Screenshots/GameInfo.png?raw=true)
|
||||
![alt text](https://github.com/Dummerle/Rare/blob/main/Screenshots/RareSettings.png?raw=true)
|
||||
![alt text](https://github.com/Dummerle/Rare/blob/main/Screenshots/RareDownloads.png?raw=true)
|
||||
![alt text](https://github.com/Dummerle/Rare/blob/main/Screenshots/GameSettings.png?raw=true)
|
||||
|
|
Before Width: | Height: | Size: 156 KiB |
Before Width: | Height: | Size: 512 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 93 KiB |
BIN
Screenshots/RareGameInfo.png
Normal file
After Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
BIN
Screenshots/RareLibraryIcon.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
Screenshots/RareLibraryList.png
Normal file
After Width: | Height: | Size: 358 KiB |
|
@ -2,4 +2,4 @@ import pkg_resources
|
|||
from subprocess import call
|
||||
|
||||
for dist in pkg_resources.working_set:
|
||||
call("python -m pip install --upgrade " + dist.project_name, shell=True)
|
||||
call(f"python -m pip install --upgrade {dist.project_name}", shell=True)
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
cwd="$(pwd)"
|
||||
cd "$(dirname "$0")"/.. || exit
|
||||
|
||||
pylupdate5 -noobsolete $(find rare/ -iname "*.py") -ts rare/resources/languages/translation_source.ts
|
||||
#py_files=$(find rare -iname "*.py" -not -path rare/ui)
|
||||
#ui_files=$(find rare/ui -iname "*.ui")
|
||||
|
||||
pylupdate5 -noobsolete $(find rare/ -iname "*.py") -ts rare/resources/languages/source.ts
|
||||
|
||||
cd "$cwd" || exit
|
||||
|
|
643
pylintrc
Normal file
|
@ -0,0 +1,643 @@
|
|||
[MAIN]
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
|
||||
# in a server-like mode.
|
||||
clear-cache-post-run=no
|
||||
|
||||
# Load and enable all available extensions. Use --list-extensions to see a list
|
||||
# all available extensions.
|
||||
#enable-all-extensions=
|
||||
|
||||
# In error mode, messages with a category besides ERROR or FATAL are
|
||||
# suppressed, and no reports are done by default. Error mode is compatible with
|
||||
# disabling specific errors.
|
||||
errors-only=yes
|
||||
|
||||
# Always return a 0 (non-error) status code, even if lint errors are found.
|
||||
# This is primarily useful in continuous integration scripts.
|
||||
#exit-zero=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-allow-list=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
|
||||
# for backward compatibility.)
|
||||
extension-pkg-whitelist=PyQt5
|
||||
|
||||
# Return non-zero exit code if any of these messages/categories are detected,
|
||||
# even if score is above --fail-under value. Syntax same as enable. Messages
|
||||
# specified are enabled, while categories only check already-enabled messages.
|
||||
fail-on=
|
||||
|
||||
# Specify a score threshold under which the program will exit with error.
|
||||
fail-under=10
|
||||
|
||||
# Interpret the stdin as a python script, whose filename needs to be passed as
|
||||
# the module_or_package argument.
|
||||
#from-stdin=
|
||||
|
||||
# Files or directories to be skipped. They should be base names, not paths.
|
||||
ignore=ui,singleton.py
|
||||
|
||||
# Add files or directories matching the regular expressions patterns to the
|
||||
# ignore-list. The regex matches against paths and can be in Posix or Windows
|
||||
# format. Because '\\' represents the directory delimiter on Windows systems,
|
||||
# it can't be used as an escape character.
|
||||
ignore-paths=
|
||||
|
||||
# Files or directories matching the regular expression patterns are skipped.
|
||||
# The regex matches against base names, not paths. The default value ignores
|
||||
# Emacs file locks
|
||||
ignore-patterns=^\.#
|
||||
|
||||
# List of module names for which member attributes should not be checked and
|
||||
# will not be imported (useful for modules/projects where namespaces are
|
||||
# manipulated during runtime and thus existing member attributes cannot be
|
||||
# deduced by static analysis). It supports qualified module names, as well as
|
||||
# Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use, and will cap the count on Windows to
|
||||
# avoid hangs.
|
||||
jobs=3
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Minimum Python version to use for version dependent checks. Will default to
|
||||
# the version used to run pylint.
|
||||
py-version=3.9
|
||||
|
||||
# Discover python modules and packages in the file system subtree.
|
||||
recursive=yes
|
||||
|
||||
# Add paths to the list of the source roots. Supports globbing patterns. The
|
||||
# source root is an absolute path or a path relative to the current working
|
||||
# directory used to determine a package namespace for modules located under the
|
||||
# source root.
|
||||
source-roots=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# In verbose mode, extra non-checker-related info will be displayed.
|
||||
#verbose=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style. If left empty, argument names will be checked with the set
|
||||
# naming style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style. If left empty, attribute names will be checked with the set naming
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style. If left empty, class attribute names will be checked
|
||||
# with the set naming style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class constant names.
|
||||
class-const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct class constant names. Overrides class-
|
||||
# const-naming-style. If left empty, class constant names will be checked with
|
||||
# the set naming style.
|
||||
#class-const-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style. If left empty, class names will be checked with the set naming style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style. If left empty, constant names will be checked with the set naming
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style. If left empty, function names will be checked with the set
|
||||
# naming style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style. If left empty, inline iteration names will be checked
|
||||
# with the set naming style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style. If left empty, method names will be checked with the set naming style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style. If left empty, module names will be checked with the set naming style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct type alias names. If left empty, type
|
||||
# alias names will be checked with the set naming style.
|
||||
#typealias-rgx=
|
||||
|
||||
# Regular expression matching correct type variable names. If left empty, type
|
||||
# variable names will be checked with the set naming style.
|
||||
#typevar-rgx=
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style. If left empty, variable names will be checked with the set
|
||||
# naming style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# Warn about protected attribute access inside special methods
|
||||
check-protected-access-in-special-methods=no
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
asyncSetUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# List of regular expressions of class ancestor names to ignore when counting
|
||||
# public methods (see R0903)
|
||||
exclude-too-few-public-methods=
|
||||
|
||||
# List of qualified class names to ignore when counting class parents (see
|
||||
# R0901)
|
||||
ignored-parents=
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when caught.
|
||||
overgeneral-exceptions=builtins.BaseException,builtins.Exception
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow explicit reexports by alias from a package __init__.
|
||||
allow-reexport-from-package=no
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of external dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of all (i.e. internal and
|
||||
# external) dependencies to the given file (report RP0402 must not be
|
||||
# disabled).
|
||||
import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of internal dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
|
||||
# UNDEFINED.
|
||||
confidence=HIGH,
|
||||
CONTROL_FLOW,
|
||||
INFERENCE,
|
||||
INFERENCE_FAILURE,
|
||||
UNDEFINED
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then re-enable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
use-implicit-booleaness-not-comparison-to-string,
|
||||
use-implicit-booleaness-not-comparison-to-zero,
|
||||
no-name-in-module,
|
||||
unexpected-keyword-arg,
|
||||
no-value-for-parameter
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=
|
||||
|
||||
|
||||
[METHOD_ARGS]
|
||||
|
||||
# List of qualified names (i.e., library.method) which require a timeout
|
||||
# parameter e.g. 'requests.api.get,requests.api.post'
|
||||
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
notes-rgx=
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit,argparse.parse_error
|
||||
|
||||
# Let 'consider-using-join' be raised when the separator to join on would be
|
||||
# non-empty (resulting in expected fixes of the type: ``"- " + " -
|
||||
# ".join(items)``)
|
||||
suggest-join-with-non-empty-separator=yes
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
|
||||
# 'convention', and 'info' which contain the number of messages in each
|
||||
# category, as well as 'statement' which is the total number of statements
|
||||
# analyzed. This score is used by the global evaluation report (RP0004).
|
||||
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
msg-template=
|
||||
|
||||
# Set the output format. Available formats are: text, parseable, colorized,
|
||||
# json2 (improved json format), json (old json format) and msvs (visual
|
||||
# studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
#output-format=
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Comments are removed from the similarity computation
|
||||
ignore-comments=yes
|
||||
|
||||
# Docstrings are removed from the similarity computation
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Imports are removed from the similarity computation
|
||||
ignore-imports=yes
|
||||
|
||||
# Signatures are removed from the similarity computation
|
||||
ignore-signatures=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. No available dictionaries : You need to install
|
||||
# both the python package and the system dependency for enchant to work.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should be considered directives if they
|
||||
# appear at the beginning of a comment and should not be checked.
|
||||
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=PyQt5.*,orjson.*
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of symbolic message names to ignore for Mixin members.
|
||||
ignored-checks-for-mixins=no-member,
|
||||
not-async-context-manager,
|
||||
not-context-manager,
|
||||
attribute-defined-outside-init
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# Regex pattern to define which classes are considered mixins.
|
||||
mixin-class-rgx=.*[Mm]ixin
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of names allowed to shadow builtins
|
||||
allowed-redefined-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import platform
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
import traceback
|
||||
|
@ -17,6 +18,7 @@ from legendary.models.game import SaveGameStatus
|
|||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.models.base_game import RareGameSlim
|
||||
from rare.models.launcher import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
|
||||
from rare.models.options import options
|
||||
from rare.widgets.rare_app import RareApp, RareAppException
|
||||
from .cloud_sync_dialog import CloudSyncDialog, CloudSyncDialogResult
|
||||
from .console_dialog import ConsoleDialog
|
||||
|
@ -29,18 +31,19 @@ DETACHED_APP_NAMES = {
|
|||
"09e442f830a341f698b4da42abd98c9b", # Fortnite Festival
|
||||
"d8f7763e07d74c209d760a679f9ed6ac", # Lego Fortnite
|
||||
"Fortnite_Studio", # Unreal Editor for Fortnite
|
||||
"dcfccf8d965a4f2281dddf9fead042de", # Homeworld Remastered Collection (issue#376)
|
||||
}
|
||||
|
||||
|
||||
class PreLaunchThread(QRunnable):
|
||||
class PreLaunch(QRunnable):
|
||||
class Signals(QObject):
|
||||
ready_to_launch = pyqtSignal(LaunchArgs)
|
||||
started_pre_launch_command = pyqtSignal()
|
||||
pre_launch_command_started = pyqtSignal()
|
||||
pre_launch_command_finished = pyqtSignal(int) # exit_code
|
||||
error_occurred = pyqtSignal(str)
|
||||
|
||||
def __init__(self, core: LegendaryCore, args: InitArgs, rgame: RareGameSlim, sync_action=None):
|
||||
super(PreLaunchThread, self).__init__()
|
||||
def __init__(self, args: InitArgs, rgame: RareGameSlim, sync_action=None):
|
||||
super(PreLaunch, self).__init__()
|
||||
self.signals = self.Signals()
|
||||
self.logger = getLogger(type(self).__name__)
|
||||
self.args = args
|
||||
|
@ -73,8 +76,10 @@ class PreLaunchThread(QRunnable):
|
|||
if launch_args.pre_launch_command:
|
||||
proc = get_configured_process()
|
||||
proc.setProcessEnvironment(launch_args.environment)
|
||||
self.signals.started_pre_launch_command.emit()
|
||||
proc.start(launch_args.pre_launch_command[0], launch_args.pre_launch_command[1:])
|
||||
self.signals.pre_launch_command_started.emit()
|
||||
pre_launch_command = shlex.split(launch_args.pre_launch_command)
|
||||
# self.logger.debug("Executing prelaunch command %s, %s", pre_launch_command[0], pre_launch_command[1:])
|
||||
proc.start(pre_launch_command[0], pre_launch_command[1:])
|
||||
if launch_args.pre_launch_wait:
|
||||
proc.waitForFinished(-1)
|
||||
return launch_args
|
||||
|
@ -141,11 +146,11 @@ class RareLauncher(RareApp):
|
|||
return
|
||||
self.rgame = RareGameSlim(self.core, game)
|
||||
|
||||
lang = self.settings.value("language", self.core.language_code, type=str)
|
||||
self.load_translator(lang)
|
||||
language = self.settings.value(*options.language)
|
||||
self.load_translator(language)
|
||||
|
||||
if QSettings().value("show_console", False, bool):
|
||||
self.console = ConsoleDialog()
|
||||
if QSettings(self).value(*options.log_games):
|
||||
self.console = ConsoleDialog(game.app_title)
|
||||
self.console.show()
|
||||
|
||||
self.game_process.finished.connect(self.__process_finished)
|
||||
|
@ -169,13 +174,13 @@ class RareLauncher(RareApp):
|
|||
|
||||
@pyqtSlot()
|
||||
def __proc_log_stdout(self):
|
||||
self.console.log(
|
||||
self.console.log_stdout(
|
||||
self.game_process.readAllStandardOutput().data().decode("utf-8", "ignore")
|
||||
)
|
||||
|
||||
@pyqtSlot()
|
||||
def __proc_log_stderr(self):
|
||||
self.console.error(
|
||||
self.console.log_stderr(
|
||||
self.game_process.readAllStandardError().data().decode("utf-8", "ignore")
|
||||
)
|
||||
|
||||
|
@ -293,10 +298,10 @@ 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.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 as a detached process")
|
||||
subprocess.Popen([args.executable] + args.arguments, cwd=args.working_directory,
|
||||
env={i: args.environment.value(i) for i in args.environment.keys()})
|
||||
self.stop()
|
||||
return
|
||||
if self.args.dry_run:
|
||||
|
@ -308,6 +313,7 @@ class RareLauncher(RareApp):
|
|||
print(args.executable, " ".join(args.arguments))
|
||||
self.stop()
|
||||
return
|
||||
# self.logger.debug("Executing prelaunch command %s, %s", args.executable, args.arguments)
|
||||
self.game_process.start(args.executable, args.arguments)
|
||||
|
||||
def error_occurred(self, error_str: str):
|
||||
|
@ -321,7 +327,7 @@ class RareLauncher(RareApp):
|
|||
self.stop()
|
||||
|
||||
def start_prepare(self, sync_action=None):
|
||||
worker = PreLaunchThread(self.core, self.args, self.rgame, sync_action)
|
||||
worker = PreLaunch(self.args, self.rgame, sync_action)
|
||||
worker.signals.ready_to_launch.connect(self.launch_game)
|
||||
worker.signals.error_occurred.connect(self.error_occurred)
|
||||
# worker.signals.started_pre_launch_command(None)
|
|
@ -9,8 +9,8 @@ from legendary.core import LegendaryCore
|
|||
from legendary.models.game import InstalledGame
|
||||
|
||||
from rare.ui.components.tabs.games.game_info.cloud_sync_widget import Ui_CloudSyncWidget
|
||||
from rare.utils.misc import icon
|
||||
from rare.widgets.dialogs import ButtonDialog, dialog_title_game
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import ButtonDialog, game_title
|
||||
|
||||
logger = getLogger("CloudSyncDialog")
|
||||
|
||||
|
@ -28,9 +28,9 @@ class CloudSyncDialog(ButtonDialog):
|
|||
def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime, parent=None):
|
||||
super(CloudSyncDialog, self).__init__(parent=parent)
|
||||
header = self.tr("Cloud saves for")
|
||||
self.setWindowTitle(dialog_title_game(header, igame.title))
|
||||
self.setWindowTitle(game_title(header, igame.title))
|
||||
|
||||
title_label = QLabel(f"<h4>{dialog_title_game(header, igame.title)}</h4>", self)
|
||||
title_label = QLabel(f"<h4>{game_title(header, igame.title)}</h4>", self)
|
||||
|
||||
sync_widget = QWidget(self)
|
||||
self.sync_ui = Ui_CloudSyncWidget()
|
||||
|
@ -41,7 +41,7 @@ class CloudSyncDialog(ButtonDialog):
|
|||
layout.addWidget(sync_widget)
|
||||
|
||||
self.accept_button.setText(self.tr("Skip"))
|
||||
self.accept_button.setIcon(icon("fa.chevron-right"))
|
||||
self.accept_button.setIcon(qta_icon("fa.chevron-right"))
|
||||
|
||||
self.setCentralLayout(layout)
|
||||
|
||||
|
@ -62,8 +62,8 @@ class CloudSyncDialog(ButtonDialog):
|
|||
self.sync_ui.date_info_local.setText(dt_local.strftime("%A, %d. %B %Y %X") if dt_local else "None")
|
||||
self.sync_ui.date_info_remote.setText(dt_remote.strftime("%A, %d. %B %Y %X") if dt_remote else "None")
|
||||
|
||||
self.sync_ui.icon_local.setPixmap(icon("mdi.harddisk", "fa.desktop").pixmap(128, 128))
|
||||
self.sync_ui.icon_remote.setPixmap(icon("mdi.cloud-outline", "ei.cloud").pixmap(128, 128))
|
||||
self.sync_ui.icon_local.setPixmap(qta_icon("mdi.harddisk", "fa.desktop").pixmap(128, 128))
|
||||
self.sync_ui.icon_remote.setPixmap(qta_icon("mdi.cloud-outline", "ei.cloud").pixmap(128, 128))
|
||||
|
||||
self.sync_ui.upload_button.clicked.connect(self.__on_upload)
|
||||
self.sync_ui.download_button.clicked.connect(self.__on_download)
|
|
@ -1,4 +1,4 @@
|
|||
import platform
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
from PyQt5.QtCore import QProcessEnvironment, pyqtSignal, QSize, Qt
|
||||
|
@ -14,7 +14,8 @@ from PyQt5.QtWidgets import (
|
|||
QSizePolicy, QTableWidgetItem, QHeaderView, QApplication,
|
||||
)
|
||||
|
||||
from rare.ui.launcher.console_env import Ui_ConsoleEnv
|
||||
from rare.ui.commands.launcher.console_env import Ui_ConsoleEnv
|
||||
from rare.widgets.dialogs import dialog_title, game_title
|
||||
|
||||
|
||||
class ConsoleDialog(QDialog):
|
||||
|
@ -22,15 +23,17 @@ class ConsoleDialog(QDialog):
|
|||
kill = pyqtSignal()
|
||||
env: QProcessEnvironment
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, app_title: str, parent=None):
|
||||
super(ConsoleDialog, self).__init__(parent=parent)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||
self.setWindowTitle("Rare - Console")
|
||||
self.setWindowTitle(
|
||||
dialog_title(game_title(self.tr("Console"), app_title))
|
||||
)
|
||||
self.setGeometry(0, 0, 640, 480)
|
||||
layout = QVBoxLayout()
|
||||
|
||||
self.console = ConsoleEdit(self)
|
||||
layout.addWidget(self.console)
|
||||
self.console_edit = ConsoleEdit(self)
|
||||
layout.addWidget(self.console_edit)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
|
@ -44,7 +47,7 @@ class ConsoleDialog(QDialog):
|
|||
|
||||
self.clear_button = QPushButton(self.tr("Clear console"))
|
||||
button_layout.addWidget(self.clear_button)
|
||||
self.clear_button.clicked.connect(self.console.clear)
|
||||
self.clear_button.clicked.connect(self.console_edit.clear)
|
||||
|
||||
button_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed))
|
||||
|
||||
|
@ -62,7 +65,7 @@ class ConsoleDialog(QDialog):
|
|||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.env_variables = ConsoleEnv(self)
|
||||
self.env_variables = ConsoleEnv(app_title, self)
|
||||
self.env_variables.hide()
|
||||
|
||||
self.accept_close = False
|
||||
|
@ -100,7 +103,7 @@ class ConsoleDialog(QDialog):
|
|||
if "." not in file:
|
||||
file += ".log"
|
||||
with open(file, "w") as f:
|
||||
f.write(self.console.toPlainText())
|
||||
f.write(self.console_edit.toPlainText())
|
||||
f.close()
|
||||
self.save_button.setText(self.tr("Saved"))
|
||||
|
||||
|
@ -111,15 +114,21 @@ class ConsoleDialog(QDialog):
|
|||
self.env_variables.setTable(self.env)
|
||||
self.env_variables.show()
|
||||
|
||||
def log(self, text: str, end: str = "\n"):
|
||||
self.console.log(text + end)
|
||||
def log(self, text: str):
|
||||
self.console_edit.log(f"Rare: {text}")
|
||||
|
||||
def error(self, text, end: str = "\n"):
|
||||
self.console.error(text + end)
|
||||
def log_stdout(self, text: str):
|
||||
self.console_edit.log(text)
|
||||
|
||||
def error(self, text):
|
||||
self.console_edit.error(f"Rare: {text}")
|
||||
|
||||
def log_stderr(self, text):
|
||||
self.console_edit.error(text)
|
||||
|
||||
def on_process_exit(self, app_title: str, status: Union[int, str]):
|
||||
self.error(
|
||||
self.tr("Application \"{}\" finished with \"{}\"\n").format(app_title, status)
|
||||
self.tr("Application finished with exit code \"{}\"").format(status)
|
||||
)
|
||||
self.accept_close = True
|
||||
|
||||
|
@ -140,11 +149,14 @@ class ConsoleDialog(QDialog):
|
|||
|
||||
class ConsoleEnv(QDialog):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, app_title: str, parent=None):
|
||||
super(ConsoleEnv, self).__init__(parent=parent)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, False)
|
||||
self.ui = Ui_ConsoleEnv()
|
||||
self.ui.setupUi(self)
|
||||
self.setWindowTitle(
|
||||
dialog_title(game_title(self.tr("Environment"), app_title))
|
||||
)
|
||||
|
||||
def setTable(self, env: QProcessEnvironment):
|
||||
self.ui.table.clearContents()
|
||||
|
@ -165,17 +177,6 @@ class ConsoleEdit(QPlainTextEdit):
|
|||
font = QFont("Monospace")
|
||||
font.setStyleHint(QFont.Monospace)
|
||||
self.setFont(font)
|
||||
self._cursor_output = self.textCursor()
|
||||
|
||||
def log(self, text):
|
||||
html = f"<p style=\"color:#BBB;white-space:pre\">{text}</p>"
|
||||
self._cursor_output.insertHtml(html)
|
||||
self.scroll_to_last_line()
|
||||
|
||||
def error(self, text):
|
||||
html = f"<p style=\"color:#eee;white-space:pre\">{text}</p>"
|
||||
self._cursor_output.insertHtml(html)
|
||||
self.scroll_to_last_line()
|
||||
|
||||
def scroll_to_last_line(self):
|
||||
cursor = self.textCursor()
|
||||
|
@ -184,3 +185,14 @@ class ConsoleEdit(QPlainTextEdit):
|
|||
QTextCursor.Up if cursor.atBlockStart() else QTextCursor.StartOfLine
|
||||
)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def print_to_console(self, text: str, color: str):
|
||||
html = f"<p style=\"color:{color};white-space:pre\">{text}</p>"
|
||||
self.appendHtml(html)
|
||||
self.scroll_to_last_line()
|
||||
|
||||
def log(self, text):
|
||||
self.print_to_console(text, "#aaa")
|
||||
|
||||
def error(self, text):
|
||||
self.print_to_console(text, "#a33")
|
|
@ -11,7 +11,7 @@ from legendary.models.game import LaunchParameters
|
|||
|
||||
from rare.models.base_game import RareGameSlim
|
||||
|
||||
logger = getLogger("Helper")
|
||||
logger = getLogger("RareLauncherHelper")
|
||||
|
||||
|
||||
class GameArgsError(Exception):
|
||||
|
@ -156,7 +156,7 @@ def get_launch_args(rgame: RareGameSlim, init_args: InitArgs = None) -> LaunchAr
|
|||
if not rgame.is_installed:
|
||||
raise GameArgsError("Game is not installed or has unsupported format")
|
||||
|
||||
if rgame.is_dlc:
|
||||
if rgame.is_dlc and not rgame.is_launchable_addon:
|
||||
raise GameArgsError("Game is a DLC")
|
||||
if not os.path.exists(rgame.install_path):
|
||||
raise GameArgsError("Game path does not exist")
|
13
rare/commands/webview.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import sys
|
||||
from argparse import Namespace
|
||||
|
||||
from legendary.utils import webview_login
|
||||
|
||||
|
||||
def launch(args: Namespace) -> int:
|
||||
if webview_login.do_webview_login(
|
||||
callback_code=sys.stdout.write, user_agent=f'EpicGamesLauncher/{args.egl_version}'
|
||||
):
|
||||
return 0
|
||||
else:
|
||||
return 1
|
|
@ -9,6 +9,7 @@ from PyQt5.QtCore import QThreadPool, QTimer, pyqtSlot, Qt
|
|||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
from requests import HTTPError
|
||||
|
||||
from rare.models.options import options
|
||||
from rare.components.dialogs.launch_dialog import LaunchDialog
|
||||
from rare.components.main_window import MainWindow
|
||||
from rare.shared import RareCore
|
||||
|
@ -45,8 +46,8 @@ class Rare(RareApp):
|
|||
self.signals = RareCore.instance().signals()
|
||||
self.core = RareCore.instance().core()
|
||||
|
||||
lang = self.settings.value("language", self.core.language_code, type=str)
|
||||
self.load_translator(lang)
|
||||
language = self.settings.value(*options.language)
|
||||
self.load_translator(language)
|
||||
|
||||
# set Application name for settings
|
||||
self.main_window: Optional[MainWindow] = None
|
||||
|
|
|
@ -13,9 +13,9 @@ from rare.models.install import InstallDownloadModel, InstallQueueItemModel, Ins
|
|||
from rare.shared.workers.install_info import InstallInfoWorker
|
||||
from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
|
||||
from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced
|
||||
from rare.utils.misc import format_size, icon
|
||||
from rare.utils.misc import format_size, qta_icon
|
||||
from rare.widgets.collapsible_widget import CollapsibleFrame
|
||||
from rare.widgets.dialogs import ActionDialog, dialog_title_game
|
||||
from rare.widgets.dialogs import ActionDialog, game_title
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
from rare.widgets.selective_widget import SelectiveWidget
|
||||
|
||||
|
@ -63,19 +63,19 @@ class InstallDialog(ActionDialog):
|
|||
super(InstallDialog, self).__init__(parent=parent)
|
||||
|
||||
header = self.tr("Install")
|
||||
bicon = icon("ri.install-line")
|
||||
bicon = qta_icon("ri.install-line")
|
||||
if options.repair_mode:
|
||||
header = self.tr("Repair")
|
||||
bicon = icon("fa.wrench")
|
||||
bicon = qta_icon("fa.wrench")
|
||||
if options.repair_and_update:
|
||||
header = self.tr("Repair and update")
|
||||
elif options.update:
|
||||
header = self.tr("Update")
|
||||
elif options.reset_sdl:
|
||||
header = self.tr("Modify")
|
||||
bicon = icon("fa.gear")
|
||||
self.setWindowTitle(dialog_title_game(header, rgame.app_title))
|
||||
self.setSubtitle(dialog_title_game(header, rgame.app_title))
|
||||
bicon = qta_icon("fa.gear")
|
||||
self.setWindowTitle(game_title(header, rgame.app_title))
|
||||
self.setSubtitle(game_title(header, rgame.app_title))
|
||||
|
||||
install_widget = QWidget(self)
|
||||
self.ui = Ui_InstallDialog()
|
||||
|
@ -198,7 +198,7 @@ class InstallDialog(ActionDialog):
|
|||
self.accept_button.setObjectName("InstallButton")
|
||||
|
||||
self.action_button.setText(self.tr("Verify"))
|
||||
self.action_button.setIcon(icon("fa.check"))
|
||||
self.action_button.setIcon(qta_icon("fa.check"))
|
||||
|
||||
self.setCentralWidget(install_widget)
|
||||
|
||||
|
@ -258,10 +258,12 @@ class InstallDialog(ActionDialog):
|
|||
def action_handler(self):
|
||||
self.error_box()
|
||||
message = self.tr("Updating...")
|
||||
font = self.font()
|
||||
font.setItalic(True)
|
||||
self.ui.download_size_text.setText(message)
|
||||
self.ui.download_size_text.setStyleSheet("font-style: italic; font-weight: normal")
|
||||
self.ui.download_size_text.setFont(font)
|
||||
self.ui.install_size_text.setText(message)
|
||||
self.ui.install_size_text.setStyleSheet("font-style: italic; font-weight: normal")
|
||||
self.ui.install_size_text.setFont(font)
|
||||
self.setActive(True)
|
||||
self.options_changed = False
|
||||
self.get_options()
|
||||
|
@ -309,15 +311,19 @@ class InstallDialog(ActionDialog):
|
|||
download_size = download.analysis.dl_size
|
||||
install_size = download.analysis.install_size
|
||||
# install_size = self.dl_item.download.analysis.disk_space_delta
|
||||
bold_font = self.font()
|
||||
bold_font.setBold(True)
|
||||
italic_font = self.font()
|
||||
italic_font.setItalic(True)
|
||||
if download_size or (not download_size and (download.game.is_dlc or download.repair)):
|
||||
self.ui.download_size_text.setText(format_size(download_size))
|
||||
self.ui.download_size_text.setStyleSheet("font-style: normal; font-weight: bold")
|
||||
self.ui.download_size_text.setFont(bold_font)
|
||||
self.accept_button.setEnabled(not self.options_changed)
|
||||
else:
|
||||
self.ui.install_size_text.setText(self.tr("Game already installed"))
|
||||
self.ui.install_size_text.setStyleSheet("font-style: italics; font-weight: normal")
|
||||
self.ui.download_size_text.setText(self.tr("Game already installed"))
|
||||
self.ui.download_size_text.setFont(italic_font)
|
||||
self.ui.install_size_text.setText(format_size(install_size))
|
||||
self.ui.install_size_text.setStyleSheet("font-style: normal; font-weight: bold")
|
||||
self.ui.install_size_text.setFont(bold_font)
|
||||
self.action_button.setEnabled(self.options_changed)
|
||||
has_prereqs = bool(download.igame.prereq_info) and not download.igame.prereq_info.get("installed", False)
|
||||
if has_prereqs:
|
||||
|
|
|
@ -7,7 +7,7 @@ from legendary.core import LegendaryCore
|
|||
from rare.shared import ArgumentsSingleton
|
||||
from rare.ui.components.dialogs.login.landing_page import Ui_LandingPage
|
||||
from rare.ui.components.dialogs.login.login_dialog import Ui_LoginDialog
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import BaseDialog
|
||||
from rare.widgets.sliding_stack import SlidingStackedWidget
|
||||
from .browser_login import BrowserLogin
|
||||
|
@ -99,9 +99,9 @@ class LoginDialog(BaseDialog):
|
|||
|
||||
self.login_stack.setCurrentWidget(self.landing_page)
|
||||
|
||||
self.ui.exit_button.setIcon(icon("fa.remove"))
|
||||
self.ui.back_button.setIcon(icon("fa.chevron-left"))
|
||||
self.ui.next_button.setIcon(icon("fa.chevron-right"))
|
||||
self.ui.exit_button.setIcon(qta_icon("fa.remove"))
|
||||
self.ui.back_button.setIcon(qta_icon("fa.chevron-left"))
|
||||
self.ui.next_button.setIcon(qta_icon("fa.chevron-right"))
|
||||
|
||||
# lk: Set next as the default button only to stop closing the dialog when pressing enter
|
||||
self.ui.exit_button.setAutoDefault(False)
|
||||
|
|
|
@ -2,14 +2,15 @@ import json
|
|||
from logging import getLogger
|
||||
from typing import Tuple
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QProcess, pyqtSlot
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtWidgets import QFrame, qApp, QFormLayout, QLineEdit
|
||||
from legendary.core import LegendaryCore
|
||||
from legendary.utils import webview_login
|
||||
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.ui.components.dialogs.login.browser_login import Ui_BrowserLogin
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.utils.paths import get_rare_executable
|
||||
from rare.widgets.indicator_edit import IndicatorLineEdit, IndicatorReasonsCommon
|
||||
|
||||
logger = getLogger("BrowserLogin")
|
||||
|
@ -33,7 +34,7 @@ class BrowserLogin(QFrame):
|
|||
)
|
||||
self.sid_edit.line_edit.setEchoMode(QLineEdit.Password)
|
||||
self.ui.link_text.setText(self.login_url)
|
||||
self.ui.copy_button.setIcon(icon("mdi.content-copy", "fa.copy"))
|
||||
self.ui.copy_button.setIcon(qta_icon("mdi.content-copy", "fa.copy"))
|
||||
self.ui.copy_button.clicked.connect(self.copy_link)
|
||||
self.ui.form_layout.setWidget(
|
||||
self.ui.form_layout.getWidgetPosition(self.ui.sid_label)[0],
|
||||
|
@ -43,6 +44,7 @@ class BrowserLogin(QFrame):
|
|||
self.ui.open_button.clicked.connect(self.open_browser)
|
||||
self.sid_edit.textChanged.connect(self.changed.emit)
|
||||
|
||||
@pyqtSlot()
|
||||
def copy_link(self):
|
||||
clipboard = qApp.clipboard()
|
||||
clipboard.setText(self.login_url)
|
||||
|
@ -79,12 +81,24 @@ class BrowserLogin(QFrame):
|
|||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
@pyqtSlot()
|
||||
def open_browser(self):
|
||||
if not webview_login.webview_available:
|
||||
logger.warning("You don't have webengine installed, you will need to manually copy the authorizationCode.")
|
||||
QDesktopServices.openUrl(QUrl(self.login_url))
|
||||
else:
|
||||
if webview_login.do_webview_login(callback_code=self.core.auth_ex_token):
|
||||
cmd = get_rare_executable() + ["login", self.core.get_egl_version()]
|
||||
proc = QProcess(self)
|
||||
proc.start(cmd[0], cmd[1:])
|
||||
proc.waitForFinished(-1)
|
||||
out, err = (
|
||||
proc.readAllStandardOutput().data().decode("utf-8", "ignore"),
|
||||
proc.readAllStandardError().data().decode("utf-8", "ignore")
|
||||
)
|
||||
proc.deleteLater()
|
||||
|
||||
if out:
|
||||
self.core.auth_ex_token(out)
|
||||
logger.info("Successfully logged in as %s", {self.core.lgd.userdata['displayName']})
|
||||
self.success.emit()
|
||||
else:
|
||||
|
|
|
@ -54,7 +54,7 @@ class ImportLogin(QFrame):
|
|||
else:
|
||||
self.ui.status_label.setText(self.text_egl_notfound)
|
||||
|
||||
self.ui.prefix_tool.clicked.connect(self.prefix_path)
|
||||
self.ui.prefix_button.clicked.connect(self.prefix_path)
|
||||
self.ui.prefix_combo.editTextChanged.connect(self.changed.emit)
|
||||
|
||||
def get_wine_prefixes(self):
|
||||
|
|
|
@ -10,8 +10,8 @@ from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QFileDialog, QLayo
|
|||
from rare.models.install import MoveGameModel
|
||||
from rare.models.game import RareGame
|
||||
from rare.shared import RareCore
|
||||
from rare.utils.misc import path_size, format_size, icon
|
||||
from rare.widgets.dialogs import ActionDialog, dialog_title_game
|
||||
from rare.utils.misc import path_size, format_size, qta_icon
|
||||
from rare.widgets.dialogs import ActionDialog, game_title
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasons, IndicatorReasonsCommon
|
||||
|
||||
|
@ -33,8 +33,8 @@ class MoveDialog(ActionDialog):
|
|||
def __init__(self, rgame: RareGame, parent=None):
|
||||
super(MoveDialog, self).__init__(parent=parent)
|
||||
header = self.tr("Move")
|
||||
self.setWindowTitle(dialog_title_game(header, rgame.app_title))
|
||||
self.setSubtitle(dialog_title_game(header, rgame.app_title))
|
||||
self.setWindowTitle(game_title(header, rgame.app_title))
|
||||
self.setSubtitle(game_title(header, rgame.app_title))
|
||||
|
||||
self.rcore = RareCore.instance()
|
||||
self.core = RareCore.instance().core()
|
||||
|
@ -76,7 +76,7 @@ class MoveDialog(ActionDialog):
|
|||
self.setCentralLayout(layout)
|
||||
|
||||
self.accept_button.setText(self.tr("Move"))
|
||||
self.accept_button.setIcon(icon("mdi.folder-move-outline"))
|
||||
self.accept_button.setIcon(qta_icon("mdi.folder-move-outline"))
|
||||
|
||||
self.action_button.setHidden(True)
|
||||
|
||||
|
@ -135,7 +135,7 @@ class MoveDialog(ActionDialog):
|
|||
if not os.access(path, os.W_OK) or not os.access(self.rgame.install_path, os.W_OK):
|
||||
return helper_func(MovePathEditReasons.NO_WRITE_PERM)
|
||||
|
||||
if src_path == dst_path or src_path == dst_install_path:
|
||||
if src_path in {dst_path, dst_install_path}:
|
||||
return helper_func(MovePathEditReasons.SAME_DIR)
|
||||
|
||||
if str(src_path) in str(dst_path):
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QLayout, QGroupBox
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QGroupBox
|
||||
|
||||
from rare.models.game import RareGame
|
||||
from rare.models.install import SelectiveDownloadsModel
|
||||
from rare.utils.misc import icon
|
||||
from rare.widgets.dialogs import ButtonDialog, dialog_title_game
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import ButtonDialog, game_title
|
||||
from rare.widgets.selective_widget import SelectiveWidget
|
||||
|
||||
|
||||
|
@ -14,8 +14,8 @@ class SelectiveDialog(ButtonDialog):
|
|||
def __init__(self, rgame: RareGame, parent=None):
|
||||
super(SelectiveDialog, self).__init__(parent=parent)
|
||||
header = self.tr("Optional downloads for")
|
||||
self.setWindowTitle(dialog_title_game(header, rgame.app_title))
|
||||
self.setSubtitle(dialog_title_game(header, rgame.app_title))
|
||||
self.setWindowTitle(game_title(header, rgame.app_title))
|
||||
self.setSubtitle(game_title(header, rgame.app_title))
|
||||
|
||||
self.rgame = rgame
|
||||
self.selective_widget = SelectiveWidget(rgame, rgame.igame.platform, self)
|
||||
|
@ -31,7 +31,7 @@ class SelectiveDialog(ButtonDialog):
|
|||
self.setCentralLayout(layout)
|
||||
|
||||
self.accept_button.setText(self.tr("Verify"))
|
||||
self.accept_button.setIcon(icon("fa.check"))
|
||||
self.accept_button.setIcon(qta_icon("fa.check"))
|
||||
|
||||
self.options: SelectiveDownloadsModel = SelectiveDownloadsModel(rgame.app_name)
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ from PyQt5.QtWidgets import (
|
|||
|
||||
from rare.models.game import RareGame
|
||||
from rare.models.install import UninstallOptionsModel
|
||||
from rare.utils.misc import icon
|
||||
from rare.widgets.dialogs import ButtonDialog, dialog_title_game
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import ButtonDialog, game_title
|
||||
|
||||
|
||||
class UninstallDialog(ButtonDialog):
|
||||
|
@ -16,8 +16,8 @@ class UninstallDialog(ButtonDialog):
|
|||
def __init__(self, rgame: RareGame, options: UninstallOptionsModel, parent=None):
|
||||
super(UninstallDialog, self).__init__(parent=parent)
|
||||
header = self.tr("Uninstall")
|
||||
self.setWindowTitle(dialog_title_game(header, rgame.app_title))
|
||||
self.setSubtitle(dialog_title_game(header, rgame.app_title))
|
||||
self.setWindowTitle(game_title(header, rgame.app_title))
|
||||
self.setSubtitle(game_title(header, rgame.app_title))
|
||||
|
||||
self.keep_files = QCheckBox(self.tr("Keep files"))
|
||||
self.keep_files.setChecked(bool(options.keep_files))
|
||||
|
@ -39,7 +39,7 @@ class UninstallDialog(ButtonDialog):
|
|||
self.setCentralLayout(layout)
|
||||
|
||||
self.accept_button.setText(self.tr("Uninstall"))
|
||||
self.accept_button.setIcon(icon("ri.uninstall-line"))
|
||||
self.accept_button.setIcon(qta_icon("ri.uninstall-line"))
|
||||
self.accept_button.setObjectName("UninstallButton")
|
||||
|
||||
if rgame.sdl_name is not None:
|
||||
|
|
|
@ -2,7 +2,7 @@ import os
|
|||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import Qt, QSettings, QTimer, QSize, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QCloseEvent, QCursor, QShowEvent
|
||||
from PyQt5.QtGui import QCloseEvent, QCursor
|
||||
from PyQt5.QtWidgets import (
|
||||
QMainWindow,
|
||||
QApplication,
|
||||
|
@ -16,6 +16,7 @@ from PyQt5.QtWidgets import (
|
|||
QHBoxLayout,
|
||||
)
|
||||
|
||||
from rare.models.options import options
|
||||
from rare.components.tabs import MainTabWidget
|
||||
from rare.components.tray_icon import TrayIcon
|
||||
from rare.shared import RareCore
|
||||
|
@ -93,8 +94,8 @@ class MainWindow(QMainWindow):
|
|||
# self.status_timer.start()
|
||||
|
||||
width, height = 1280, 720
|
||||
if self.settings.value("save_size", False, bool):
|
||||
width, height = self.settings.value("window_size", (width, height), tuple)
|
||||
if self.settings.value(*options.save_size):
|
||||
width, height = self.settings.value(*options.window_size)
|
||||
|
||||
self.resize(width, height)
|
||||
|
||||
|
@ -151,9 +152,9 @@ class MainWindow(QMainWindow):
|
|||
self._window_launched = True
|
||||
|
||||
def hide(self) -> None:
|
||||
if self.settings.value("save_size", False, bool):
|
||||
if self.settings.value(*options.save_size):
|
||||
size = self.size().width(), self.size().height()
|
||||
self.settings.setValue("window_size", size)
|
||||
self.settings.setValue(options.window_size.key, size)
|
||||
super(MainWindow, self).hide()
|
||||
|
||||
def toggle(self):
|
||||
|
@ -214,7 +215,7 @@ class MainWindow(QMainWindow):
|
|||
# lk: `accept_close` is set to `True` by the `close()` method, overrides exiting to tray in `closeEvent()`
|
||||
# lk: ensures exiting instead of hiding when `close()` is called programmatically
|
||||
if not self.__accept_close:
|
||||
if self.settings.value("sys_tray", True, bool):
|
||||
if self.settings.value(*options.sys_tray):
|
||||
self.hide()
|
||||
e.ignore()
|
||||
return
|
||||
|
|
|
@ -2,13 +2,13 @@ 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, ExitCodes
|
||||
from rare.utils.misc import qta_icon, ExitCodes
|
||||
from .account import AccountWidget
|
||||
from .downloads import DownloadsTab
|
||||
from .games import GamesTab
|
||||
from .settings import SettingsTab
|
||||
from .settings.debug import DebugSettings
|
||||
from .store import Shop
|
||||
from .store import StoreTab
|
||||
from .tab_widgets import MainTabBar, TabButtonWidget
|
||||
|
||||
|
||||
|
@ -18,6 +18,7 @@ class MainTabWidget(QTabWidget):
|
|||
|
||||
def __init__(self, parent):
|
||||
super(MainTabWidget, self).__init__(parent=parent)
|
||||
|
||||
self.rcore = RareCore.instance()
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.signals = GlobalSignalsSingleton()
|
||||
|
@ -31,14 +32,14 @@ class MainTabWidget(QTabWidget):
|
|||
self.games_index = self.addTab(self.games_tab, self.tr("Games"))
|
||||
|
||||
# Downloads Tab after Games Tab to use populated RareCore games list
|
||||
if not self.args.offline:
|
||||
self.downloads_tab = DownloadsTab(self)
|
||||
self.downloads_index = self.addTab(self.downloads_tab, "")
|
||||
self.downloads_tab.update_title.connect(self.__on_downloads_update_title)
|
||||
self.downloads_tab.update_queues_count()
|
||||
self.setTabEnabled(self.downloads_index, not self.args.offline)
|
||||
self.downloads_tab = DownloadsTab(self)
|
||||
self.downloads_index = self.addTab(self.downloads_tab, "")
|
||||
self.downloads_tab.update_title.connect(self.__on_downloads_update_title)
|
||||
self.downloads_tab.update_queues_count()
|
||||
self.setTabEnabled(self.downloads_index, not self.args.offline)
|
||||
|
||||
self.store_tab = Shop(self.core)
|
||||
if not self.args.offline:
|
||||
self.store_tab = StoreTab(self.core, parent=self)
|
||||
self.store_index = self.addTab(self.store_tab, self.tr("Store (Beta)"))
|
||||
self.setTabEnabled(self.store_index, not self.args.offline)
|
||||
|
||||
|
@ -54,22 +55,22 @@ class MainTabWidget(QTabWidget):
|
|||
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")
|
||||
account_button.setMenu(QMenu())
|
||||
account_button.menu().addAction(account_action)
|
||||
account_button = TabButtonWidget(qta_icon("mdi.account-circle", fallback="fa.user"), tooltip="Menu")
|
||||
account_menu = QMenu(account_button)
|
||||
account_menu.addAction(account_action)
|
||||
account_button.setMenu(account_menu)
|
||||
self.tab_bar.setTabButton(
|
||||
button_index, MainTabBar.RightSide, account_button
|
||||
)
|
||||
|
||||
self.settings_tab = SettingsTab(self)
|
||||
self.settings_index = self.addTab(self.settings_tab, icon("fa.gear"), "")
|
||||
self.settings_index = self.addTab(self.settings_tab, qta_icon("fa.gear"), "")
|
||||
self.settings_tab.about.update_available_ready.connect(
|
||||
lambda: self.tab_bar.setTabText(self.settings_index, "(!)")
|
||||
)
|
||||
|
||||
# Open game list on click on Games tab button
|
||||
self.tabBarClicked.connect(self.mouse_clicked)
|
||||
self.setIconSize(QSize(24, 24))
|
||||
|
||||
# shortcuts
|
||||
QShortcut("Alt+1", self).activated.connect(lambda: self.setCurrentIndex(self.games_index))
|
||||
|
|
|
@ -4,7 +4,7 @@ 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, ExitCodes
|
||||
from rare.utils.misc import qta_icon, ExitCodes
|
||||
|
||||
|
||||
class AccountWidget(QWidget):
|
||||
|
@ -20,7 +20,7 @@ class AccountWidget(QWidget):
|
|||
if not username:
|
||||
username = "Offline"
|
||||
|
||||
self.open_browser = QPushButton(icon("fa.external-link"), self.tr("Account settings"))
|
||||
self.open_browser = QPushButton(qta_icon("fa.external-link"), self.tr("Account settings"))
|
||||
self.open_browser.clicked.connect(
|
||||
lambda: webbrowser.open(
|
||||
"https://www.epicgames.com/account/personal?productName=epicgames"
|
||||
|
|
|
@ -15,6 +15,7 @@ from rare.components.dialogs.uninstall_dialog import UninstallDialog
|
|||
from rare.lgndr.models.downloading import UIUpdate
|
||||
from rare.models.game import RareGame
|
||||
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
|
||||
from rare.models.options import options
|
||||
from rare.shared import RareCore
|
||||
from rare.shared.workers.install_info import InstallInfoWorker
|
||||
from rare.shared.workers.uninstall import UninstallWorker
|
||||
|
@ -60,6 +61,8 @@ class DownloadsTab(QWidget):
|
|||
queue_contents = QWidget(self.queue_scrollarea)
|
||||
queue_contents.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.queue_scrollarea.setWidget(queue_contents)
|
||||
self.queue_scrollarea.widget().setAutoFillBackground(False)
|
||||
self.queue_scrollarea.viewport().setAutoFillBackground(False)
|
||||
|
||||
queue_contents_layout = QVBoxLayout(queue_contents)
|
||||
queue_contents_layout.setContentsMargins(0, 0, 3, 0)
|
||||
|
@ -105,9 +108,13 @@ class DownloadsTab(QWidget):
|
|||
def __add_update(self, update: Union[str, RareGame]):
|
||||
if isinstance(update, str):
|
||||
update = self.rcore.get_game(update)
|
||||
if QSettings().value(
|
||||
f"{update.app_name}/auto_update", False, bool
|
||||
) or QSettings().value("auto_update", False, bool):
|
||||
|
||||
auto_update = QSettings(self).value(
|
||||
f"{update.app_name}/{options.auto_update.key}",
|
||||
QSettings(self).value(*options.auto_update),
|
||||
options.auto_update.dtype
|
||||
)
|
||||
if auto_update:
|
||||
self.__get_install_options(
|
||||
InstallOptionsModel(app_name=update.app_name, update=True, silent=True)
|
||||
)
|
||||
|
|
|
@ -52,16 +52,18 @@ class DownloadWidget(ImageWidget):
|
|||
# lk: trade some possible delay and start-up time
|
||||
# lk: for faster rendering. Gradients are expensive
|
||||
# lk: so pre-generate the image
|
||||
super(DownloadWidget, self).setPixmap(self.prepare_pixmap(pixmap))
|
||||
if not pixmap.isNull():
|
||||
pixmap = self.prepare_pixmap(pixmap)
|
||||
super(DownloadWidget, self).setPixmap(pixmap)
|
||||
|
||||
def paint_image_empty(self, painter: QPainter, a0: QPaintEvent) -> None:
|
||||
# when pixmap object is not available yet, show a gray rectangle
|
||||
painter.setOpacity(0.5 * self._opacity)
|
||||
painter.fillRect(a0.rect(), self.palette().color(QPalette.Background))
|
||||
painter.fillRect(a0.rect(), self.palette().color(QPalette.Window))
|
||||
|
||||
def paint_image_cover(self, painter: QPainter, a0: QPaintEvent) -> None:
|
||||
painter.setOpacity(self._opacity)
|
||||
color = self.palette().color(QPalette.Background).darker(75)
|
||||
color = self.palette().color(QPalette.Window).darker(75)
|
||||
painter.fillRect(self.rect(), color)
|
||||
brush = QBrush(self._pixmap)
|
||||
brush.setTransform(self._transform)
|
||||
|
|
|
@ -55,7 +55,7 @@ class DlThread(QThread):
|
|||
if result.code == DlResultCode.FINISHED:
|
||||
self.rgame.set_installed(True)
|
||||
self.rgame.state = RareGame.State.IDLE
|
||||
self.rgame.signals.progress.finish.emit(not result.code == DlResultCode.FINISHED)
|
||||
self.rgame.signals.progress.finish.emit(result.code != DlResultCode.FINISHED)
|
||||
self.result.emit(result)
|
||||
|
||||
def __status_callback(self, status: UIUpdate):
|
||||
|
@ -125,15 +125,14 @@ class DlThread(QThread):
|
|||
dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name)
|
||||
if dlcs and not self.item.options.skip_dlcs:
|
||||
result.dlcs = []
|
||||
for dlc in dlcs:
|
||||
result.dlcs.append(
|
||||
{
|
||||
"app_name": dlc.app_name,
|
||||
"app_title": dlc.app_title,
|
||||
"app_version": dlc.app_version(self.item.options.platform),
|
||||
}
|
||||
)
|
||||
|
||||
result.dlcs.extend(
|
||||
{
|
||||
"app_name": dlc.app_name,
|
||||
"app_title": dlc.app_title,
|
||||
"app_version": dlc.app_version(self.item.options.platform),
|
||||
}
|
||||
for dlc in dlcs
|
||||
)
|
||||
if (
|
||||
self.item.download.game.supports_cloud_saves
|
||||
or self.item.download.game.supports_mac_cloud_saves
|
||||
|
|
|
@ -12,10 +12,9 @@ from rare.shared import (
|
|||
ImageManagerSingleton,
|
||||
)
|
||||
from rare.shared import RareCore
|
||||
from rare.widgets.library_layout import LibraryLayout
|
||||
from rare.widgets.sliding_stack import SlidingStackedWidget
|
||||
from rare.models.options import options
|
||||
from .game_info import GameInfoTabs
|
||||
from .game_widgets import LibraryWidgetController
|
||||
from .game_widgets import LibraryWidgetController, LibraryFilter, LibraryOrder, LibraryView
|
||||
from .game_widgets.icon_game_widget import IconGameWidget
|
||||
from .game_widgets.list_game_widget import ListGameWidget
|
||||
from .head_bar import GameListHeadBar
|
||||
|
@ -53,51 +52,22 @@ class GamesTab(QStackedWidget):
|
|||
self.integrations_page.back_clicked.connect(lambda: self.setCurrentWidget(self.games_page))
|
||||
self.addWidget(self.integrations_page)
|
||||
|
||||
self.view_stack = SlidingStackedWidget(self.games_page)
|
||||
self.view_stack.setFrameStyle(QFrame.NoFrame)
|
||||
self.view_scroll = QScrollArea(self.games_page)
|
||||
self.view_scroll.setWidgetResizable(True)
|
||||
self.view_scroll.setFrameShape(QFrame.StyledPanel)
|
||||
self.view_scroll.horizontalScrollBar().setDisabled(True)
|
||||
|
||||
self.icon_view_scroll = QScrollArea(self.view_stack)
|
||||
self.icon_view_scroll.setWidgetResizable(True)
|
||||
self.icon_view_scroll.setFrameShape(QFrame.StyledPanel)
|
||||
self.icon_view_scroll.horizontalScrollBar().setDisabled(True)
|
||||
library_view = LibraryView(self.settings.value(*options.library_view))
|
||||
self.library_controller = LibraryWidgetController(library_view, self.view_scroll)
|
||||
games_page_layout.addWidget(self.view_scroll)
|
||||
|
||||
self.list_view_scroll = QScrollArea(self.view_stack)
|
||||
self.list_view_scroll.setWidgetResizable(True)
|
||||
self.list_view_scroll.setFrameShape(QFrame.StyledPanel)
|
||||
self.list_view_scroll.horizontalScrollBar().setDisabled(True)
|
||||
|
||||
self.icon_view = QWidget(self.icon_view_scroll)
|
||||
icon_view_layout = LibraryLayout(self.icon_view)
|
||||
icon_view_layout.setSpacing(9)
|
||||
icon_view_layout.setContentsMargins(0, 13, 0, 13)
|
||||
icon_view_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.list_view = QWidget(self.list_view_scroll)
|
||||
list_view_layout = QVBoxLayout(self.list_view)
|
||||
list_view_layout.setContentsMargins(3, 3, 9, 3)
|
||||
list_view_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.library_controller = LibraryWidgetController(self.icon_view, self.list_view, self)
|
||||
self.icon_view_scroll.setWidget(self.icon_view)
|
||||
self.list_view_scroll.setWidget(self.list_view)
|
||||
self.view_stack.addWidget(self.icon_view_scroll)
|
||||
self.view_stack.addWidget(self.list_view_scroll)
|
||||
games_page_layout.addWidget(self.view_stack)
|
||||
|
||||
if not self.settings.value("icon_view", True, bool):
|
||||
self.view_stack.setCurrentWidget(self.list_view_scroll)
|
||||
self.head_bar.view.list()
|
||||
else:
|
||||
self.view_stack.setCurrentWidget(self.icon_view_scroll)
|
||||
|
||||
self.head_bar.search_bar.textChanged.connect(lambda x: self.filter_games("", x))
|
||||
self.head_bar.search_bar.textChanged.connect(self.search_games)
|
||||
self.head_bar.search_bar.textChanged.connect(self.scroll_to_top)
|
||||
self.head_bar.filterChanged.connect(self.filter_games)
|
||||
self.head_bar.filterChanged.connect(self.scroll_to_top)
|
||||
self.head_bar.refresh_list.clicked.connect(self.library_controller.update_list)
|
||||
self.head_bar.view.toggled.connect(self.toggle_view)
|
||||
|
||||
self.active_filter: str = self.head_bar.filter.currentData(Qt.UserRole)
|
||||
self.head_bar.orderChanged.connect(self.order_games)
|
||||
self.head_bar.orderChanged.connect(self.scroll_to_top)
|
||||
self.head_bar.refresh_list.clicked.connect(self.library_controller.update_game_views)
|
||||
|
||||
# signals
|
||||
self.signals.game.installed.connect(self.update_count_games_label)
|
||||
|
@ -114,11 +84,8 @@ class GamesTab(QStackedWidget):
|
|||
|
||||
@pyqtSlot()
|
||||
def scroll_to_top(self):
|
||||
self.icon_view_scroll.verticalScrollBar().setSliderPosition(
|
||||
self.icon_view_scroll.verticalScrollBar().minimum()
|
||||
)
|
||||
self.list_view_scroll.verticalScrollBar().setSliderPosition(
|
||||
self.list_view_scroll.verticalScrollBar().minimum()
|
||||
self.view_scroll.verticalScrollBar().setSliderPosition(
|
||||
self.view_scroll.verticalScrollBar().minimum()
|
||||
)
|
||||
|
||||
@pyqtSlot()
|
||||
|
@ -139,8 +106,8 @@ class GamesTab(QStackedWidget):
|
|||
|
||||
@pyqtSlot(RareGame)
|
||||
def show_game_info(self, rgame):
|
||||
self.setCurrentWidget(self.game_info_page)
|
||||
self.game_info_page.update_game(rgame)
|
||||
self.setCurrentWidget(self.game_info_page)
|
||||
|
||||
@pyqtSlot()
|
||||
def update_count_games_label(self):
|
||||
|
@ -151,42 +118,38 @@ class GamesTab(QStackedWidget):
|
|||
|
||||
def setup_game_list(self):
|
||||
for rgame in self.rcore.games:
|
||||
icon_widget, list_widget = self.add_library_widget(rgame)
|
||||
if not icon_widget or not list_widget:
|
||||
widget = self.add_library_widget(rgame)
|
||||
if not widget:
|
||||
logger.warning("Excluding %s from the game list", rgame.app_title)
|
||||
continue
|
||||
self.icon_view.layout().addWidget(icon_widget)
|
||||
self.list_view.layout().addWidget(list_widget)
|
||||
self.filter_games(self.active_filter)
|
||||
self.filter_games(self.head_bar.current_filter())
|
||||
self.update_count_games_label()
|
||||
|
||||
def add_library_widget(self, rgame: RareGame):
|
||||
try:
|
||||
icon_widget, list_widget = self.library_controller.add_game(rgame)
|
||||
widget = self.library_controller.add_game(rgame)
|
||||
except Exception as e:
|
||||
logger.error("Could not add widget for %s to library: %s", rgame.app_name, e)
|
||||
return None, None
|
||||
icon_widget.show_info.connect(self.show_game_info)
|
||||
list_widget.show_info.connect(self.show_game_info)
|
||||
return icon_widget, list_widget
|
||||
return None
|
||||
widget.show_info.connect(self.show_game_info)
|
||||
return widget
|
||||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot(str, str)
|
||||
def filter_games(self, filter_name="all", search_text: str = ""):
|
||||
def search_games(self, search_text: str = ""):
|
||||
self.filter_games(self.head_bar.current_filter(), search_text)
|
||||
|
||||
@pyqtSlot(object)
|
||||
@pyqtSlot(object, str)
|
||||
def filter_games(self, library_filter: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
|
||||
if not search_text and (t := self.head_bar.search_bar.text()):
|
||||
search_text = t
|
||||
|
||||
if filter_name:
|
||||
self.active_filter = filter_name
|
||||
if not filter_name and (t := self.active_filter):
|
||||
filter_name = t
|
||||
self.library_controller.filter_game_views(library_filter, search_text.lower())
|
||||
|
||||
self.library_controller.filter_list(filter_name, search_text.lower())
|
||||
@pyqtSlot(object)
|
||||
@pyqtSlot(object, str)
|
||||
def order_games(self, library_order: LibraryOrder = LibraryFilter.ALL, search_text: str = ""):
|
||||
if not search_text and (t := self.head_bar.search_bar.text()):
|
||||
search_text = t
|
||||
|
||||
def toggle_view(self):
|
||||
self.settings.setValue("icon_view", not self.head_bar.view.isChecked())
|
||||
|
||||
if not self.head_bar.view.isChecked():
|
||||
self.view_stack.slideInWidget(self.icon_view_scroll)
|
||||
else:
|
||||
self.view_stack.slideInWidget(self.list_view_scroll)
|
||||
self.library_controller.order_game_views(library_order, search_text.lower())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
from PyQt5.QtWidgets import QTreeView
|
||||
|
||||
|
@ -8,9 +8,9 @@ from rare.models.game import RareGame
|
|||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
||||
from rare.utils.json_formatter import QJsonModel
|
||||
from rare.widgets.side_tab import SideTabWidget, SideTabContents
|
||||
from .game_dlc import GameDlc
|
||||
from .game_info import GameInfo
|
||||
from .game_settings import GameSettings
|
||||
from .dlcs import GameDlcs
|
||||
from .details import GameDetails
|
||||
from .settings import GameSettings
|
||||
from .cloud_saves import CloudSaves
|
||||
|
||||
|
||||
|
@ -24,9 +24,9 @@ class GameInfoTabs(SideTabWidget):
|
|||
self.signals = GlobalSignalsSingleton()
|
||||
self.args = ArgumentsSingleton()
|
||||
|
||||
self.info_tab = GameInfo(self)
|
||||
self.info_tab.import_clicked.connect(self.import_clicked)
|
||||
self.info_index = self.addTab(self.info_tab, self.tr("Information"))
|
||||
self.details_tab = GameDetails(self)
|
||||
self.details_tab.import_clicked.connect(self.import_clicked)
|
||||
self.details_index = self.addTab(self.details_tab, self.tr("Information"))
|
||||
|
||||
self.settings_tab = GameSettings(self)
|
||||
self.settings_index = self.addTab(self.settings_tab, self.tr("Settings"))
|
||||
|
@ -34,8 +34,8 @@ class GameInfoTabs(SideTabWidget):
|
|||
self.cloud_saves_tab = CloudSaves(self)
|
||||
self.cloud_saves_index = self.addTab(self.cloud_saves_tab, self.tr("Cloud Saves"))
|
||||
|
||||
self.dlc_tab = GameDlc(self)
|
||||
self.dlc_index = self.addTab(self.dlc_tab, self.tr("Downloadable Content"))
|
||||
self.dlcs_tab = GameDlcs(self)
|
||||
self.dlcs_index = self.addTab(self.dlcs_tab, self.tr("Downloadable Content"))
|
||||
|
||||
# FIXME: Hiding didn't work, so don't add these tabs in normal mode. Fix this properly later
|
||||
if self.args.debug:
|
||||
|
@ -43,17 +43,19 @@ class GameInfoTabs(SideTabWidget):
|
|||
self.game_meta_index = self.addTab(self.game_meta_view, self.tr("Game Metadata"))
|
||||
self.igame_meta_view = GameMetadataView(self)
|
||||
self.igame_meta_index = self.addTab(self.igame_meta_view, self.tr("InstalledGame Metadata"))
|
||||
self.rgame_meta_view = GameMetadataView(self)
|
||||
self.rgame_meta_index = self.addTab(self.rgame_meta_view, self.tr("RareGame Metadata"))
|
||||
|
||||
self.setCurrentIndex(self.info_index)
|
||||
self.setCurrentIndex(self.details_index)
|
||||
|
||||
def update_game(self, rgame: RareGame):
|
||||
self.info_tab.update_game(rgame)
|
||||
self.details_tab.update_game(rgame)
|
||||
|
||||
self.settings_tab.load_settings(rgame)
|
||||
self.settings_tab.setEnabled(rgame.is_installed or rgame.is_origin)
|
||||
|
||||
self.dlc_tab.update_dlcs(rgame)
|
||||
self.dlc_tab.setEnabled(rgame.is_installed and bool(rgame.owned_dlcs))
|
||||
self.dlcs_tab.update_dlcs(rgame)
|
||||
self.dlcs_tab.setEnabled(rgame.is_installed and bool(rgame.owned_dlcs))
|
||||
|
||||
self.cloud_saves_tab.update_game(rgame)
|
||||
# self.cloud_saves_tab.setEnabled(rgame.game.supports_cloud_saves or rgame.game.supports_mac_cloud_saves)
|
||||
|
@ -61,11 +63,12 @@ class GameInfoTabs(SideTabWidget):
|
|||
if self.args.debug:
|
||||
self.game_meta_view.update_game(rgame, rgame.game)
|
||||
self.igame_meta_view.update_game(rgame, rgame.igame)
|
||||
self.rgame_meta_view.update_game(rgame, rgame.metadata)
|
||||
|
||||
self.setCurrentIndex(self.info_index)
|
||||
self.setCurrentIndex(self.details_index)
|
||||
|
||||
def keyPressEvent(self, e: QKeyEvent):
|
||||
if e.key() == Qt.Key_Escape:
|
||||
def keyPressEvent(self, a0: QKeyEvent):
|
||||
if a0.key() == Qt.Key_Escape:
|
||||
self.back_clicked.emit()
|
||||
|
||||
|
||||
|
@ -75,6 +78,7 @@ class GameMetadataView(QTreeView, SideTabContents):
|
|||
self.implements_scrollarea = True
|
||||
self.setColumnWidth(0, 300)
|
||||
self.setWordWrap(True)
|
||||
self.setEditTriggers(QTreeView.NoEditTriggers)
|
||||
self.model = QJsonModel()
|
||||
self.setModel(self.model)
|
||||
self.rgame: Optional[RareGame] = None
|
||||
|
|
|
@ -3,7 +3,7 @@ import platform
|
|||
from logging import getLogger
|
||||
from typing import Tuple
|
||||
|
||||
from PyQt5.QtCore import QThreadPool, QSettings
|
||||
from PyQt5.QtCore import QThreadPool, QSettings, pyqtSlot
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QFileDialog,
|
||||
|
@ -19,15 +19,16 @@ from legendary.models.game import SaveGameStatus
|
|||
|
||||
from rare.models.game import RareGame
|
||||
from rare.shared import RareCore
|
||||
from rare.shared.workers.wine_resolver import WineResolver
|
||||
from rare.shared.workers.wine_resolver import WineSavePathResolver
|
||||
from rare.ui.components.tabs.games.game_info.cloud_settings_widget import Ui_CloudSettingsWidget
|
||||
from rare.ui.components.tabs.games.game_info.cloud_sync_widget import Ui_CloudSyncWidget
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.utils.metrics import timelogger
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
from rare.widgets.loading_widget import LoadingWidget
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
|
||||
logger = getLogger("CloudWidget")
|
||||
logger = getLogger("CloudSaves")
|
||||
|
||||
|
||||
class CloudSaves(QWidget, SideTabContents):
|
||||
|
@ -44,8 +45,8 @@ class CloudSaves(QWidget, SideTabContents):
|
|||
self.core = RareCore.instance().core()
|
||||
self.settings = QSettings()
|
||||
|
||||
self.sync_ui.icon_local.setPixmap(icon("mdi.harddisk", "fa.desktop").pixmap(128, 128))
|
||||
self.sync_ui.icon_remote.setPixmap(icon("mdi.cloud-outline", "ei.cloud").pixmap(128, 128))
|
||||
self.sync_ui.icon_local.setPixmap(qta_icon("mdi.harddisk", "fa.desktop").pixmap(128, 128))
|
||||
self.sync_ui.icon_remote.setPixmap(qta_icon("mdi.cloud-outline", "ei.cloud").pixmap(128, 128))
|
||||
|
||||
self.sync_ui.upload_button.clicked.connect(self.upload)
|
||||
self.sync_ui.download_button.clicked.connect(self.download)
|
||||
|
@ -72,7 +73,7 @@ class CloudSaves(QWidget, SideTabContents):
|
|||
self.cloud_save_path_edit
|
||||
)
|
||||
|
||||
self.compute_save_path_button = QPushButton(icon("fa.magic"), self.tr("Calculate path"))
|
||||
self.compute_save_path_button = QPushButton(qta_icon("fa.magic"), self.tr("Calculate path"))
|
||||
self.compute_save_path_button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
self.compute_save_path_button.clicked.connect(self.compute_save_path)
|
||||
self.cloud_ui.main_layout.addRow(None, self.compute_save_path_button)
|
||||
|
@ -114,30 +115,32 @@ class CloudSaves(QWidget, SideTabContents):
|
|||
def compute_save_path(self):
|
||||
if self.rgame.is_installed and self.rgame.game.supports_cloud_saves:
|
||||
try:
|
||||
new_path = self.core.get_save_path(self.rgame.app_name)
|
||||
with timelogger(logger, "Detecting save path"):
|
||||
new_path = self.core.get_save_path(self.rgame.app_name)
|
||||
if platform.system() != "Windows" and not os.path.exists(new_path):
|
||||
raise ValueError(f'Path "{new_path}" does not exist.')
|
||||
except Exception as e:
|
||||
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
|
||||
resolver = WineSavePathResolver(self.core, self.rgame)
|
||||
# if not resolver.environ.get("WINEPREFIX"):
|
||||
# del resolver
|
||||
# self.cloud_save_path_edit.setText("")
|
||||
# QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
|
||||
# return
|
||||
self.cloud_save_path_edit.setText(self.tr("Loading..."))
|
||||
self.cloud_save_path_edit.setDisabled(True)
|
||||
self.compute_save_path_button.setDisabled(True)
|
||||
|
||||
app_name = self.rgame.app_name
|
||||
resolver.signals.result_ready.connect(lambda x: self.wine_resolver_finished(x, app_name))
|
||||
resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
|
||||
QThreadPool.globalInstance().start(resolver)
|
||||
return
|
||||
else:
|
||||
self.cloud_save_path_edit.setText(new_path)
|
||||
|
||||
def wine_resolver_finished(self, path, app_name):
|
||||
logger.info(f"Wine resolver finished for {app_name}. Computed save path: {path}")
|
||||
@pyqtSlot(str, str)
|
||||
def __on_wine_resolver_result(self, path, app_name):
|
||||
logger.info("Wine resolver finished for %s", app_name)
|
||||
logger.info("Computed save path: %s", path)
|
||||
if app_name == self.rgame.app_name:
|
||||
self.cloud_save_path_edit.setDisabled(False)
|
||||
self.compute_save_path_button.setDisabled(False)
|
||||
|
@ -158,8 +161,6 @@ class CloudSaves(QWidget, SideTabContents):
|
|||
self.cloud_save_path_edit.setText("")
|
||||
return
|
||||
self.cloud_save_path_edit.setText(path)
|
||||
elif path:
|
||||
self.rcore.get_game(app_name).save_path = path
|
||||
|
||||
def __update_widget(self):
|
||||
supports_saves = self.rgame.igame is not None and (
|
||||
|
|
|
@ -19,8 +19,8 @@ from rare.components.dialogs.selective_dialog import SelectiveDialog
|
|||
from rare.models.game import RareGame
|
||||
from rare.shared import RareCore
|
||||
from rare.shared.workers import VerifyWorker, MoveWorker
|
||||
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
|
||||
from rare.utils.misc import format_size, icon
|
||||
from rare.ui.components.tabs.games.game_info.details import Ui_GameDetails
|
||||
from rare.utils.misc import format_size, qta_icon, style_hyperlink
|
||||
from rare.widgets.image_widget import ImageWidget, ImageSize
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from rare.components.dialogs.move_dialog import MoveDialog, is_game_dir
|
||||
|
@ -28,27 +28,27 @@ from rare.components.dialogs.move_dialog import MoveDialog, is_game_dir
|
|||
logger = getLogger("GameInfo")
|
||||
|
||||
|
||||
class GameInfo(QWidget, SideTabContents):
|
||||
class GameDetails(QWidget, SideTabContents):
|
||||
# str: app_name
|
||||
import_clicked = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(GameInfo, self).__init__(parent=parent)
|
||||
self.ui = Ui_GameInfo()
|
||||
super(GameDetails, self).__init__(parent=parent)
|
||||
self.ui = Ui_GameDetails()
|
||||
self.ui.setupUi(self)
|
||||
# lk: set object names for CSS properties
|
||||
self.ui.install_button.setObjectName("InstallButton")
|
||||
self.ui.modify_button.setObjectName("InstallButton")
|
||||
self.ui.uninstall_button.setObjectName("UninstallButton")
|
||||
|
||||
self.ui.install_button.setIcon(icon("ri.install-line"))
|
||||
self.ui.import_button.setIcon(icon("mdi.application-import"))
|
||||
self.ui.install_button.setIcon(qta_icon("ri.install-line"))
|
||||
self.ui.import_button.setIcon(qta_icon("mdi.application-import"))
|
||||
|
||||
self.ui.modify_button.setIcon(icon("fa.gear"))
|
||||
self.ui.verify_button.setIcon(icon("fa.check"))
|
||||
self.ui.repair_button.setIcon(icon("fa.wrench"))
|
||||
self.ui.move_button.setIcon(icon("mdi.folder-move-outline"))
|
||||
self.ui.uninstall_button.setIcon(icon("ri.uninstall-line"))
|
||||
self.ui.modify_button.setIcon(qta_icon("fa.gear"))
|
||||
self.ui.verify_button.setIcon(qta_icon("fa.check"))
|
||||
self.ui.repair_button.setIcon(qta_icon("fa.wrench"))
|
||||
self.ui.move_button.setIcon(qta_icon("mdi.folder-move-outline"))
|
||||
self.ui.uninstall_button.setIcon(qta_icon("ri.uninstall-line"))
|
||||
|
||||
self.rcore = RareCore.instance()
|
||||
self.core = RareCore.instance().core()
|
||||
|
@ -270,8 +270,7 @@ class GameInfo(QWidget, SideTabContents):
|
|||
@pyqtSlot()
|
||||
def __update_widget(self):
|
||||
""" React to state updates from RareGame """
|
||||
# self.image.setPixmap(self.image_manager.get_pixmap(self.rgame.app_name, True))
|
||||
self.image.setPixmap(self.rgame.pixmap)
|
||||
self.image.setPixmap(self.rgame.get_pixmap(True))
|
||||
|
||||
self.ui.lbl_version.setDisabled(self.rgame.is_non_asset)
|
||||
self.ui.version.setDisabled(self.rgame.is_non_asset)
|
||||
|
@ -303,7 +302,12 @@ class GameInfo(QWidget, SideTabContents):
|
|||
self.ui.grade.setDisabled(
|
||||
self.rgame.is_unreal or platform.system() == "Windows"
|
||||
)
|
||||
self.ui.grade.setText(self.steam_grade_ratings[self.rgame.steam_grade()])
|
||||
self.ui.grade.setText(
|
||||
style_hyperlink(
|
||||
f"https://www.protondb.com/app/{self.rgame.steam_appid}",
|
||||
self.steam_grade_ratings[self.rgame.steam_grade()]
|
||||
)
|
||||
)
|
||||
|
||||
self.ui.install_button.setEnabled(
|
||||
(not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle
|
|
@ -6,11 +6,11 @@ from PyQt5.QtWidgets import QFrame, QMessageBox, QToolBox
|
|||
|
||||
from rare.models.game import RareGame
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc
|
||||
from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget
|
||||
from rare.ui.components.tabs.games.game_info.dlcs import Ui_GameDlcs
|
||||
from rare.ui.components.tabs.games.game_info.dlc_widget import Ui_GameDlcWidget
|
||||
from rare.widgets.image_widget import ImageWidget, ImageSize
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from rare.utils.misc import widget_object_name, icon
|
||||
from rare.utils.misc import widget_object_name, qta_icon
|
||||
|
||||
|
||||
class GameDlcWidget(QFrame):
|
||||
|
@ -57,7 +57,7 @@ class InstalledGameDlcWidget(GameDlcWidget):
|
|||
self.ui.action_button.setObjectName("UninstallButton")
|
||||
self.ui.action_button.clicked.connect(self.uninstall_dlc)
|
||||
self.ui.action_button.setText(self.tr("Uninstall DLC"))
|
||||
self.ui.action_button.setIcon(icon("ri.uninstall-line"))
|
||||
self.ui.action_button.setIcon(qta_icon("ri.uninstall-line"))
|
||||
# lk: don't reference `self.rdlc` here because the object has been deleted
|
||||
rdlc.signals.game.uninstalled.connect(self.__uninstalled)
|
||||
|
||||
|
@ -78,7 +78,7 @@ class AvailableGameDlcWidget(GameDlcWidget):
|
|||
self.ui.action_button.setObjectName("InstallButton")
|
||||
self.ui.action_button.clicked.connect(self.install_dlc)
|
||||
self.ui.action_button.setText(self.tr("Install DLC"))
|
||||
self.ui.action_button.setIcon(icon("ri.install-line"))
|
||||
self.ui.action_button.setIcon(qta_icon("ri.install-line"))
|
||||
|
||||
# lk: don't reference `self.rdlc` here because the object has been deleted
|
||||
rdlc.signals.game.installed.connect(self.__installed)
|
||||
|
@ -98,12 +98,12 @@ class AvailableGameDlcWidget(GameDlcWidget):
|
|||
self.rdlc.install()
|
||||
|
||||
|
||||
class GameDlc(QToolBox, SideTabContents):
|
||||
class GameDlcs(QToolBox, SideTabContents):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(GameDlc, self).__init__(parent=parent)
|
||||
super(GameDlcs, self).__init__(parent=parent)
|
||||
self.implements_scrollarea = True
|
||||
self.ui = Ui_GameDlc()
|
||||
self.ui = Ui_GameDlcs()
|
||||
self.ui.setupUi(self)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.signals = GlobalSignalsSingleton()
|
|
@ -1,139 +0,0 @@
|
|||
import os.path
|
||||
import platform
|
||||
from logging import getLogger
|
||||
from typing import Tuple
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QLabel, QFileDialog
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
|
||||
from rare.components.tabs.settings import DefaultGameSettings
|
||||
from rare.components.tabs.settings.widgets.pre_launch import PreLaunchSettings
|
||||
from rare.models.game import RareGame
|
||||
from rare.utils import config_helper
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
|
||||
logger = getLogger("GameSettings")
|
||||
|
||||
|
||||
class GameSettings(DefaultGameSettings, SideTabContents):
|
||||
def __init__(self, parent=None):
|
||||
super(GameSettings, self).__init__(False, parent=parent)
|
||||
self.pre_launch_settings = PreLaunchSettings()
|
||||
self.ui.launch_settings_group.layout().addRow(
|
||||
QLabel(self.tr("Pre-launch command")), self.pre_launch_settings
|
||||
)
|
||||
|
||||
self.ui.skip_update.currentIndexChanged.connect(
|
||||
lambda x: self.update_combobox("skip_update_check", x)
|
||||
)
|
||||
self.ui.offline.currentIndexChanged.connect(
|
||||
lambda x: self.update_combobox("offline", x)
|
||||
)
|
||||
self.ui.launch_params.textChanged.connect(
|
||||
lambda x: self.line_edit_save_callback("start_params", x)
|
||||
)
|
||||
|
||||
self.override_exe_edit = PathEdit(
|
||||
file_mode=QFileDialog.ExistingFile,
|
||||
name_filters=["*.exe", "*.app"],
|
||||
placeholder=self.tr("Relative path to launch executable"),
|
||||
edit_func=self.override_exe_edit_callback,
|
||||
save_func=self.override_exe_save_callback,
|
||||
parent=self
|
||||
)
|
||||
self.ui.launch_settings_layout.insertRow(
|
||||
self.ui.launch_settings_layout.getWidgetPosition(self.ui.launch_params)[0] + 1,
|
||||
QLabel(self.tr("Override executable"), self), self.override_exe_edit
|
||||
)
|
||||
|
||||
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.game: Game = None
|
||||
self.igame: InstalledGame = None
|
||||
|
||||
def override_exe_edit_callback(self, path: str) -> Tuple[bool, str, int]:
|
||||
if not path or self.igame is None:
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(self.igame.install_path, path)
|
||||
if self.igame.install_path not in path:
|
||||
return False, self.igame.install_path, IndicatorReasonsCommon.WRONG_PATH
|
||||
if not os.path.exists(path):
|
||||
return False, path, IndicatorReasonsCommon.WRONG_PATH
|
||||
|
||||
if not path.endswith(".exe") and not path.endswith(".app"):
|
||||
return False, path, IndicatorReasonsCommon.WRONG_PATH
|
||||
path = os.path.relpath(path, self.igame.install_path)
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
|
||||
def override_exe_save_callback(self, path: str):
|
||||
self.line_edit_save_callback("override_exe", path)
|
||||
|
||||
def line_edit_save_callback(self, option, value) -> None:
|
||||
if value:
|
||||
config_helper.add_option(self.game.app_name, option, value)
|
||||
else:
|
||||
config_helper.remove_option(self.game.app_name, option)
|
||||
config_helper.save_config()
|
||||
|
||||
def update_combobox(self, option, index):
|
||||
if self.change:
|
||||
# remove section
|
||||
if index:
|
||||
if index == 1:
|
||||
config_helper.add_option(self.game.app_name, option, "true")
|
||||
if index == 2:
|
||||
config_helper.add_option(self.game.app_name, option, "false")
|
||||
else:
|
||||
config_helper.remove_option(self.game.app_name, option)
|
||||
config_helper.save_config()
|
||||
|
||||
def load_settings(self, rgame: RareGame):
|
||||
self.change = False
|
||||
# FIXME: Use RareGame for the rest of the code
|
||||
app_name = rgame.app_name
|
||||
super(GameSettings, self).load_settings(app_name)
|
||||
self.game = rgame.game
|
||||
self.igame = rgame.igame
|
||||
if self.igame:
|
||||
if self.igame.can_run_offline:
|
||||
offline = self.core.lgd.config.get(self.game.app_name, "offline", fallback="unset")
|
||||
if offline == "true":
|
||||
self.ui.offline.setCurrentIndex(1)
|
||||
elif offline == "false":
|
||||
self.ui.offline.setCurrentIndex(2)
|
||||
else:
|
||||
self.ui.offline.setCurrentIndex(0)
|
||||
|
||||
self.ui.offline.setEnabled(True)
|
||||
else:
|
||||
self.ui.offline.setEnabled(False)
|
||||
self.override_exe_edit.set_root(self.igame.install_path)
|
||||
else:
|
||||
self.ui.offline.setEnabled(False)
|
||||
self.override_exe_edit.set_root("")
|
||||
|
||||
skip_update = self.core.lgd.config.get(self.game.app_name, "skip_update_check", fallback="unset")
|
||||
if skip_update == "true":
|
||||
self.ui.skip_update.setCurrentIndex(1)
|
||||
elif skip_update == "false":
|
||||
self.ui.skip_update.setCurrentIndex(2)
|
||||
else:
|
||||
self.ui.skip_update.setCurrentIndex(0)
|
||||
|
||||
self.set_title.emit(self.game.app_title)
|
||||
if platform.system() != "Windows":
|
||||
if self.igame and self.igame.platform == "Mac":
|
||||
self.ui.linux_settings_widget.setVisible(False)
|
||||
else:
|
||||
self.ui.linux_settings_widget.setVisible(True)
|
||||
|
||||
self.ui.launch_params.setText(self.core.lgd.config.get(self.game.app_name, "start_params", fallback=""))
|
||||
self.override_exe_edit.setText(
|
||||
self.core.lgd.config.get(self.game.app_name, "override_exe", fallback="")
|
||||
)
|
||||
self.pre_launch_settings.load_settings(app_name)
|
||||
|
||||
self.change = True
|
197
rare/components/tabs/games/game_info/settings.py
Normal file
|
@ -0,0 +1,197 @@
|
|||
import os.path
|
||||
import platform as pf
|
||||
from logging import getLogger
|
||||
from typing import Tuple
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QShowEvent
|
||||
from PyQt5.QtWidgets import QFileDialog, QComboBox, QLineEdit
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
|
||||
from rare.components.tabs.settings.widgets.env_vars import EnvVars
|
||||
from rare.components.tabs.settings.widgets.game import GameSettingsBase
|
||||
from rare.components.tabs.settings.widgets.launch import LaunchSettingsBase
|
||||
from rare.components.tabs.settings.widgets.overlay import DxvkSettings
|
||||
from rare.components.tabs.settings.widgets.wrappers import WrapperSettings
|
||||
from rare.models.game import RareGame
|
||||
from rare.utils import config_helper as config
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
|
||||
if pf.system() != "Windows":
|
||||
from rare.components.tabs.settings.widgets.wine import WineSettings
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
from rare.components.tabs.settings.widgets.proton import ProtonSettings
|
||||
from rare.components.tabs.settings.widgets.overlay import MangoHudSettings
|
||||
|
||||
logger = getLogger("GameSettings")
|
||||
|
||||
|
||||
class GameWrapperSettings(WrapperSettings):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
def load_settings(self, app_name: str):
|
||||
self.app_name = app_name
|
||||
|
||||
|
||||
class GameLaunchSettings(LaunchSettingsBase):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(GameLaunchSettings, self).__init__(GameWrapperSettings, parent=parent)
|
||||
|
||||
self.game: Game = None
|
||||
self.igame: InstalledGame = None
|
||||
|
||||
self.skip_update_combo = QComboBox(self)
|
||||
self.skip_update_combo.addItem(self.tr("Default"), None)
|
||||
self.skip_update_combo.addItem(self.tr("No"), "false")
|
||||
self.skip_update_combo.addItem(self.tr("Yes"), "true")
|
||||
self.skip_update_combo.currentIndexChanged.connect(self.__skip_update_changed)
|
||||
|
||||
self.offline_combo = QComboBox(self)
|
||||
self.offline_combo.addItem(self.tr("Default"), None)
|
||||
self.offline_combo.addItem(self.tr("No"), "false")
|
||||
self.offline_combo.addItem(self.tr("Yes"), "true")
|
||||
self.offline_combo.currentIndexChanged.connect(self.__offline_changed)
|
||||
|
||||
self.override_exe_edit = PathEdit(
|
||||
file_mode=QFileDialog.ExistingFile,
|
||||
name_filters=["*.exe", "*.app"],
|
||||
placeholder=self.tr("Relative path to the replacement executable"),
|
||||
edit_func=self.__override_exe_edit_callback,
|
||||
save_func=self.__override_exe_save_callback,
|
||||
parent=self
|
||||
)
|
||||
|
||||
self.launch_params_edit = QLineEdit(self)
|
||||
self.launch_params_edit.setPlaceholderText(self.tr("Game specific command line arguments"))
|
||||
self.launch_params_edit.textChanged.connect(self.__launch_params_changed)
|
||||
|
||||
self.main_layout.insertRow(0, self.tr("Skip update check"), self.skip_update_combo)
|
||||
self.main_layout.insertRow(1, self.tr("Offline mode"), self.offline_combo)
|
||||
self.main_layout.insertRow(2, self.tr("Launch parameters"), self.launch_params_edit)
|
||||
self.main_layout.insertRow(3, self.tr("Override executable"), self.override_exe_edit)
|
||||
|
||||
def showEvent(self, a0: QShowEvent):
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
|
||||
skip_update = config.get_option(self.app_name, "skip_update_check", fallback=None)
|
||||
self.skip_update_combo.setCurrentIndex(self.offline_combo.findData(skip_update, Qt.UserRole))
|
||||
|
||||
offline = config.get_option(self.app_name, "offline", fallback=None)
|
||||
self.offline_combo.setCurrentIndex(self.offline_combo.findData(offline, Qt.UserRole))
|
||||
|
||||
if self.igame:
|
||||
self.offline_combo.setEnabled(self.igame.can_run_offline)
|
||||
self.override_exe_edit.setRootPath(self.igame.install_path)
|
||||
else:
|
||||
self.offline_combo.setEnabled(False)
|
||||
self.override_exe_edit.setRootPath(os.path.expanduser("~/"))
|
||||
|
||||
launch_params = config.get_option(self.app_name, "start_params", "")
|
||||
self.launch_params_edit.setText(launch_params)
|
||||
|
||||
override_exe = config.get_option(self.app_name, "override_exe", fallback="")
|
||||
self.override_exe_edit.setText(override_exe)
|
||||
|
||||
return super().showEvent(a0)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def __skip_update_changed(self, index):
|
||||
data = self.skip_update_combo.itemData(index, Qt.UserRole)
|
||||
config.save_option(self.app_name, "skip_update_check", data)
|
||||
|
||||
def __override_exe_edit_callback(self, path: str) -> Tuple[bool, str, int]:
|
||||
if not path or self.igame is None:
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(self.igame.install_path, path)
|
||||
# lk: Compare paths through python's commonpath because in windows we
|
||||
# cannot compare as strings
|
||||
# antonia disapproves of this
|
||||
if os.path.commonpath([self.igame.install_path, path]) != self.igame.install_path:
|
||||
return False, self.igame.install_path, IndicatorReasonsCommon.WRONG_PATH
|
||||
if not os.path.exists(path):
|
||||
return False, path, IndicatorReasonsCommon.WRONG_PATH
|
||||
|
||||
if not path.endswith(".exe") and not path.endswith(".app"):
|
||||
return False, path, IndicatorReasonsCommon.WRONG_PATH
|
||||
path = os.path.relpath(path, self.igame.install_path)
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
|
||||
def __override_exe_save_callback(self, path: str):
|
||||
config.save_option(self.app_name, "override_exe", path)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def __offline_changed(self, index):
|
||||
data = self.skip_update_combo.itemData(index, Qt.UserRole)
|
||||
config.save_option(self.app_name, "offline", data)
|
||||
|
||||
def __launch_params_changed(self, value) -> None:
|
||||
config.save_option(self.app_name, "start_params", value)
|
||||
|
||||
def load_settings(self, rgame: RareGame):
|
||||
self.game = rgame.game
|
||||
self.igame = rgame.igame
|
||||
self.app_name = rgame.app_name
|
||||
self.wrappers_widget.load_settings(rgame.app_name)
|
||||
|
||||
|
||||
if pf.system() != "Windows":
|
||||
class GameWineSettings(WineSettings):
|
||||
def load_settings(self, app_name):
|
||||
self.app_name = app_name
|
||||
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
class GameProtonSettings(ProtonSettings):
|
||||
def load_settings(self, app_name: str):
|
||||
self.app_name = app_name
|
||||
|
||||
class GameMangoHudSettings(MangoHudSettings):
|
||||
def load_settings(self, app_name: str):
|
||||
self.app_name = app_name
|
||||
|
||||
|
||||
class GameDxvkSettings(DxvkSettings):
|
||||
def load_settings(self, app_name: str):
|
||||
self.app_name = app_name
|
||||
|
||||
|
||||
class GameEnvVars(EnvVars):
|
||||
def load_settings(self, app_name):
|
||||
self.app_name = app_name
|
||||
|
||||
|
||||
class GameSettings(GameSettingsBase):
|
||||
def __init__(self, parent=None):
|
||||
if pf.system() != "Windows":
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
super(GameSettings, self).__init__(
|
||||
GameLaunchSettings, GameDxvkSettings, GameEnvVars,
|
||||
GameWineSettings, GameProtonSettings, GameMangoHudSettings,
|
||||
parent=parent
|
||||
)
|
||||
else:
|
||||
super(GameSettings, self).__init__(
|
||||
GameLaunchSettings, GameDxvkSettings, GameEnvVars,
|
||||
GameWineSettings,
|
||||
parent=parent
|
||||
)
|
||||
else:
|
||||
super(GameSettings, self).__init__(
|
||||
GameLaunchSettings, GameDxvkSettings, GameEnvVars,
|
||||
parent=parent
|
||||
)
|
||||
|
||||
def load_settings(self, rgame: RareGame):
|
||||
self.set_title.emit(rgame.app_title)
|
||||
self.app_name = rgame.app_name
|
||||
self.launch.load_settings(rgame)
|
||||
if pf.system() != "Windows":
|
||||
self.wine.load_settings(rgame.app_name)
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
self.proton_tool.load_settings(rgame.app_name)
|
||||
self.mangohud.load_settings(rgame.app_name)
|
||||
self.dxvk.load_settings(rgame.app_name)
|
||||
self.env_vars.load_settings(rgame.app_name)
|
|
@ -1,55 +1,50 @@
|
|||
from typing import Tuple, List, Union, Optional
|
||||
from abc import abstractmethod
|
||||
from typing import Tuple, List, Union, Type, TypeVar
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, Qt
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QScrollArea
|
||||
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.models.game import RareGame
|
||||
from rare.models.signals import GlobalSignals
|
||||
from rare.models.library import LibraryFilter, LibraryOrder, LibraryView
|
||||
from rare.shared import RareCore
|
||||
from rare.widgets.library_layout import LibraryLayout
|
||||
from .icon_game_widget import IconGameWidget
|
||||
from .list_game_widget import ListGameWidget
|
||||
|
||||
ViewWidget = TypeVar("ViewWidget", IconGameWidget, ListGameWidget)
|
||||
|
||||
class LibraryWidgetController(QObject):
|
||||
def __init__(self, icon_container: QWidget, list_container: QWidget, parent: QWidget = None):
|
||||
super(LibraryWidgetController, self).__init__(parent=parent)
|
||||
self._icon_container: QWidget = icon_container
|
||||
self._list_container: QWidget = list_container
|
||||
self.rcore = RareCore.instance()
|
||||
self.core: LegendaryCore = self.rcore.core()
|
||||
self.signals: GlobalSignals = self.rcore.signals()
|
||||
|
||||
self.signals.game.installed.connect(self.sort_list)
|
||||
self.signals.game.uninstalled.connect(self.sort_list)
|
||||
class ViewContainer(QWidget):
|
||||
def __init__(self, rcore: RareCore, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.rcore: RareCore = rcore
|
||||
|
||||
def add_game(self, rgame: RareGame):
|
||||
return self.add_widgets(rgame)
|
||||
|
||||
def add_widgets(self, rgame: RareGame) -> Tuple[IconGameWidget, ListGameWidget]:
|
||||
icon_widget = IconGameWidget(rgame, self._icon_container)
|
||||
list_widget = ListGameWidget(rgame, self._list_container)
|
||||
return icon_widget, list_widget
|
||||
def _add_widget(self, widget_type: Type[ViewWidget], rgame: RareGame) -> ViewWidget:
|
||||
widget = widget_type(rgame, self)
|
||||
self.layout().addWidget(widget)
|
||||
return widget
|
||||
|
||||
@staticmethod
|
||||
def __visibility(widget: Union[IconGameWidget,ListGameWidget], filter_name, search_text) -> Tuple[bool, float]:
|
||||
if filter_name == "hidden":
|
||||
def __visibility(widget: ViewWidget, library_filter, search_text) -> Tuple[bool, float]:
|
||||
if library_filter == LibraryFilter.HIDDEN:
|
||||
visible = "hidden" in widget.rgame.metadata.tags
|
||||
elif "hidden" in widget.rgame.metadata.tags:
|
||||
visible = False
|
||||
elif filter_name == "installed":
|
||||
elif library_filter == LibraryFilter.INSTALLED:
|
||||
visible = widget.rgame.is_installed and not widget.rgame.is_unreal
|
||||
elif filter_name == "offline":
|
||||
elif library_filter == LibraryFilter.OFFLINE:
|
||||
visible = widget.rgame.can_run_offline and not widget.rgame.is_unreal
|
||||
elif filter_name == "32bit":
|
||||
elif library_filter == LibraryFilter.WIN32:
|
||||
visible = widget.rgame.is_win32 and not widget.rgame.is_unreal
|
||||
elif filter_name == "mac":
|
||||
elif library_filter == LibraryFilter.MAC:
|
||||
visible = widget.rgame.is_mac and not widget.rgame.is_unreal
|
||||
elif filter_name == "installable":
|
||||
elif library_filter == LibraryFilter.INSTALLABLE:
|
||||
visible = not widget.rgame.is_non_asset and not widget.rgame.is_unreal
|
||||
elif filter_name == "include_ue":
|
||||
elif library_filter == LibraryFilter.INCLUDE_UE:
|
||||
visible = True
|
||||
elif filter_name == "all":
|
||||
elif library_filter == LibraryFilter.ALL:
|
||||
visible = not widget.rgame.is_unreal
|
||||
else:
|
||||
visible = True
|
||||
|
@ -64,74 +59,159 @@ class LibraryWidgetController(QObject):
|
|||
|
||||
return visible, opacity
|
||||
|
||||
def filter_list(self, filter_name="all", search_text: str = ""):
|
||||
icon_widgets = self._icon_container.findChildren(IconGameWidget)
|
||||
list_widgets = self._list_container.findChildren(ListGameWidget)
|
||||
for iw in icon_widgets:
|
||||
visibility, opacity = self.__visibility(iw, filter_name, search_text)
|
||||
def _filter_view(self, widget_type: Type[ViewWidget], filter_by: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
|
||||
widgets = self.findChildren(widget_type)
|
||||
for iw in widgets:
|
||||
visibility, opacity = self.__visibility(iw, filter_by, search_text)
|
||||
iw.setOpacity(opacity)
|
||||
iw.setVisible(visibility)
|
||||
for lw in list_widgets:
|
||||
visibility, opacity = self.__visibility(lw, filter_name, search_text)
|
||||
lw.setOpacity(opacity)
|
||||
lw.setVisible(visibility)
|
||||
self.sort_list(search_text)
|
||||
|
||||
def _update_view(self, widget_type: Type[ViewWidget]):
|
||||
widgets = self.findChildren(widget_type)
|
||||
app_names = {iw.rgame.app_name for iw in widgets}
|
||||
games = list(self.rcore.games)
|
||||
game_app_names = {g.app_name for g in games}
|
||||
new_app_names = game_app_names.difference(app_names)
|
||||
for app_name in new_app_names:
|
||||
game = self.rcore.get_game(app_name)
|
||||
w = widget_type(game, self)
|
||||
self.layout().addWidget(w)
|
||||
|
||||
def _find_widget(self, widget_type: Type[ViewWidget], app_name: str) -> ViewWidget:
|
||||
w = self.findChild(widget_type, app_name)
|
||||
return w
|
||||
|
||||
@abstractmethod
|
||||
def order_view(self):
|
||||
pass
|
||||
|
||||
|
||||
class IconViewContainer(ViewContainer):
|
||||
def __init__(self, rcore: RareCore, parent=None):
|
||||
super().__init__(rcore, parent=parent)
|
||||
view_layout = LibraryLayout(self)
|
||||
view_layout.setSpacing(9)
|
||||
view_layout.setContentsMargins(0, 13, 0, 13)
|
||||
view_layout.setAlignment(Qt.AlignTop)
|
||||
self.setLayout(view_layout)
|
||||
|
||||
def add_widget(self, rgame: RareGame) -> IconGameWidget:
|
||||
return self._add_widget(IconGameWidget, rgame)
|
||||
|
||||
def filter_view(self, filter_by: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
|
||||
self._filter_view(IconGameWidget, filter_by, search_text)
|
||||
|
||||
def update_view(self):
|
||||
self._update_view(IconGameWidget)
|
||||
|
||||
def find_widget(self, app_name: str) -> ViewWidget:
|
||||
return self._find_widget(IconGameWidget, app_name)
|
||||
|
||||
def order_view(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
|
||||
if search_text:
|
||||
self.layout().sort(
|
||||
lambda x: (search_text not in x.widget().rgame.app_title.lower(),)
|
||||
)
|
||||
else:
|
||||
if (newest := order_by == LibraryOrder.NEWEST) or order_by == LibraryOrder.OLDEST:
|
||||
# Sort by grant date
|
||||
self.layout().sort(
|
||||
key=lambda x: (x.widget().rgame.is_installed, not x.widget().rgame.is_non_asset, x.widget().rgame.grant_date()),
|
||||
reverse=newest,
|
||||
)
|
||||
elif order_by == LibraryOrder.RECENT:
|
||||
# Sort by recently played
|
||||
self.layout().sort(
|
||||
key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.metadata.last_played),
|
||||
reverse=True,
|
||||
)
|
||||
else:
|
||||
# Sort by title
|
||||
self.layout().sort(
|
||||
key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.app_title)
|
||||
)
|
||||
|
||||
|
||||
class ListViewContainer(ViewContainer):
|
||||
def __init__(self, rcore, parent=None):
|
||||
super().__init__(rcore, parent=parent)
|
||||
view_layout = QVBoxLayout(self)
|
||||
view_layout.setContentsMargins(3, 3, 9, 3)
|
||||
view_layout.setAlignment(Qt.AlignTop)
|
||||
self.setLayout(view_layout)
|
||||
|
||||
def add_widget(self, rgame: RareGame) -> ListGameWidget:
|
||||
return self._add_widget(ListGameWidget, rgame)
|
||||
|
||||
def filter_view(self, filter_by: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
|
||||
self._filter_view(ListGameWidget, filter_by, search_text)
|
||||
|
||||
def update_view(self):
|
||||
self._update_view(ListGameWidget)
|
||||
|
||||
def find_widget(self, app_name: str) -> ViewWidget:
|
||||
return self._find_widget(ListGameWidget, app_name)
|
||||
|
||||
def order_view(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
|
||||
list_widgets = self.findChildren(ListGameWidget)
|
||||
if search_text:
|
||||
list_widgets.sort(key=lambda x: (search_text not in x.rgame.app_title.lower(),))
|
||||
else:
|
||||
if (newest := order_by == LibraryOrder.NEWEST) or order_by == LibraryOrder.OLDEST:
|
||||
list_widgets.sort(
|
||||
key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()),
|
||||
reverse=newest,
|
||||
)
|
||||
elif order_by == LibraryOrder.RECENT:
|
||||
list_widgets.sort(
|
||||
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.metadata.last_played),
|
||||
reverse=True,
|
||||
)
|
||||
else:
|
||||
list_widgets.sort(
|
||||
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
|
||||
)
|
||||
for idx, wl in enumerate(list_widgets):
|
||||
self.layout().insertWidget(idx, wl)
|
||||
|
||||
|
||||
class LibraryWidgetController(QObject):
|
||||
def __init__(self, view: LibraryView, parent: QScrollArea = None):
|
||||
super(LibraryWidgetController, self).__init__(parent=parent)
|
||||
self.rcore = RareCore.instance()
|
||||
self.core: LegendaryCore = self.rcore.core()
|
||||
self.signals: GlobalSignals = self.rcore.signals()
|
||||
|
||||
if view == LibraryView.COVER:
|
||||
self._container: IconViewContainer = IconViewContainer(self.rcore, parent)
|
||||
else:
|
||||
self._container: ListViewContainer = ListViewContainer(self.rcore, parent)
|
||||
parent.setWidget(self._container)
|
||||
|
||||
self.signals.game.installed.connect(self.order_game_views)
|
||||
self.signals.game.uninstalled.connect(self.order_game_views)
|
||||
|
||||
def add_game(self, rgame: RareGame):
|
||||
return self.add_widgets(rgame)
|
||||
|
||||
def add_widgets(self, rgame: RareGame) -> ViewWidget:
|
||||
return self._container.add_widget(rgame)
|
||||
|
||||
def filter_game_views(self, filter_by: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
|
||||
self._container.filter_view(filter_by, search_text)
|
||||
self.order_game_views(search_text=search_text)
|
||||
|
||||
@pyqtSlot()
|
||||
def sort_list(self, sort_by: str = ""):
|
||||
# lk: this is the existing sorting implemenation
|
||||
# lk: it sorts by installed then by title
|
||||
if sort_by:
|
||||
self._icon_container.layout().sort(lambda x: (sort_by not in x.widget().rgame.app_title.lower(),))
|
||||
else:
|
||||
self._icon_container.layout().sort(
|
||||
key=lambda x: (
|
||||
# Sort by grant date
|
||||
# x.widget().rgame.is_installed,
|
||||
# not x.widget().rgame.is_non_asset,
|
||||
# x.widget().rgame.grant_date(),
|
||||
# ), reverse=True
|
||||
not x.widget().rgame.is_installed,
|
||||
x.widget().rgame.is_non_asset,
|
||||
x.widget().rgame.app_title,
|
||||
)
|
||||
)
|
||||
list_widgets = self._list_container.findChildren(ListGameWidget)
|
||||
if sort_by:
|
||||
list_widgets.sort(key=lambda x: (sort_by not in x.rgame.app_title.lower(),))
|
||||
else:
|
||||
list_widgets.sort(
|
||||
# Sort by grant date
|
||||
# key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()), reverse=True
|
||||
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
|
||||
)
|
||||
for idx, wl in enumerate(list_widgets):
|
||||
self._list_container.layout().insertWidget(idx, wl)
|
||||
def order_game_views(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
|
||||
self._container.order_view(order_by, search_text)
|
||||
|
||||
@pyqtSlot()
|
||||
@pyqtSlot(list)
|
||||
def update_list(self, app_names: List[str] = None):
|
||||
if not app_names:
|
||||
# lk: base it on icon widgets, the two lists should be identical
|
||||
icon_widgets = self._icon_container.findChildren(IconGameWidget)
|
||||
list_widgets = self._list_container.findChildren(ListGameWidget)
|
||||
icon_app_names = set([iw.rgame.app_name for iw in icon_widgets])
|
||||
list_app_names = set([lw.rgame.app_name for lw in list_widgets])
|
||||
games = list(self.rcore.games)
|
||||
game_app_names = set([g.app_name for g in games])
|
||||
new_icon_app_names = game_app_names.difference(icon_app_names)
|
||||
new_list_app_names = game_app_names.difference(list_app_names)
|
||||
for app_name in new_icon_app_names:
|
||||
game = self.rcore.get_game(app_name)
|
||||
iw = IconGameWidget(game)
|
||||
self._icon_container.layout().addWidget(iw)
|
||||
for app_name in new_list_app_names:
|
||||
game = self.rcore.get_game(app_name)
|
||||
lw = ListGameWidget(game)
|
||||
self._list_container.layout().addWidget(lw)
|
||||
self.sort_list()
|
||||
def update_game_views(self, app_names: List[str] = None):
|
||||
if app_names:
|
||||
return
|
||||
self._container.update_view()
|
||||
self.order_game_views()
|
||||
|
||||
def __find_widget(self, app_name: str) -> Tuple[Union[IconGameWidget, None], Union[ListGameWidget, None]]:
|
||||
iw = self._icon_container.findChild(IconGameWidget, app_name)
|
||||
lw = self._list_container.findChild(ListGameWidget, app_name)
|
||||
return iw, lw
|
||||
def __find_widget(self, app_name: str) -> Union[ViewWidget, None]:
|
||||
return self._container.find_widget(app_name)
|
||||
|
|
|
@ -111,7 +111,6 @@ class GameWidget(LibraryWidget):
|
|||
self.startTimer(random.randrange(42, 2361, 129), Qt.CoarseTimer)
|
||||
# self.startTimer(random.randrange(42, 2361, 363), Qt.VeryCoarseTimer)
|
||||
# self.rgame.load_pixmap()
|
||||
# QTimer.singleShot(random.randrange(42, 2361, 7), Qt.VeryCoarseTimer, self.rgame.load_pixmap)
|
||||
super().paintEvent(a0)
|
||||
|
||||
def timerEvent(self, a0):
|
||||
|
|
|
@ -10,7 +10,7 @@ from PyQt5.QtWidgets import (
|
|||
QPushButton,
|
||||
)
|
||||
|
||||
from rare.utils.misc import icon, widget_object_name
|
||||
from rare.utils.misc import qta_icon, widget_object_name
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
|
||||
|
||||
|
@ -59,13 +59,13 @@ class IconWidget(object):
|
|||
# play button
|
||||
self.launch_btn = QPushButton(parent=self.mini_widget)
|
||||
self.launch_btn.setObjectName(f"{type(self).__name__}Button")
|
||||
self.launch_btn.setIcon(icon("ei.play-alt", color="white"))
|
||||
self.launch_btn.setIcon(qta_icon("ei.play-alt", color="white"))
|
||||
self.launch_btn.setIconSize(QSize(20, 20))
|
||||
self.launch_btn.setFixedSize(QSize(widget.width() // 4, widget.width() // 4))
|
||||
|
||||
self.install_btn = QPushButton(parent=self.mini_widget)
|
||||
self.install_btn.setObjectName(f"{type(self).__name__}Button")
|
||||
self.install_btn.setIcon(icon("ri.install-fill", color="white"))
|
||||
self.install_btn.setIcon(qta_icon("ri.install-fill", color="white"))
|
||||
self.install_btn.setIconSize(QSize(20, 20))
|
||||
self.install_btn.setFixedSize(QSize(widget.width() // 4, widget.width() // 4))
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class ProgressLabel(QLabel):
|
|||
|
||||
def __center_on_parent(self):
|
||||
fm = QFontMetrics(self.font())
|
||||
rect = fm.boundingRect(f" {self.text()} ")
|
||||
rect = fm.boundingRect(" 100% ")
|
||||
rect.moveCenter(self.parent().contentsRect().center())
|
||||
self.setGeometry(rect)
|
||||
|
||||
|
@ -52,7 +52,7 @@ class ProgressLabel(QLabel):
|
|||
origin_h = (image.height() - min_d) // 2
|
||||
for x, y in zip(range(origin_w, min_d), range(origin_h, min_d)):
|
||||
pixel = image.pixelColor(x, y).getRgb()
|
||||
color = list(map(lambda t: sum(t) // 2, zip(pixel[0:3], color)))
|
||||
color = list(map(lambda t: sum(t) // 2, zip(pixel[:3], color)))
|
||||
# take the V component of the HSV color
|
||||
fg_color = QColor(0, 0, 0) if QColor(*color).value() < 127 else QColor(255, 255, 255)
|
||||
bg_color = QColor(*map(lambda c: 255 - c, color))
|
||||
|
|
|
@ -9,12 +9,10 @@ from PyQt5.QtGui import (
|
|||
QLinearGradient,
|
||||
QPixmap,
|
||||
QImage,
|
||||
QResizeEvent,
|
||||
)
|
||||
|
||||
from rare.models.game import RareGame
|
||||
from rare.utils.misc import format_size
|
||||
from rare.widgets.image_widget import ImageWidget
|
||||
from .game_widget import GameWidget
|
||||
from .list_widget import ListWidget
|
||||
|
||||
|
@ -70,23 +68,6 @@ class ListGameWidget(GameWidget):
|
|||
refactored to be used in downloads and/or dlcs
|
||||
"""
|
||||
|
||||
def event(self, e: QEvent) -> bool:
|
||||
if e.type() == QEvent.LayoutRequest:
|
||||
if self.progress_label.isVisible():
|
||||
width = int(self._pixmap.width() / self._pixmap.devicePixelRatioF())
|
||||
origin = self.width() - width
|
||||
fill_rect = QRect(origin, 0, width, self.sizeHint().height())
|
||||
self.progress_label.setGeometry(fill_rect)
|
||||
return ImageWidget.event(self, e)
|
||||
|
||||
def resizeEvent(self, a0: QResizeEvent) -> None:
|
||||
if self.progress_label.isVisible():
|
||||
width = int(self._pixmap.width() / self._pixmap.devicePixelRatioF())
|
||||
origin = self.width() - width
|
||||
fill_rect = QRect(origin, 0, width, self.sizeHint().height())
|
||||
self.progress_label.setGeometry(fill_rect)
|
||||
ImageWidget.resizeEvent(self, a0)
|
||||
|
||||
def prepare_pixmap(self, pixmap: QPixmap) -> QPixmap:
|
||||
device: QImage = QImage(
|
||||
pixmap.size().width() * 3,
|
||||
|
@ -112,11 +93,13 @@ class ListGameWidget(GameWidget):
|
|||
# lk: trade some possible delay and start-up time
|
||||
# lk: for faster rendering. Gradients are expensive
|
||||
# lk: so pre-generate the image
|
||||
super(ListGameWidget, self).setPixmap(self.prepare_pixmap(pixmap))
|
||||
if not pixmap.isNull():
|
||||
pixmap = self.prepare_pixmap(pixmap)
|
||||
super(ListGameWidget, self).setPixmap(pixmap)
|
||||
|
||||
def paint_image_cover(self, painter: QPainter, a0: QPaintEvent) -> None:
|
||||
painter.setOpacity(self._opacity)
|
||||
color = self.palette().color(QPalette.Background).darker(75)
|
||||
color = self.palette().color(QPalette.Window).darker(75)
|
||||
painter.fillRect(self.rect(), color)
|
||||
brush = QBrush(self._pixmap)
|
||||
brush.setTransform(self._transform)
|
||||
|
|
|
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import (
|
|||
QWidget,
|
||||
)
|
||||
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
|
||||
|
||||
|
@ -26,7 +26,7 @@ class ListWidget(object):
|
|||
self.size_label = None
|
||||
|
||||
def setupUi(self, widget: QWidget):
|
||||
self.title_label = QLabel(parent=widget)
|
||||
self.title_label = ElideLabel(parent=widget)
|
||||
self.title_label.setObjectName(f"{type(self).__name__}TitleLabel")
|
||||
self.title_label.setWordWrap(False)
|
||||
|
||||
|
@ -40,14 +40,14 @@ class ListWidget(object):
|
|||
|
||||
self.install_btn = QPushButton(parent=widget)
|
||||
self.install_btn.setObjectName(f"{type(self).__name__}Button")
|
||||
self.install_btn.setIcon(icon("ri.install-line"))
|
||||
self.install_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.install_btn.setIcon(qta_icon("ri.install-line"))
|
||||
self.install_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.install_btn.setFixedWidth(120)
|
||||
|
||||
self.launch_btn = QPushButton(parent=widget)
|
||||
self.launch_btn.setObjectName(f"{type(self).__name__}Button")
|
||||
self.launch_btn.setIcon(icon("ei.play-alt"))
|
||||
self.launch_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.launch_btn.setIcon(qta_icon("ei.play-alt"))
|
||||
self.launch_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.launch_btn.setFixedWidth(120)
|
||||
|
||||
# lk: do not focus on button
|
||||
|
@ -71,19 +71,19 @@ class ListWidget(object):
|
|||
self.size_label.setFixedWidth(60)
|
||||
|
||||
# Create layouts
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.setAlignment(Qt.AlignLeft)
|
||||
left_layout = QVBoxLayout()
|
||||
left_layout.setAlignment(Qt.AlignLeft)
|
||||
|
||||
bottom_layout = QHBoxLayout()
|
||||
bottom_layout.setAlignment(Qt.AlignRight)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout = QHBoxLayout()
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(3, 3, 3, 3)
|
||||
|
||||
# Layout the widgets
|
||||
# (from inner to outer)
|
||||
top_layout.addWidget(self.title_label, stretch=1)
|
||||
left_layout.addWidget(self.title_label, stretch=1)
|
||||
|
||||
bottom_layout.addWidget(self.developer_label, stretch=0, alignment=Qt.AlignLeft)
|
||||
bottom_layout.addItem(QSpacerItem(20, 0, QSizePolicy.Fixed, QSizePolicy.Minimum))
|
||||
|
@ -94,15 +94,17 @@ class ListWidget(object):
|
|||
bottom_layout.addWidget(self.status_label, stretch=0, alignment=Qt.AlignLeft)
|
||||
bottom_layout.addItem(QSpacerItem(20, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
||||
bottom_layout.addWidget(self.tooltip_label, stretch=0, alignment=Qt.AlignRight)
|
||||
bottom_layout.addWidget(self.install_btn, stretch=0, alignment=Qt.AlignRight)
|
||||
bottom_layout.addWidget(self.launch_btn, stretch=0, alignment=Qt.AlignRight)
|
||||
|
||||
layout.addLayout(top_layout)
|
||||
layout.addLayout(bottom_layout)
|
||||
left_layout.addLayout(bottom_layout)
|
||||
|
||||
layout.addLayout(left_layout)
|
||||
layout.addWidget(self.install_btn, stretch=0, alignment=Qt.AlignRight)
|
||||
layout.addWidget(self.launch_btn, stretch=0, alignment=Qt.AlignRight)
|
||||
|
||||
widget.setLayout(layout)
|
||||
|
||||
widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
|
||||
widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
widget.setFixedHeight(widget.sizeHint().height())
|
||||
widget.leaveEvent(None)
|
||||
|
||||
self.translateUi(widget)
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
import platform as pf
|
||||
|
||||
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, Qt
|
||||
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, QSize, Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
QComboBox, QToolButton, QMenu, QAction,
|
||||
QComboBox,
|
||||
QMenu,
|
||||
QAction, QSpacerItem, QSizePolicy,
|
||||
)
|
||||
from qtawesome import IconWidget
|
||||
|
||||
from rare.models.options import options, LibraryFilter, LibraryOrder
|
||||
from rare.shared import RareCore
|
||||
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.extra_widgets import ButtonLineEdit
|
||||
from rare.utils.misc import qta_icon
|
||||
|
||||
|
||||
class GameListHeadBar(QWidget):
|
||||
filterChanged: pyqtSignal = pyqtSignal(str)
|
||||
goto_import: pyqtSignal = pyqtSignal()
|
||||
goto_egl_sync: pyqtSignal = pyqtSignal()
|
||||
goto_eos_ubisoft: pyqtSignal = pyqtSignal()
|
||||
filterChanged = pyqtSignal(object)
|
||||
orderChanged = pyqtSignal(object)
|
||||
viewChanged = pyqtSignal(object)
|
||||
goto_import = pyqtSignal()
|
||||
goto_egl_sync = pyqtSignal()
|
||||
goto_eos_ubisoft = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(GameListHeadBar, self).__init__(parent=parent)
|
||||
|
@ -27,57 +29,87 @@ class GameListHeadBar(QWidget):
|
|||
self.settings = QSettings(self)
|
||||
|
||||
self.filter = QComboBox(self)
|
||||
self.filter.addItem(self.tr("All games"), "all")
|
||||
self.filter.addItem(self.tr("Installed"), "installed")
|
||||
self.filter.addItem(self.tr("Offline"), "offline")
|
||||
# self.filter.addItem(self.tr("Hidden"), "hidden")
|
||||
filters = {
|
||||
LibraryFilter.ALL: self.tr("All games"),
|
||||
LibraryFilter.INSTALLED: self.tr("Installed"),
|
||||
LibraryFilter.OFFLINE: self.tr("Offline"),
|
||||
# LibraryFilter.HIDDEN: self.tr("Hidden"),
|
||||
}
|
||||
for data, text in filters.items():
|
||||
self.filter.addItem(text, data)
|
||||
|
||||
if self.rcore.bit32_games:
|
||||
self.filter.addItem(self.tr("32bit games"), "32bit")
|
||||
self.filter.addItem(self.tr("32bit games"), LibraryFilter.WIN32)
|
||||
if self.rcore.mac_games:
|
||||
self.filter.addItem(self.tr("macOS games"), "mac")
|
||||
self.filter.addItem(self.tr("macOS games"), LibraryFilter.MAC)
|
||||
if self.rcore.origin_games:
|
||||
self.filter.addItem(self.tr("Exclude Origin"), "installable")
|
||||
self.filter.addItem(self.tr("Include Unreal"), "include_ue")
|
||||
self.filter.addItem(self.tr("Exclude Origin"), LibraryFilter.INSTALLABLE)
|
||||
self.filter.addItem(self.tr("Include Unreal"), LibraryFilter.INCLUDE_UE)
|
||||
|
||||
filter_default = "mac" if pf.system() == "Darwin" else "all"
|
||||
filter_index = i if (i := self.filter.findData(filter_default, Qt.UserRole)) >= 0 else 0
|
||||
try:
|
||||
self.filter.setCurrentIndex(self.settings.value("library_filter", filter_index, int))
|
||||
except TypeError:
|
||||
self.settings.setValue("library_filter", filter_index)
|
||||
self.filter.setCurrentIndex(filter_index)
|
||||
self.filter.currentIndexChanged.connect(self.filter_changed)
|
||||
_filter = self.settings.value(*options.library_filter)
|
||||
if (index := self.filter.findData(_filter, Qt.UserRole)) < 0:
|
||||
raise ValueError
|
||||
else:
|
||||
self.filter.setCurrentIndex(index)
|
||||
except (TypeError, ValueError):
|
||||
self.settings.setValue(options.library_filter.key, options.library_filter.default)
|
||||
_filter = LibraryFilter(options.library_filter.default)
|
||||
self.filter.setCurrentIndex(self.filter.findData(_filter, Qt.UserRole))
|
||||
self.filter.currentIndexChanged.connect(self.__filter_changed)
|
||||
|
||||
integrations_menu = QMenu(self)
|
||||
import_action = QAction(icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu)
|
||||
self.order = QComboBox(parent=self)
|
||||
sortings = {
|
||||
LibraryOrder.TITLE: self.tr("Title"),
|
||||
LibraryOrder.RECENT: self.tr("Recently played"),
|
||||
LibraryOrder.NEWEST: self.tr("Newest"),
|
||||
LibraryOrder.OLDEST: self.tr("Oldest"),
|
||||
}
|
||||
for data, text in sortings.items():
|
||||
self.order.addItem(text, data)
|
||||
|
||||
try:
|
||||
_order = LibraryOrder(self.settings.value(*options.library_order))
|
||||
if (index := self.order.findData(_order, Qt.UserRole)) < 0:
|
||||
raise ValueError
|
||||
else:
|
||||
self.order.setCurrentIndex(index)
|
||||
except (TypeError, ValueError):
|
||||
self.settings.setValue(options.library_order.key, options.library_order.default)
|
||||
_order = LibraryOrder(options.library_order.default)
|
||||
self.order.setCurrentIndex(self.order.findData(_order, Qt.UserRole))
|
||||
self.order.currentIndexChanged.connect(self.__order_changed)
|
||||
|
||||
integrations_menu = QMenu(parent=self)
|
||||
import_action = QAction(
|
||||
qta_icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu
|
||||
)
|
||||
|
||||
import_action.triggered.connect(self.goto_import)
|
||||
egl_sync_action = QAction(icon("mdi.sync", "fa.refresh"), self.tr("Sync with EGL"), integrations_menu)
|
||||
egl_sync_action = QAction(qta_icon("mdi.sync", "fa.refresh"), self.tr("Sync with EGL"), integrations_menu)
|
||||
egl_sync_action.triggered.connect(self.goto_egl_sync)
|
||||
|
||||
eos_ubisoft_action = QAction(icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"),
|
||||
integrations_menu)
|
||||
eos_ubisoft_action = QAction(
|
||||
qta_icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"), integrations_menu
|
||||
)
|
||||
eos_ubisoft_action.triggered.connect(self.goto_eos_ubisoft)
|
||||
|
||||
integrations_menu.addAction(import_action)
|
||||
integrations_menu.addAction(egl_sync_action)
|
||||
integrations_menu.addAction(eos_ubisoft_action)
|
||||
|
||||
integrations = QToolButton(self)
|
||||
integrations = QPushButton(parent=self)
|
||||
integrations.setText(self.tr("Integrations"))
|
||||
integrations.setMenu(integrations_menu)
|
||||
integrations.setPopupMode(QToolButton.InstantPopup)
|
||||
|
||||
self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search Game"))
|
||||
self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search"))
|
||||
self.search_bar.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
|
||||
self.search_bar.setObjectName("SearchBar")
|
||||
self.search_bar.setFrame(False)
|
||||
self.search_bar.setMinimumWidth(200)
|
||||
|
||||
checked = QSettings().value("icon_view", True, bool)
|
||||
self.search_bar.setMinimumWidth(250)
|
||||
|
||||
installed_tooltip = self.tr("Installed games")
|
||||
self.installed_icon = IconWidget(parent=self)
|
||||
self.installed_icon.setIcon(icon("ph.floppy-disk-back-fill"))
|
||||
self.installed_icon = QLabel(parent=self)
|
||||
self.installed_icon.setPixmap(qta_icon("ph.floppy-disk-back-fill").pixmap(QSize(16, 16)))
|
||||
self.installed_icon.setToolTip(installed_tooltip)
|
||||
self.installed_label = QLabel(parent=self)
|
||||
font = self.installed_label.font()
|
||||
|
@ -85,45 +117,52 @@ class GameListHeadBar(QWidget):
|
|||
self.installed_label.setFont(font)
|
||||
self.installed_label.setToolTip(installed_tooltip)
|
||||
available_tooltip = self.tr("Available games")
|
||||
self.available_icon = IconWidget(parent=self)
|
||||
self.available_icon.setIcon(icon("ph.floppy-disk-back-light"))
|
||||
self.available_icon = QLabel(parent=self)
|
||||
self.available_icon.setPixmap(qta_icon("ph.floppy-disk-back-light").pixmap(QSize(16, 16)))
|
||||
self.available_icon.setToolTip(available_tooltip)
|
||||
self.available_label = QLabel(parent=self)
|
||||
self.available_label.setToolTip(available_tooltip)
|
||||
|
||||
self.view = SelectViewWidget(checked)
|
||||
self.refresh_list = QPushButton(parent=self)
|
||||
self.refresh_list.setIcon(qta_icon("fa.refresh")) # Reload icon
|
||||
self.refresh_list.clicked.connect(self.__refresh_clicked)
|
||||
|
||||
self.refresh_list = QPushButton()
|
||||
self.refresh_list.setIcon(icon("fa.refresh")) # Reload icon
|
||||
self.refresh_list.clicked.connect(self.refresh_clicked)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.setContentsMargins(0, 5, 0, 5)
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.filter)
|
||||
layout.addStretch(0)
|
||||
layout.addWidget(integrations)
|
||||
layout.addStretch(5)
|
||||
layout.addWidget(self.order)
|
||||
layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed))
|
||||
layout.addWidget(self.search_bar)
|
||||
layout.addStretch(2)
|
||||
layout.addWidget(self.installed_icon)
|
||||
layout.addWidget(self.installed_label)
|
||||
layout.addWidget(self.available_icon)
|
||||
layout.addWidget(self.available_label)
|
||||
layout.addStretch(2)
|
||||
layout.addWidget(self.view)
|
||||
layout.addStretch(2)
|
||||
layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed))
|
||||
layout.addWidget(integrations)
|
||||
layout.addWidget(self.refresh_list)
|
||||
self.setLayout(layout)
|
||||
|
||||
def set_games_count(self, inst: int, avail: int) -> None:
|
||||
self.installed_label.setText(str(inst))
|
||||
self.available_label.setText(str(avail))
|
||||
|
||||
@pyqtSlot()
|
||||
def refresh_clicked(self):
|
||||
def __refresh_clicked(self):
|
||||
self.rcore.fetch()
|
||||
|
||||
def current_filter(self) -> LibraryFilter:
|
||||
return self.filter.currentData(Qt.UserRole)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def filter_changed(self, index: int):
|
||||
self.filterChanged.emit(self.filter.itemData(index, Qt.UserRole))
|
||||
self.settings.setValue("library_filter", index)
|
||||
def __filter_changed(self, index: int):
|
||||
data = self.filter.itemData(index, Qt.UserRole)
|
||||
self.filterChanged.emit(data)
|
||||
self.settings.setValue(options.library_filter.key, int(data))
|
||||
|
||||
def current_order(self) -> LibraryOrder:
|
||||
return self.order.currentData(Qt.UserRole)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def __order_changed(self, index: int):
|
||||
data = self.order.itemData(index, Qt.UserRole)
|
||||
self.orderChanged.emit(data)
|
||||
self.settings.setValue(options.library_order.key, int(data))
|
||||
|
|
|
@ -13,9 +13,10 @@ from legendary.models.game import InstalledGame
|
|||
from rare.lgndr.glue.exception import LgndrException
|
||||
from rare.models.pathspec import PathSpec
|
||||
from rare.shared import RareCore
|
||||
from rare.shared.workers.wine_resolver import WineResolver
|
||||
from rare.shared.workers.wine_resolver import WinePathResolver
|
||||
from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup
|
||||
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup
|
||||
from rare.utils.compat import utils as compat_utils
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
|
||||
|
@ -45,7 +46,6 @@ class EGLSyncGroup(QGroupBox):
|
|||
)
|
||||
|
||||
self.egl_path_info = ElideLabel(parent=self)
|
||||
self.egl_path_info.setProperty("infoLabel", 1)
|
||||
self.ui.egl_sync_layout.setWidget(
|
||||
self.ui.egl_sync_layout.getWidgetPosition(self.ui.egl_path_info_label)[0],
|
||||
QFormLayout.FieldRole, self.egl_path_info
|
||||
|
@ -87,11 +87,7 @@ class EGLSyncGroup(QGroupBox):
|
|||
|
||||
def __run_wine_resolver(self):
|
||||
self.egl_path_info.setText(self.tr("Updating..."))
|
||||
wine_resolver = WineResolver(
|
||||
self.core,
|
||||
PathSpec.egl_programdata,
|
||||
"default"
|
||||
)
|
||||
wine_resolver = WinePathResolver(self.core, "default", str(PathSpec.egl_programdata()))
|
||||
wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
|
||||
QThreadPool.globalInstance().start(wine_resolver)
|
||||
|
||||
|
@ -122,14 +118,8 @@ class EGLSyncGroup(QGroupBox):
|
|||
os.path.join(path, "dosdevices/c:")
|
||||
):
|
||||
# path is a wine prefix
|
||||
path = os.path.join(
|
||||
path,
|
||||
"dosdevices/c:",
|
||||
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests",
|
||||
)
|
||||
elif not path.rstrip("/").endswith(
|
||||
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
|
||||
):
|
||||
path = PathSpec.prefix_egl_programdata(path)
|
||||
elif not path.rstrip("/").endswith(PathSpec.wine_egl_programdata()):
|
||||
# lower() might or might not be needed in the check
|
||||
return False, path, IndicatorReasonsCommon.WRONG_FORMAT
|
||||
if os.path.exists(path):
|
||||
|
@ -311,7 +301,8 @@ class EGLSyncListGroup(QGroupBox):
|
|||
def items(self) -> Iterable[EGLSyncListItem]:
|
||||
# for i in range(self.list.count()):
|
||||
# yield self.list.item(i)
|
||||
return [self.ui.list.item(i) for i in range(self.ui.list.count())]
|
||||
return map(self.ui.list.item, range(self.ui.list.count()))
|
||||
# return [self.ui.list.item(i) for i in range(self.ui.list.count())]
|
||||
|
||||
|
||||
class EGLSyncExportGroup(EGLSyncListGroup):
|
||||
|
|
|
@ -22,7 +22,7 @@ from rare.models.game import RareEosOverlay
|
|||
from rare.shared import RareCore
|
||||
from rare.ui.components.tabs.games.integrations.eos_widget import Ui_EosWidget
|
||||
from rare.utils import config_helper as config
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
|
||||
logger = getLogger("EpicOverlay")
|
||||
|
@ -102,15 +102,15 @@ class EosPrefixWidget(QFrame):
|
|||
|
||||
if not self.overlay.is_installed and not self.overlay.available_paths(self.prefix):
|
||||
self.setDisabled(True)
|
||||
self.indicator.setPixmap(icon("fa.circle-o", color="grey").pixmap(20, 20))
|
||||
self.indicator.setPixmap(qta_icon("fa.circle-o", color="grey").pixmap(20, 20))
|
||||
self.overlay_label.setText(self.overlay.active_path(self.prefix))
|
||||
self.button.setText(self.tr("Unavailable"))
|
||||
return
|
||||
|
||||
if self.overlay.is_enabled(self.prefix):
|
||||
self.indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
|
||||
self.indicator.setPixmap(qta_icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
|
||||
else:
|
||||
self.indicator.setPixmap(icon("fa.times-circle-o", color="red").pixmap(QSize(20, 20)))
|
||||
self.indicator.setPixmap(qta_icon("fa.times-circle-o", color="red").pixmap(QSize(20, 20)))
|
||||
|
||||
install_path = os.path.normpath(p) if (p := self.overlay.install_path) else ""
|
||||
|
||||
|
@ -171,8 +171,8 @@ class EosGroup(QGroupBox):
|
|||
self.ui.install_page_layout.setAlignment(Qt.AlignTop)
|
||||
self.ui.info_page_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.ui.install_button.setIcon(icon("ri.install-line"))
|
||||
self.ui.uninstall_button.setIcon(icon("ri.uninstall-line"))
|
||||
self.ui.install_button.setIcon(qta_icon("ri.install-line"))
|
||||
self.ui.uninstall_button.setIcon(qta_icon("ri.uninstall-line"))
|
||||
|
||||
self.installed_path_label = ElideLabel(parent=self)
|
||||
self.installed_version_label = ElideLabel(parent=self)
|
||||
|
@ -224,7 +224,7 @@ class EosGroup(QGroupBox):
|
|||
|
||||
if platform.system() != "Windows":
|
||||
prefixes = config.get_prefixes()
|
||||
prefixes = {prefix for prefix in prefixes if config.prefix_exists(prefix)}
|
||||
prefixes = {prefix for prefix, _ in prefixes if config.prefix_exists(prefix)}
|
||||
if platform.system() == "Darwin":
|
||||
# TODO: add crossover support
|
||||
pass
|
||||
|
|
|
@ -263,13 +263,12 @@ class ImportGroup(QGroupBox):
|
|||
self.app_name_edit.setText(app_name)
|
||||
|
||||
def path_edit_callback(self, path) -> Tuple[bool, str, int]:
|
||||
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_dirs:
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
else:
|
||||
if not os.path.exists(path):
|
||||
return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS
|
||||
if os.path.exists(os.path.join(path, ".egstore")):
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
elif os.path.basename(path) in self.__install_dirs:
|
||||
return True, path, IndicatorReasonsCommon.VALID
|
||||
return False, path, IndicatorReasonsCommon.UNDEFINED
|
||||
|
||||
@pyqtSlot(str)
|
||||
|
|
|
@ -20,7 +20,7 @@ from rare.lgndr.core import LegendaryCore
|
|||
from rare.shared import RareCore
|
||||
from rare.shared.workers.worker import Worker
|
||||
from rare.utils.metrics import timelogger
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
from rare.widgets.loading_widget import LoadingWidget
|
||||
|
||||
|
@ -104,7 +104,7 @@ class UbiLinkWidget(QFrame):
|
|||
self.ubi_account_id = ubi_account_id
|
||||
|
||||
self.ok_indicator = QLabel(parent=self)
|
||||
self.ok_indicator.setPixmap(icon("fa.circle-o", color="grey").pixmap(20, 20))
|
||||
self.ok_indicator.setPixmap(qta_icon("fa.circle-o", color="grey").pixmap(20, 20))
|
||||
self.ok_indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
|
||||
|
||||
self.title_label = ElideLabel(game.app_title, parent=self)
|
||||
|
@ -116,7 +116,7 @@ class UbiLinkWidget(QFrame):
|
|||
if activated:
|
||||
self.link_button.setText(self.tr("Already activated"))
|
||||
self.link_button.setDisabled(True)
|
||||
self.ok_indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
|
||||
self.ok_indicator.setPixmap(qta_icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(-1, 0, 0, 0)
|
||||
|
@ -127,7 +127,7 @@ class UbiLinkWidget(QFrame):
|
|||
def activate(self):
|
||||
self.link_button.setDisabled(True)
|
||||
# self.ok_indicator.setPixmap(icon("mdi.loading", color="grey").pixmap(20, 20))
|
||||
self.ok_indicator.setPixmap(icon("mdi.transit-connection-horizontal", color="grey").pixmap(20, 20))
|
||||
self.ok_indicator.setPixmap(qta_icon("mdi.transit-connection-horizontal", color="grey").pixmap(20, 20))
|
||||
|
||||
if self.args.debug:
|
||||
worker = UbiConnectWorker(RareCore.instance().core(), None, None)
|
||||
|
@ -140,11 +140,11 @@ class UbiLinkWidget(QFrame):
|
|||
|
||||
def worker_finished(self, error):
|
||||
if not error:
|
||||
self.ok_indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
|
||||
self.ok_indicator.setPixmap(qta_icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
|
||||
self.link_button.setDisabled(True)
|
||||
self.link_button.setText(self.tr("Already activated"))
|
||||
else:
|
||||
self.ok_indicator.setPixmap(icon("fa.times-circle-o", color="red").pixmap(QSize(20, 20)))
|
||||
self.ok_indicator.setPixmap(qta_icon("fa.times-circle-o", color="red").pixmap(QSize(20, 20)))
|
||||
self.ok_indicator.setToolTip(error)
|
||||
self.link_button.setText(self.tr("Try again"))
|
||||
self.link_button.setDisabled(False)
|
||||
|
@ -243,15 +243,14 @@ class UbisoftGroup(QGroupBox):
|
|||
|
||||
if not uplay_games:
|
||||
self.info_label.setText(self.tr("You don't own any Ubisoft games."))
|
||||
elif activated == len(uplay_games):
|
||||
self.info_label.setText(self.tr("All your Ubisoft games have already been activated."))
|
||||
else:
|
||||
if activated == len(uplay_games):
|
||||
self.info_label.setText(self.tr("All your Ubisoft games have already been activated."))
|
||||
else:
|
||||
self.info_label.setText(
|
||||
self.tr("You have <b>{}</b> games available to redeem.").format(
|
||||
len(uplay_games) - activated
|
||||
)
|
||||
self.info_label.setText(
|
||||
self.tr("You have <b>{}</b> games available to redeem.").format(
|
||||
len(uplay_games) - activated
|
||||
)
|
||||
)
|
||||
logger.info(f"Found {len(uplay_games) - activated} game(s) to redeem.")
|
||||
|
||||
self.loading_widget.stop()
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
from rare.components.tabs.settings.widgets.linux import LinuxSettings
|
||||
from rare.shared import ArgumentsSingleton
|
||||
from rare.widgets.side_tab import SideTabWidget
|
||||
from .about import About
|
||||
from .debug import DebugSettings
|
||||
from .game_settings import DefaultGameSettings
|
||||
from .settings import GameSettings
|
||||
from .legendary import LegendarySettings
|
||||
from .rare import RareSettings
|
||||
|
||||
|
@ -13,17 +12,24 @@ class SettingsTab(SideTabWidget):
|
|||
super(SettingsTab, self).__init__(parent=parent)
|
||||
self.args = ArgumentsSingleton()
|
||||
|
||||
self.rare_index = self.addTab(RareSettings(self), "Rare")
|
||||
self.legendary_index = self.addTab(LegendarySettings(self), "Legendary")
|
||||
self.settings_index = self.addTab(DefaultGameSettings(True, self), self.tr("Default Settings"))
|
||||
rare_settings = RareSettings(self)
|
||||
self.rare_index = self.addTab(rare_settings, "Rare")
|
||||
|
||||
legendary_settings = LegendarySettings(self)
|
||||
self.legendary_index = self.addTab(legendary_settings, "Legendary")
|
||||
|
||||
game_settings = GameSettings(self)
|
||||
self.settings_index = self.addTab(game_settings, self.tr("Defaults"))
|
||||
|
||||
self.about = About(self)
|
||||
self.about_index = self.addTab(self.about, "About", "About")
|
||||
title = self.tr("About")
|
||||
self.about_index = self.addTab(self.about, title, title)
|
||||
self.about.update_available_ready.connect(
|
||||
lambda: self.tabBar().setTabText(self.about_index, "About (!)")
|
||||
)
|
||||
|
||||
if self.args.debug:
|
||||
self.debug_index = self.addTab(DebugSettings(self), "Debug")
|
||||
title = self.tr("Debug")
|
||||
self.debug_index = self.addTab(DebugSettings(self), title, title)
|
||||
|
||||
self.setCurrentIndex(self.rare_index)
|
||||
|
|
|
@ -16,7 +16,7 @@ def versiontuple(v):
|
|||
try:
|
||||
return tuple(map(int, (v.split("."))))
|
||||
except Exception:
|
||||
return tuple((9, 9, 9)) # It is a beta version and newer
|
||||
return 9, 9, 9
|
||||
|
||||
|
||||
class About(QWidget):
|
||||
|
@ -63,7 +63,7 @@ class About(QWidget):
|
|||
|
||||
if self.update_available:
|
||||
logger.info(f"Update available: {__version__} -> {latest_tag}")
|
||||
self.ui.update_lbl.setText("{} -> {}".format(__version__, latest_tag))
|
||||
self.ui.update_lbl.setText(f"{__version__} -> {latest_tag}")
|
||||
self.update_available_ready.emit()
|
||||
else:
|
||||
self.ui.update_lbl.setText(self.tr("You have the latest version"))
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
import platform
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import QSettings, Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QLabel
|
||||
)
|
||||
|
||||
from rare.components.tabs.settings.widgets.env_vars import EnvVars
|
||||
from rare.components.tabs.settings.widgets.wrapper import WrapperSettings
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.ui.components.tabs.settings.game_settings import Ui_GameSettings
|
||||
|
||||
if platform.system() != "Windows":
|
||||
from rare.components.tabs.settings.widgets.linux import LinuxSettings
|
||||
if platform.system() != "Darwin":
|
||||
from rare.components.tabs.settings.widgets.proton import ProtonSettings
|
||||
|
||||
logger = getLogger("GameSettings")
|
||||
|
||||
|
||||
class DefaultGameSettings(QWidget):
|
||||
# variable to no update when changing game
|
||||
change = False
|
||||
app_name: str
|
||||
|
||||
def __init__(self, is_default, parent=None):
|
||||
super(DefaultGameSettings, self).__init__(parent=parent)
|
||||
self.ui = Ui_GameSettings()
|
||||
self.ui.setupUi(self)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.settings = QSettings()
|
||||
|
||||
self.wrapper_settings = WrapperSettings()
|
||||
|
||||
self.ui.launch_settings_group.layout().addRow(
|
||||
QLabel("Wrapper"), self.wrapper_settings
|
||||
)
|
||||
|
||||
self.env_vars = EnvVars(self)
|
||||
self.ui.game_settings_layout.addWidget(self.env_vars)
|
||||
|
||||
if platform.system() != "Windows":
|
||||
self.linux_settings = LinuxAppSettings()
|
||||
if platform.system() != "Darwin":
|
||||
self.proton_settings = ProtonSettings(self.linux_settings, self.wrapper_settings)
|
||||
self.ui.proton_layout.addWidget(self.proton_settings)
|
||||
self.proton_settings.environ_changed.connect(self.env_vars.reset_model)
|
||||
|
||||
# FIXME: Remove the spacerItem and margins from the linux settings
|
||||
# FIXME: This should be handled differently at soem point in the future
|
||||
# NOTE: specerItem has been removed
|
||||
self.linux_settings.layout().setContentsMargins(0, 0, 0, 0)
|
||||
# FIXME: End of FIXME
|
||||
self.ui.linux_settings_layout.addWidget(self.linux_settings)
|
||||
self.ui.linux_settings_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.linux_settings.mangohud.set_wrapper_activated.connect(
|
||||
lambda active: self.wrapper_settings.add_wrapper("mangohud")
|
||||
if active else self.wrapper_settings.delete_wrapper("mangohud"))
|
||||
self.linux_settings.environ_changed.connect(self.env_vars.reset_model)
|
||||
else:
|
||||
self.ui.linux_settings_widget.setVisible(False)
|
||||
|
||||
if is_default:
|
||||
self.ui.launch_settings_layout.removeRow(self.ui.skip_update)
|
||||
self.ui.launch_settings_layout.removeRow(self.ui.offline)
|
||||
self.ui.launch_settings_layout.removeRow(self.ui.launch_params)
|
||||
|
||||
self.load_settings("default")
|
||||
|
||||
def load_settings(self, app_name):
|
||||
self.app_name = app_name
|
||||
self.wrapper_settings.load_settings(app_name)
|
||||
if platform.system() != "Windows":
|
||||
self.linux_settings.update_game(app_name)
|
||||
proton = self.wrapper_settings.wrappers.get("proton", "")
|
||||
if proton:
|
||||
proton = proton.text
|
||||
if platform.system() != "Darwin":
|
||||
self.proton_settings.load_settings(app_name, proton)
|
||||
else:
|
||||
proton = ""
|
||||
if proton:
|
||||
self.linux_settings.ui.wine_groupbox.setEnabled(False)
|
||||
else:
|
||||
self.linux_settings.ui.wine_groupbox.setEnabled(True)
|
||||
self.env_vars.update_game(app_name)
|
||||
|
||||
|
||||
if platform.system() != "Windows":
|
||||
class LinuxAppSettings(LinuxSettings):
|
||||
def __init__(self):
|
||||
super(LinuxAppSettings, self).__init__()
|
||||
|
||||
def update_game(self, app_name):
|
||||
self.name = app_name
|
||||
self.wine_prefix.setText(self.load_prefix())
|
||||
self.wine_exec.setText(self.load_setting(self.name, "wine_executable"))
|
||||
|
||||
self.dxvk.load_settings(self.name)
|
||||
|
||||
self.mangohud.load_settings(self.name)
|
|
@ -1,11 +1,13 @@
|
|||
import platform as pf
|
||||
import re
|
||||
from logging import getLogger
|
||||
from typing import Tuple, List
|
||||
from typing import Tuple, Set
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSettings
|
||||
from PyQt5.QtGui import QShowEvent, QHideEvent
|
||||
from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox
|
||||
|
||||
from rare.models.options import options
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.shared.workers.worker import Worker
|
||||
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
|
||||
|
@ -19,14 +21,11 @@ class RefreshGameMetaWorker(Worker):
|
|||
class Signals(QObject):
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, platforms: List[str], include_unreal: bool):
|
||||
def __init__(self, platforms: Set[str], include_unreal: bool):
|
||||
super(RefreshGameMetaWorker, self).__init__()
|
||||
self.signals = RefreshGameMetaWorker.Signals()
|
||||
self.core = LegendaryCoreSingleton()
|
||||
if platforms:
|
||||
self.platforms = platforms
|
||||
else:
|
||||
self.platforms = ["Windows"]
|
||||
self.platforms = platforms if platforms else {"Windows"}
|
||||
self.skip_ue = not include_unreal
|
||||
|
||||
def run_real(self) -> None:
|
||||
|
@ -37,10 +36,11 @@ class RefreshGameMetaWorker(Worker):
|
|||
self.signals.finished.emit()
|
||||
|
||||
|
||||
class LegendarySettings(QWidget, Ui_LegendarySettings):
|
||||
class LegendarySettings(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(LegendarySettings, self).__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
self.ui = Ui_LegendarySettings()
|
||||
self.ui.setupUi(self)
|
||||
self.settings = QSettings(self)
|
||||
|
||||
self.core = LegendaryCoreSingleton()
|
||||
|
@ -53,7 +53,7 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
|
|||
file_mode=QFileDialog.DirectoryOnly,
|
||||
save_func=self.__mac_path_save,
|
||||
)
|
||||
self.install_dir_layout.addWidget(self.mac_install_dir)
|
||||
self.ui.install_dir_layout.addWidget(self.mac_install_dir)
|
||||
|
||||
# Platform-independent installation directory
|
||||
self.install_dir = PathEdit(
|
||||
|
@ -62,34 +62,34 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
|
|||
file_mode=QFileDialog.DirectoryOnly,
|
||||
save_func=self.__win_path_save,
|
||||
)
|
||||
self.install_dir_layout.addWidget(self.install_dir)
|
||||
self.ui.install_dir_layout.addWidget(self.install_dir)
|
||||
|
||||
# Max Workers
|
||||
max_workers = self.core.lgd.config["Legendary"].getint(
|
||||
"max_workers", fallback=0
|
||||
)
|
||||
self.max_worker_spin.setValue(max_workers)
|
||||
self.max_worker_spin.valueChanged.connect(self.max_worker_save)
|
||||
self.ui.max_worker_spin.setValue(max_workers)
|
||||
self.ui.max_worker_spin.valueChanged.connect(self.max_worker_save)
|
||||
# Max memory
|
||||
max_memory = self.core.lgd.config["Legendary"].getint("max_memory", fallback=0)
|
||||
self.max_memory_spin.setValue(max_memory)
|
||||
self.max_memory_spin.valueChanged.connect(self.max_memory_save)
|
||||
self.ui.max_memory_spin.setValue(max_memory)
|
||||
self.ui.max_memory_spin.valueChanged.connect(self.max_memory_save)
|
||||
# Preferred CDN
|
||||
preferred_cdn = self.core.lgd.config["Legendary"].get(
|
||||
"preferred_cdn", fallback=""
|
||||
)
|
||||
self.preferred_cdn_line.setText(preferred_cdn)
|
||||
self.preferred_cdn_line.textChanged.connect(self.preferred_cdn_save)
|
||||
self.ui.preferred_cdn_line.setText(preferred_cdn)
|
||||
self.ui.preferred_cdn_line.textChanged.connect(self.preferred_cdn_save)
|
||||
# Disable HTTPS
|
||||
disable_https = self.core.lgd.config["Legendary"].getboolean(
|
||||
"disable_https", fallback=False
|
||||
)
|
||||
self.disable_https_check.setChecked(disable_https)
|
||||
self.disable_https_check.stateChanged.connect(self.disable_https_save)
|
||||
self.ui.disable_https_check.setChecked(disable_https)
|
||||
self.ui.disable_https_check.stateChanged.connect(self.disable_https_save)
|
||||
|
||||
# Cleanup
|
||||
self.clean_button.clicked.connect(lambda: self.cleanup(False))
|
||||
self.clean_keep_manifests_button.clicked.connect(lambda: self.cleanup(True))
|
||||
self.ui.clean_button.clicked.connect(lambda: self.cleanup(False))
|
||||
self.ui.clean_keep_manifests_button.clicked.connect(lambda: self.cleanup(True))
|
||||
|
||||
self.locale_edit = IndicatorLineEdit(
|
||||
f"{self.core.language_code}-{self.core.country_code}",
|
||||
|
@ -98,58 +98,66 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
|
|||
horiz_policy=QSizePolicy.Minimum,
|
||||
parent=self,
|
||||
)
|
||||
self.locale_layout.addWidget(self.locale_edit)
|
||||
self.ui.locale_layout.addWidget(self.locale_edit)
|
||||
|
||||
self.fetch_win32_check.setChecked(self.settings.value("win32_meta", False, bool))
|
||||
self.fetch_win32_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue("win32_meta", self.fetch_win32_check.isChecked())
|
||||
self.ui.fetch_win32_check.setChecked(self.settings.value(*options.win32_meta))
|
||||
self.ui.fetch_win32_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.win32_meta.key, self.ui.fetch_win32_check.isChecked())
|
||||
)
|
||||
|
||||
self.fetch_macos_check.setChecked(self.settings.value("macos_meta", pf.system() == "Darwin", bool))
|
||||
self.fetch_macos_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue("macos_meta", self.fetch_macos_check.isChecked())
|
||||
self.ui.fetch_macos_check.setChecked(self.settings.value(*options.macos_meta))
|
||||
self.ui.fetch_macos_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.macos_meta.key, self.ui.fetch_macos_check.isChecked())
|
||||
)
|
||||
self.fetch_macos_check.setDisabled(pf.system() == "Darwin")
|
||||
self.ui.fetch_macos_check.setDisabled(pf.system() == "Darwin")
|
||||
|
||||
self.fetch_unreal_check.setChecked(self.settings.value("unreal_meta", False, bool))
|
||||
self.fetch_unreal_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue("unreal_meta", self.fetch_unreal_check.isChecked())
|
||||
self.ui.fetch_unreal_check.setChecked(self.settings.value(*options.unreal_meta))
|
||||
self.ui.fetch_unreal_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.unreal_meta.key, self.ui.fetch_unreal_check.isChecked())
|
||||
)
|
||||
|
||||
self.exclude_non_asset_check.setChecked(
|
||||
self.settings.value("exclude_non_asset", False, bool)
|
||||
)
|
||||
self.exclude_non_asset_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue("exclude_non_asset", self.exclude_non_asset_check.isChecked())
|
||||
)
|
||||
self.exclude_entitlements_check.setChecked(
|
||||
self.settings.value("exclude_entitlements", False, bool)
|
||||
)
|
||||
self.exclude_entitlements_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue("exclude_entitlements", self.exclude_entitlements_check.isChecked())
|
||||
self.ui.exclude_non_asset_check.setChecked(self.settings.value(*options.exclude_non_asset))
|
||||
self.ui.exclude_non_asset_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.exclude_non_asset.key, self.ui.exclude_non_asset_check.isChecked())
|
||||
)
|
||||
|
||||
self.refresh_metadata_button.clicked.connect(self.refresh_metadata)
|
||||
self.ui.exclude_entitlements_check.setChecked(self.settings.value(*options.exclude_entitlements))
|
||||
self.ui.exclude_entitlements_check.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.exclude_entitlements.key, self.ui.exclude_entitlements_check.isChecked())
|
||||
)
|
||||
|
||||
self.ui.refresh_metadata_button.clicked.connect(self.refresh_metadata)
|
||||
# FIXME: Disable the button for now because it interferes with RareCore
|
||||
self.refresh_metadata_button.setEnabled(False)
|
||||
self.refresh_metadata_button.setVisible(False)
|
||||
self.ui.refresh_metadata_button.setEnabled(False)
|
||||
self.ui.refresh_metadata_button.setVisible(False)
|
||||
|
||||
def showEvent(self, a0: QShowEvent):
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
return super().showEvent(a0)
|
||||
|
||||
def hideEvent(self, a0: QHideEvent):
|
||||
if a0.spontaneous():
|
||||
return super().hideEvent(a0)
|
||||
self.core.lgd.save_config()
|
||||
return super().hideEvent(a0)
|
||||
|
||||
def refresh_metadata(self):
|
||||
self.refresh_metadata_button.setDisabled(True)
|
||||
platforms = []
|
||||
if self.fetch_win32_check.isChecked():
|
||||
platforms.append("Win32")
|
||||
if self.fetch_macos_check.isChecked():
|
||||
platforms.append("Mac")
|
||||
worker = RefreshGameMetaWorker(platforms, self.fetch_unreal_check.isChecked())
|
||||
worker.signals.finished.connect(lambda: self.refresh_metadata_button.setDisabled(False))
|
||||
self.ui.refresh_metadata_button.setDisabled(True)
|
||||
platforms = set()
|
||||
if self.ui.fetch_win32_check.isChecked():
|
||||
platforms.add("Win32")
|
||||
if self.ui.fetch_macos_check.isChecked():
|
||||
platforms.add("Mac")
|
||||
worker = RefreshGameMetaWorker(platforms, self.ui.fetch_unreal_check.isChecked())
|
||||
worker.signals.finished.connect(lambda: self.ui.refresh_metadata_button.setDisabled(False))
|
||||
QThreadPool.globalInstance().start(worker)
|
||||
|
||||
@staticmethod
|
||||
def locale_edit_cb(text: str) -> Tuple[bool, str, int]:
|
||||
if text:
|
||||
if re.match("^[a-zA-Z]{2,3}[-_][a-zA-Z]{2,3}$", text):
|
||||
language, country = text.replace("_", "-").split("-")
|
||||
language, country = text.split("-" if "-" in text else "_")
|
||||
text = "-".join([language.lower(), country.upper()])
|
||||
if bool(re.match("^[a-z]{2,3}-[A-Z]{2,3}$", text)):
|
||||
return True, text, IndicatorReasonsCommon.VALID
|
||||
|
@ -162,10 +170,8 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
|
|||
if text:
|
||||
self.core.egs.language_code, self.core.egs.country_code = text.split("-")
|
||||
self.core.lgd.config.set("Legendary", "locale", text)
|
||||
else:
|
||||
if self.core.lgd.config.has_option("Legendary", "locale"):
|
||||
self.core.lgd.config.remove_option("Legendary", "locale")
|
||||
self.core.lgd.save_config()
|
||||
elif self.core.lgd.config.has_option("Legendary", "locale"):
|
||||
self.core.lgd.config.remove_option("Legendary", "locale")
|
||||
|
||||
def __mac_path_save(self, text: str) -> None:
|
||||
self.__path_save(text, "mac_install_dir")
|
||||
|
@ -180,40 +186,35 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
|
|||
if not text and option in self.core.lgd.config["Legendary"].keys():
|
||||
self.core.lgd.config["Legendary"].pop(option)
|
||||
else:
|
||||
logger.debug(f"Set %s option in config to %s", option, text)
|
||||
self.core.lgd.save_config()
|
||||
logger.debug("Set %s option in config to %s", option, text)
|
||||
|
||||
def max_worker_save(self, workers: str):
|
||||
if workers := int(workers):
|
||||
self.core.lgd.config.set("Legendary", "max_workers", str(workers))
|
||||
else:
|
||||
self.core.lgd.config.remove_option("Legendary", "max_workers")
|
||||
self.core.lgd.save_config()
|
||||
|
||||
def max_memory_save(self, memory: str):
|
||||
if memory := int(memory):
|
||||
self.core.lgd.config.set("Legendary", "max_memory", str(memory))
|
||||
else:
|
||||
self.core.lgd.config.remove_option("Legendary", "max_memory")
|
||||
self.core.lgd.save_config()
|
||||
|
||||
def preferred_cdn_save(self, cdn: str):
|
||||
if cdn:
|
||||
self.core.lgd.config.set("Legendary", "preferred_cdn", cdn.strip())
|
||||
else:
|
||||
self.core.lgd.config.remove_option("Legendary", "preferred_cdn")
|
||||
self.core.lgd.save_config()
|
||||
|
||||
def disable_https_save(self, checked: int):
|
||||
self.core.lgd.config.set(
|
||||
"Legendary", "disable_https", str(bool(checked)).lower()
|
||||
)
|
||||
self.core.lgd.save_config()
|
||||
|
||||
def cleanup(self, keep_manifests: bool):
|
||||
before = self.core.lgd.get_dir_size()
|
||||
logger.debug("Removing app metadata...")
|
||||
app_names = set(g.app_name for g in self.core.get_assets(update_assets=False))
|
||||
app_names = {g.app_name for g in self.core.get_assets(update_assets=False)}
|
||||
self.core.lgd.clean_metadata(app_names)
|
||||
|
||||
if not keep_manifests:
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import locale
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import QSettings, Qt
|
||||
from PyQt5.QtCore import QSettings, Qt, pyqtSlot, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtWidgets import QWidget, QMessageBox
|
||||
|
||||
from rare.components.tabs.settings.widgets.rpc import RPCSettings
|
||||
from rare.models.options import options, LibraryView
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
|
||||
from rare.utils.misc import (
|
||||
|
@ -22,116 +22,117 @@ from rare.utils.paths import create_desktop_link, desktop_link_path, log_dir, de
|
|||
|
||||
logger = getLogger("RareSettings")
|
||||
|
||||
languages = [("en", "English"),
|
||||
("de", "Deutsch"),
|
||||
("fr", "Français"),
|
||||
("zh-Hans", "Simplified Chinese"),
|
||||
("zh_TW", "Chinese Taiwan"),
|
||||
("pt_BR", "Portuguese (Brazil)"),
|
||||
("ca", "Catalan"),
|
||||
("ru", "Russian"),
|
||||
("tr", "Turkish"),
|
||||
("uk", "Ukrainian")]
|
||||
|
||||
|
||||
class RareSettings(QWidget, Ui_RareSettings):
|
||||
class RareSettings(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(RareSettings, self).__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
self.ui = Ui_RareSettings()
|
||||
self.ui.setupUi(self)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
# (widget_name, option_name, default)
|
||||
self.checkboxes = [
|
||||
(self.sys_tray, "sys_tray", True),
|
||||
(self.auto_update, "auto_update", False),
|
||||
(self.confirm_start, "confirm_start", False),
|
||||
(self.auto_sync_cloud, "auto_sync_cloud", False),
|
||||
(self.notification, "notification", True),
|
||||
(self.save_size, "save_size", False),
|
||||
(self.log_games, "show_console", False),
|
||||
]
|
||||
|
||||
self.settings = QSettings()
|
||||
language = self.settings.value("language", self.core.language_code, type=str)
|
||||
self.settings = QSettings(self)
|
||||
|
||||
# Select lang
|
||||
self.lang_select.addItems([i[1] for i in languages])
|
||||
if language in get_translations():
|
||||
index = [lang[0] for lang in languages].index(language)
|
||||
self.lang_select.setCurrentIndex(index)
|
||||
self.ui.lang_select.addItem(self.tr("System default"), options.language.default)
|
||||
for lang_code, title in get_translations():
|
||||
self.ui.lang_select.addItem(title, lang_code)
|
||||
language = self.settings.value(*options.language)
|
||||
if (index := self.ui.lang_select.findData(language, Qt.UserRole)) > 0:
|
||||
self.ui.lang_select.setCurrentIndex(index)
|
||||
else:
|
||||
self.lang_select.setCurrentIndex(0)
|
||||
self.lang_select.currentIndexChanged.connect(self.update_lang)
|
||||
self.ui.lang_select.setCurrentIndex(0)
|
||||
self.ui.lang_select.currentIndexChanged.connect(self.on_lang_changed)
|
||||
|
||||
colors = get_color_schemes()
|
||||
self.color_select.addItems(colors)
|
||||
if (color := self.settings.value("color_scheme")) in colors:
|
||||
self.color_select.setCurrentIndex(self.color_select.findText(color))
|
||||
self.color_select.setDisabled(False)
|
||||
self.style_select.setDisabled(True)
|
||||
self.ui.color_select.addItem(self.tr("None"), "")
|
||||
for item in get_color_schemes():
|
||||
self.ui.color_select.addItem(item, item)
|
||||
color = self.settings.value(*options.color_scheme)
|
||||
if (index := self.ui.color_select.findData(color, Qt.UserRole)) > 0:
|
||||
self.ui.color_select.setCurrentIndex(index)
|
||||
self.ui.color_select.setDisabled(False)
|
||||
self.ui.style_select.setDisabled(True)
|
||||
else:
|
||||
self.color_select.setCurrentIndex(0)
|
||||
self.color_select.currentIndexChanged.connect(self.on_color_select_changed)
|
||||
self.ui.color_select.setCurrentIndex(0)
|
||||
self.ui.color_select.currentIndexChanged.connect(self.on_color_select_changed)
|
||||
|
||||
styles = get_style_sheets()
|
||||
self.style_select.addItems(styles)
|
||||
if (style := self.settings.value("style_sheet")) in styles:
|
||||
self.style_select.setCurrentIndex(self.style_select.findText(style))
|
||||
self.style_select.setDisabled(False)
|
||||
self.color_select.setDisabled(True)
|
||||
self.ui.style_select.addItem(self.tr("None"), "")
|
||||
for item in get_style_sheets():
|
||||
self.ui.style_select.addItem(item, item)
|
||||
style = self.settings.value(*options.style_sheet)
|
||||
if (index := self.ui.style_select.findData(style, Qt.UserRole)) > 0:
|
||||
self.ui.style_select.setCurrentIndex(index)
|
||||
self.ui.style_select.setDisabled(False)
|
||||
self.ui.color_select.setDisabled(True)
|
||||
else:
|
||||
self.style_select.setCurrentIndex(0)
|
||||
self.style_select.currentIndexChanged.connect(self.on_style_select_changed)
|
||||
self.ui.style_select.setCurrentIndex(0)
|
||||
self.ui.style_select.currentIndexChanged.connect(self.on_style_select_changed)
|
||||
|
||||
self.ui.view_combo.addItem(self.tr("Game covers"), LibraryView.COVER)
|
||||
self.ui.view_combo.addItem(self.tr("Vertical list"), LibraryView.VLIST)
|
||||
view = LibraryView(self.settings.value(*options.library_view))
|
||||
if (index := self.ui.view_combo.findData(view)) > -1:
|
||||
self.ui.view_combo.setCurrentIndex(index)
|
||||
else:
|
||||
self.ui.view_combo.setCurrentIndex(0)
|
||||
self.ui.view_combo.currentIndexChanged.connect(self.on_view_combo_changed)
|
||||
|
||||
self.rpc = RPCSettings(self)
|
||||
self.right_layout.insertWidget(1, self.rpc, alignment=Qt.AlignTop)
|
||||
self.ui.right_layout.insertWidget(1, self.rpc, alignment=Qt.AlignTop)
|
||||
|
||||
self.init_checkboxes(self.checkboxes)
|
||||
self.sys_tray.stateChanged.connect(
|
||||
lambda: self.settings.setValue("sys_tray", self.sys_tray.isChecked())
|
||||
self.ui.sys_tray.setChecked(self.settings.value(*options.sys_tray))
|
||||
self.ui.sys_tray.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.sys_tray.key, self.ui.sys_tray.isChecked())
|
||||
)
|
||||
self.auto_update.stateChanged.connect(
|
||||
lambda: self.settings.setValue("auto_update", self.auto_update.isChecked())
|
||||
|
||||
self.ui.auto_update.setChecked(self.settings.value(*options.auto_update))
|
||||
self.ui.auto_update.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.auto_update.key, self.ui.auto_update.isChecked())
|
||||
)
|
||||
self.confirm_start.stateChanged.connect(
|
||||
lambda: self.settings.setValue(
|
||||
"confirm_start", self.confirm_start.isChecked()
|
||||
)
|
||||
|
||||
self.ui.confirm_start.setChecked(self.settings.value(*options.confirm_start))
|
||||
self.ui.confirm_start.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.confirm_start.key, self.ui.confirm_start.isChecked())
|
||||
)
|
||||
self.auto_sync_cloud.stateChanged.connect(
|
||||
lambda: self.settings.setValue(
|
||||
"auto_sync_cloud", self.auto_sync_cloud.isChecked()
|
||||
)
|
||||
|
||||
self.ui.auto_sync_cloud.setChecked(self.settings.value(*options.auto_sync_cloud))
|
||||
self.ui.auto_sync_cloud.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.auto_sync_cloud.key, self.ui.auto_sync_cloud.isChecked())
|
||||
)
|
||||
self.notification.stateChanged.connect(
|
||||
lambda: self.settings.setValue("notification", self.notification.isChecked())
|
||||
|
||||
self.ui.notification.setChecked(self.settings.value(*options.notification))
|
||||
self.ui.notification.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.notification.key, self.ui.notification.isChecked())
|
||||
)
|
||||
self.save_size.stateChanged.connect(self.save_window_size)
|
||||
self.log_games.stateChanged.connect(
|
||||
lambda: self.settings.setValue("show_console", self.log_games.isChecked())
|
||||
|
||||
self.ui.save_size.setChecked(self.settings.value(*options.save_size))
|
||||
self.ui.save_size.stateChanged.connect(self.save_window_size)
|
||||
|
||||
self.ui.log_games.setChecked(self.settings.value(*options.log_games))
|
||||
self.ui.log_games.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.log_games.key, self.ui.log_games.isChecked())
|
||||
)
|
||||
|
||||
if desktop_links_supported():
|
||||
self.desktop_link = desktop_link_path("Rare", "desktop")
|
||||
self.start_menu_link = desktop_link_path("Rare", "start_menu")
|
||||
else:
|
||||
self.desktop_link_btn.setToolTip(self.tr("Not supported"))
|
||||
self.desktop_link_btn.setDisabled(True)
|
||||
self.startmenu_link_btn.setToolTip(self.tr("Not supported"))
|
||||
self.startmenu_link_btn.setDisabled(True)
|
||||
self.ui.desktop_link_btn.setToolTip(self.tr("Not supported"))
|
||||
self.ui.desktop_link_btn.setDisabled(True)
|
||||
self.ui.startmenu_link_btn.setToolTip(self.tr("Not supported"))
|
||||
self.ui.startmenu_link_btn.setDisabled(True)
|
||||
self.desktop_link = ""
|
||||
self.start_menu_link = ""
|
||||
|
||||
if self.desktop_link and self.desktop_link.exists():
|
||||
self.desktop_link_btn.setText(self.tr("Remove desktop link"))
|
||||
self.ui.desktop_link_btn.setText(self.tr("Remove desktop link"))
|
||||
|
||||
if self.start_menu_link and self.start_menu_link.exists():
|
||||
self.startmenu_link_btn.setText(self.tr("Remove start menu link"))
|
||||
self.ui.startmenu_link_btn.setText(self.tr("Remove start menu link"))
|
||||
|
||||
self.desktop_link_btn.clicked.connect(self.create_desktop_link)
|
||||
self.startmenu_link_btn.clicked.connect(self.create_start_menu_link)
|
||||
self.ui.desktop_link_btn.clicked.connect(self.create_desktop_link)
|
||||
self.ui.startmenu_link_btn.clicked.connect(self.create_start_menu_link)
|
||||
|
||||
self.log_dir_open_button.clicked.connect(self.open_dir)
|
||||
self.log_dir_clean_button.clicked.connect(self.clean_logdir)
|
||||
self.ui.log_dir_open_button.clicked.connect(self.open_directory)
|
||||
self.ui.log_dir_clean_button.clicked.connect(self.clean_logdir)
|
||||
|
||||
# get size of logdir
|
||||
size = sum(
|
||||
|
@ -139,10 +140,11 @@ class RareSettings(QWidget, Ui_RareSettings):
|
|||
for f in log_dir().iterdir()
|
||||
if log_dir().joinpath(f).is_file()
|
||||
)
|
||||
self.log_dir_size_label.setText(format_size(size))
|
||||
self.ui.log_dir_size_label.setText(format_size(size))
|
||||
# self.log_dir_clean_button.setVisible(False)
|
||||
# self.log_dir_size_label.setVisible(False)
|
||||
|
||||
@pyqtSlot()
|
||||
def clean_logdir(self):
|
||||
for f in log_dir().iterdir():
|
||||
try:
|
||||
|
@ -155,17 +157,18 @@ class RareSettings(QWidget, Ui_RareSettings):
|
|||
for f in log_dir().iterdir()
|
||||
if log_dir().joinpath(f).is_file()
|
||||
)
|
||||
self.log_dir_size_label.setText(format_size(size))
|
||||
self.ui.log_dir_size_label.setText(format_size(size))
|
||||
|
||||
@pyqtSlot()
|
||||
def create_start_menu_link(self):
|
||||
try:
|
||||
if not os.path.exists(self.start_menu_link):
|
||||
if not create_desktop_link(app_name="rare_shortcut", link_type="start_menu"):
|
||||
return
|
||||
self.startmenu_link_btn.setText(self.tr("Remove start menu link"))
|
||||
self.ui.startmenu_link_btn.setText(self.tr("Remove start menu link"))
|
||||
else:
|
||||
os.remove(self.start_menu_link)
|
||||
self.startmenu_link_btn.setText(self.tr("Create start menu link"))
|
||||
self.ui.startmenu_link_btn.setText(self.tr("Create start menu link"))
|
||||
except PermissionError as e:
|
||||
logger.error(str(e))
|
||||
QMessageBox.warning(
|
||||
|
@ -174,15 +177,16 @@ class RareSettings(QWidget, Ui_RareSettings):
|
|||
self.tr("Permission error, cannot remove {}").format(self.start_menu_link),
|
||||
)
|
||||
|
||||
@pyqtSlot()
|
||||
def create_desktop_link(self):
|
||||
try:
|
||||
if not os.path.exists(self.desktop_link):
|
||||
if not create_desktop_link(app_name="rare_shortcut", link_type="desktop"):
|
||||
return
|
||||
self.desktop_link_btn.setText(self.tr("Remove Desktop link"))
|
||||
self.ui.desktop_link_btn.setText(self.tr("Remove Desktop link"))
|
||||
else:
|
||||
os.remove(self.desktop_link)
|
||||
self.desktop_link_btn.setText(self.tr("Create desktop link"))
|
||||
self.ui.desktop_link_btn.setText(self.tr("Create desktop link"))
|
||||
except PermissionError as e:
|
||||
logger.error(str(e))
|
||||
logger.warning(
|
||||
|
@ -191,43 +195,46 @@ class RareSettings(QWidget, Ui_RareSettings):
|
|||
self.tr("Permission error, cannot remove {}").format(self.start_menu_link),
|
||||
)
|
||||
|
||||
def on_color_select_changed(self, scheme):
|
||||
@pyqtSlot(int)
|
||||
def on_color_select_changed(self, index: int):
|
||||
scheme = self.ui.color_select.itemData(index, Qt.UserRole)
|
||||
if scheme:
|
||||
self.style_select.setCurrentIndex(0)
|
||||
self.style_select.setDisabled(True)
|
||||
self.settings.setValue("color_scheme", self.color_select.currentText())
|
||||
set_color_pallete(self.color_select.currentText())
|
||||
self.ui.style_select.setCurrentIndex(0)
|
||||
self.ui.style_select.setDisabled(True)
|
||||
else:
|
||||
self.settings.setValue("color_scheme", "")
|
||||
self.style_select.setDisabled(False)
|
||||
set_color_pallete("")
|
||||
self.ui.style_select.setDisabled(False)
|
||||
self.settings.setValue("color_scheme", scheme)
|
||||
set_color_pallete(scheme)
|
||||
|
||||
def on_style_select_changed(self, style):
|
||||
@pyqtSlot(int)
|
||||
def on_style_select_changed(self, index: int):
|
||||
style = self.ui.style_select.itemData(index, Qt.UserRole)
|
||||
if style:
|
||||
self.color_select.setCurrentIndex(0)
|
||||
self.color_select.setDisabled(True)
|
||||
self.settings.setValue("style_sheet", self.style_select.currentText())
|
||||
set_style_sheet(self.style_select.currentText())
|
||||
self.ui.color_select.setCurrentIndex(0)
|
||||
self.ui.color_select.setDisabled(True)
|
||||
else:
|
||||
self.settings.setValue("style_sheet", "")
|
||||
self.color_select.setDisabled(False)
|
||||
set_style_sheet("")
|
||||
self.ui.color_select.setDisabled(False)
|
||||
self.settings.setValue("style_sheet", style)
|
||||
set_style_sheet(style)
|
||||
|
||||
def open_dir(self):
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(log_dir()) # pylint: disable=E1101
|
||||
else:
|
||||
opener = "open" if sys.platform == "darwin" else "xdg-open"
|
||||
subprocess.Popen([opener, log_dir()])
|
||||
@pyqtSlot(int)
|
||||
def on_view_combo_changed(self, index: int):
|
||||
view = LibraryView(self.ui.view_combo.itemData(index, Qt.UserRole))
|
||||
self.settings.setValue(options.library_view.key, int(view))
|
||||
|
||||
@pyqtSlot()
|
||||
def open_directory(self):
|
||||
QDesktopServices.openUrl(QUrl(f"file://{log_dir()}"))
|
||||
|
||||
@pyqtSlot()
|
||||
def save_window_size(self):
|
||||
self.settings.setValue("save_size", self.save_size.isChecked())
|
||||
self.settings.remove("window_size")
|
||||
self.settings.setValue(options.save_size.key, self.ui.save_size.isChecked())
|
||||
self.settings.remove(options.window_size.key)
|
||||
|
||||
def update_lang(self, i: int):
|
||||
self.settings.setValue("language", languages[i][0])
|
||||
|
||||
def init_checkboxes(self, checkboxes):
|
||||
for cb in checkboxes:
|
||||
widget, option, default = cb
|
||||
widget.setChecked(self.settings.value(option, default, bool))
|
||||
@pyqtSlot(int)
|
||||
def on_lang_changed(self, index: int):
|
||||
lang_code = self.ui.lang_select.itemData(index, Qt.UserRole)
|
||||
if lang_code == locale.getlocale()[0]:
|
||||
self.settings.remove(options.language.key)
|
||||
else:
|
||||
self.settings.setValue(options.language.key, lang_code)
|
||||
|
|
43
rare/components/tabs/settings/settings.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import platform as pf
|
||||
from logging import getLogger
|
||||
|
||||
from .widgets.env_vars import EnvVars
|
||||
from .widgets.game import GameSettingsBase
|
||||
from .widgets.launch import LaunchSettingsBase
|
||||
from .widgets.overlay import DxvkSettings
|
||||
from .widgets.wrappers import WrapperSettings
|
||||
|
||||
if pf.system() != "Windows":
|
||||
from .widgets.wine import WineSettings
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
from .widgets.proton import ProtonSettings
|
||||
from .widgets.overlay import MangoHudSettings
|
||||
|
||||
logger = getLogger("GameSettings")
|
||||
|
||||
|
||||
class LaunchSettings(LaunchSettingsBase):
|
||||
def __init__(self, parent=None):
|
||||
super(LaunchSettings, self).__init__(WrapperSettings, parent=parent)
|
||||
|
||||
|
||||
class GameSettings(GameSettingsBase):
|
||||
def __init__(self, parent=None):
|
||||
if pf.system() != "Windows":
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
super(GameSettings, self).__init__(
|
||||
LaunchSettings, DxvkSettings, EnvVars,
|
||||
WineSettings, ProtonSettings, MangoHudSettings,
|
||||
parent=parent
|
||||
)
|
||||
else:
|
||||
super(GameSettings, self).__init__(
|
||||
LaunchSettings, DxvkSettings, EnvVars,
|
||||
WineSettings,
|
||||
parent=parent
|
||||
)
|
||||
else:
|
||||
super(GameSettings, self).__init__(
|
||||
LaunchSettings, DxvkSettings, EnvVars,
|
||||
parent=parent
|
||||
)
|
|
@ -1,26 +0,0 @@
|
|||
from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from .overlay_settings import OverlaySettings, CustomOption
|
||||
|
||||
|
||||
class DxvkSettings(OverlaySettings):
|
||||
def __init__(self):
|
||||
super(DxvkSettings, self).__init__(
|
||||
[
|
||||
("fps", QCoreApplication.translate("DxvkSettings", "FPS")),
|
||||
("frametime", QCoreApplication.translate("DxvkSettings", "Frametime")),
|
||||
("memory", QCoreApplication.translate("DxvkSettings", "Memory usage")),
|
||||
("gpuload", QCoreApplication.translate("DxvkSettings", "GPU usage")),
|
||||
("devinfo", QCoreApplication.translate("DxvkSettings", "Show Device info")),
|
||||
("version", QCoreApplication.translate("DxvkSettings", "DXVK Version")),
|
||||
("api", QCoreApplication.translate("DxvkSettings", "D3D feature level")),
|
||||
("compiler", QCoreApplication.translate("DxvkSettings", "Compiler activity")),
|
||||
],
|
||||
[
|
||||
(CustomOption.number_input("scale", 1, True), QCoreApplication.translate("DxvkSettings", "Scale"))
|
||||
],
|
||||
"DXVK_HUD", "0"
|
||||
)
|
||||
|
||||
self.setTitle(self.tr("DXVK Settings"))
|
||||
self.gb_options.setTitle(self.tr("Custom options"))
|
|
@ -1,6 +1,7 @@
|
|||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import QFileSystemWatcher, Qt
|
||||
from PyQt5.QtGui import QShowEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QGroupBox,
|
||||
QHeaderView,
|
||||
|
@ -20,6 +21,7 @@ class EnvVars(QGroupBox):
|
|||
self.setTitle(self.tr("Environment variables"))
|
||||
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.app_name: str = "default"
|
||||
|
||||
self.table_model = EnvVarsTableModel(self.core)
|
||||
self.table_view = QTableView(self)
|
||||
|
@ -44,8 +46,14 @@ class EnvVars(QGroupBox):
|
|||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.table_view)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
if e.key() in {Qt.Key_Delete, Qt.Key_Backspace}:
|
||||
def showEvent(self, a0: QShowEvent):
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.table_model.load(self.app_name)
|
||||
return super().showEvent(a0)
|
||||
|
||||
def keyPressEvent(self, a0):
|
||||
if a0.key() in {Qt.Key_Delete, Qt.Key_Backspace}:
|
||||
indexes = self.table_view.selectedIndexes()
|
||||
if not len(indexes):
|
||||
return
|
||||
|
@ -54,11 +62,8 @@ class EnvVars(QGroupBox):
|
|||
self.table_view.model().removeRow(idx.row())
|
||||
elif idx.column() == 1:
|
||||
self.table_view.model().setData(idx, "", Qt.EditRole)
|
||||
elif e.key() == Qt.Key_Escape:
|
||||
e.ignore()
|
||||
elif a0.key() == Qt.Key_Escape:
|
||||
a0.ignore()
|
||||
|
||||
def reset_model(self):
|
||||
self.table_model.reset()
|
||||
|
||||
def update_game(self, app_name):
|
||||
self.table_model.load(app_name)
|
||||
|
|
|
@ -8,15 +8,17 @@ from PyQt5.QtCore import Qt, QModelIndex, QAbstractTableModel, pyqtSlot
|
|||
from PyQt5.QtGui import QFont
|
||||
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
|
||||
if platform.system() != "Windows":
|
||||
if platform.system() != "Darwin":
|
||||
from rare.utils import proton
|
||||
from rare.utils.compat.wine import get_wine_environment
|
||||
|
||||
if platform.system() in {"Linux", "FreeBSD"}:
|
||||
from rare.utils.compat.steam import get_steam_environment
|
||||
|
||||
|
||||
class EnvVarsTableModel(QAbstractTableModel):
|
||||
def __init__(self, core: LegendaryCore, parent = None):
|
||||
def __init__(self, core: LegendaryCore, parent=None):
|
||||
super(EnvVarsTableModel, self).__init__(parent=parent)
|
||||
self.core = core
|
||||
|
||||
|
@ -26,15 +28,15 @@ class EnvVarsTableModel(QAbstractTableModel):
|
|||
self.__validator = re.compile(r"(^[A-Za-z_][A-Za-z0-9_]*)")
|
||||
self.__data_map: ChainMap = ChainMap()
|
||||
|
||||
self.__readonly = [
|
||||
"STEAM_COMPAT_DATA_PATH",
|
||||
"WINEPREFIX",
|
||||
self.__readonly = {
|
||||
"DXVK_HUD",
|
||||
"MANGOHUD",
|
||||
"MANGOHUD_CONFIG",
|
||||
]
|
||||
}
|
||||
if platform.system() != "Windows":
|
||||
if platform.system() != "Darwin":
|
||||
self.__readonly.extend(proton.get_steam_environment(None).keys())
|
||||
self.__readonly.update(get_wine_environment().keys())
|
||||
if platform.system() in {"Linux", "FreeBSD"}:
|
||||
self.__readonly.update(get_steam_environment().keys())
|
||||
|
||||
self.__default: str = "default"
|
||||
self.__appname: str = None
|
||||
|
@ -137,12 +139,12 @@ class EnvVarsTableModel(QAbstractTableModel):
|
|||
if orientation == Qt.Vertical:
|
||||
if section < self.__data_length():
|
||||
if self.__is_readonly(section) or not self.__is_local(section):
|
||||
return icon("mdi.lock", "ei.lock")
|
||||
return qta_icon("mdi.lock", "ei.lock")
|
||||
if self.__is_global(section) and self.__is_local(section):
|
||||
return icon("mdi.refresh", "ei.refresh")
|
||||
return qta_icon("mdi.refresh", "ei.refresh")
|
||||
if self.__is_local(section):
|
||||
return icon("mdi.delete", "ei.remove-sign")
|
||||
return icon("mdi.plus", "ei.plus-sign")
|
||||
return qta_icon("mdi.delete", "ei.remove-sign")
|
||||
return qta_icon("mdi.plus", "ei.plus-sign")
|
||||
if role == Qt.TextAlignmentRole:
|
||||
return Qt.AlignVCenter + Qt.AlignHCenter
|
||||
return None
|
||||
|
@ -256,8 +258,6 @@ class EnvVarsTableModel(QAbstractTableModel):
|
|||
if __name__ == "__main__":
|
||||
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QTableView, QHeaderView
|
||||
|
||||
from rare.resources import static_css
|
||||
from rare.resources.stylesheets import RareStyle
|
||||
from rare.utils.misc import set_style_sheet
|
||||
from legendary.core import LegendaryCore
|
||||
|
||||
|
|
81
rare/components/tabs/settings/widgets/game.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
import platform as pf
|
||||
from typing import Type
|
||||
|
||||
from PyQt5.QtCore import QSettings, Qt
|
||||
from PyQt5.QtGui import QHideEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout
|
||||
)
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.utils import config_helper as config
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from .env_vars import EnvVars
|
||||
from .launch import LaunchSettingsType
|
||||
from .overlay import DxvkSettings
|
||||
|
||||
if pf.system() != "Windows":
|
||||
from .wine import WineSettings
|
||||
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
from .proton import ProtonSettings
|
||||
from .overlay import MangoHudSettings
|
||||
|
||||
|
||||
class GameSettingsBase(QWidget, SideTabContents):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
launch_widget: Type[LaunchSettingsType],
|
||||
dxvk_widget: Type[DxvkSettings],
|
||||
envvar_widget: Type[EnvVars],
|
||||
wine_widget: Type['WineSettings'] = None,
|
||||
proton_widget: Type['ProtonSettings'] = None,
|
||||
mangohud_widget: Type['MangoHudSettings'] = None,
|
||||
parent=None
|
||||
):
|
||||
super(GameSettingsBase, self).__init__(parent=parent)
|
||||
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.settings = QSettings(self)
|
||||
self.app_name: str = "default"
|
||||
|
||||
self.launch = launch_widget(self)
|
||||
self.env_vars = envvar_widget(self)
|
||||
|
||||
if pf.system() != "Windows":
|
||||
self.wine = wine_widget(self)
|
||||
self.wine.environ_changed.connect(self.env_vars.reset_model)
|
||||
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
self.proton_tool = proton_widget(self)
|
||||
self.proton_tool.environ_changed.connect(self.env_vars.reset_model)
|
||||
self.proton_tool.tool_enabled.connect(self.wine.tool_enabled)
|
||||
self.proton_tool.tool_enabled.connect(self.launch.tool_enabled)
|
||||
|
||||
self.dxvk = dxvk_widget(self)
|
||||
self.dxvk.environ_changed.connect(self.env_vars.reset_model)
|
||||
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
self.mangohud = mangohud_widget(self)
|
||||
self.mangohud.environ_changed.connect(self.env_vars.reset_model)
|
||||
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.main_layout.addWidget(self.launch)
|
||||
if pf.system() != "Windows":
|
||||
self.main_layout.addWidget(self.wine)
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
self.main_layout.addWidget(self.proton_tool)
|
||||
self.main_layout.addWidget(self.dxvk)
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
self.main_layout.addWidget(self.mangohud)
|
||||
self.main_layout.addWidget(self.env_vars)
|
||||
|
||||
self.main_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
def hideEvent(self, a0: QHideEvent):
|
||||
if a0.spontaneous():
|
||||
return super().hideEvent(a0)
|
||||
config.save_config()
|
||||
return super().hideEvent(a0)
|
96
rare/components/tabs/settings/widgets/launch.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
from typing import Tuple, Type, TypeVar
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QShowEvent
|
||||
from PyQt5.QtWidgets import QCheckBox, QFileDialog, QFormLayout, QVBoxLayout, QGroupBox
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
import rare.utils.config_helper as config
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
from .wrappers import WrapperSettings
|
||||
|
||||
|
||||
class LaunchSettingsBase(QGroupBox):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
wrapper_widget: Type[WrapperSettings],
|
||||
parent=None
|
||||
):
|
||||
super(LaunchSettingsBase, self).__init__(parent=parent)
|
||||
self.setTitle(self.tr("Launch settings"))
|
||||
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.app_name: str = "default"
|
||||
|
||||
self.prelaunch_edit = PathEdit(
|
||||
path="",
|
||||
placeholder=self.tr("Path to script or program to run before the game launches"),
|
||||
file_mode=QFileDialog.ExistingFile,
|
||||
edit_func=self.__prelaunch_edit_callback,
|
||||
save_func=self.__prelaunch_save_callback,
|
||||
)
|
||||
|
||||
self.wrappers_widget = wrapper_widget(self)
|
||||
|
||||
self.prelaunch_check = QCheckBox(self.tr("Wait for command to finish before starting the game"))
|
||||
font = self.font()
|
||||
font.setItalic(True)
|
||||
self.prelaunch_check.setFont(font)
|
||||
self.prelaunch_check.stateChanged.connect(self.__prelauch_check_changed)
|
||||
|
||||
prelaunch_layout = QVBoxLayout()
|
||||
prelaunch_layout.addWidget(self.prelaunch_edit)
|
||||
prelaunch_layout.addWidget(self.prelaunch_check)
|
||||
|
||||
self.main_layout = QFormLayout(self)
|
||||
self.main_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
||||
self.main_layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.main_layout.setFormAlignment(Qt.AlignLeading | Qt.AlignTop)
|
||||
|
||||
self.main_layout.addRow(self.tr("Wrappers"), self.wrappers_widget)
|
||||
self.main_layout.addRow(self.tr("Prelaunch"), prelaunch_layout)
|
||||
|
||||
def showEvent(self, a0: QShowEvent):
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
command = config.get_option(self.app_name, "pre_launch_command", fallback="")
|
||||
wait = config.get_boolean(self.app_name, "pre_launch_wait", fallback=False)
|
||||
|
||||
self.prelaunch_edit.setText(command)
|
||||
self.prelaunch_check.setChecked(wait)
|
||||
self.prelaunch_check.setEnabled(bool(command))
|
||||
|
||||
return super().showEvent(a0)
|
||||
|
||||
@pyqtSlot()
|
||||
def tool_enabled(self):
|
||||
self.wrappers_widget.update_state()
|
||||
|
||||
@staticmethod
|
||||
def __prelaunch_edit_callback(text: str) -> Tuple[bool, str, int]:
|
||||
if not text.strip():
|
||||
return True, text, IndicatorReasonsCommon.VALID
|
||||
try:
|
||||
command = shlex.split(text)[0]
|
||||
except ValueError:
|
||||
return False, text, IndicatorReasonsCommon.WRONG_FORMAT
|
||||
if not os.path.isfile(command) and not shutil.which(command):
|
||||
return False, text, IndicatorReasonsCommon.FILE_NOT_EXISTS
|
||||
else:
|
||||
return True, text, IndicatorReasonsCommon.VALID
|
||||
|
||||
def __prelaunch_save_callback(self, text):
|
||||
config.save_option(self.app_name, "pre_launch_command", text)
|
||||
self.prelaunch_check.setEnabled(bool(text))
|
||||
if not text:
|
||||
config.remove_option(self.app_name, "pre_launch_wait")
|
||||
|
||||
def __prelauch_check_changed(self):
|
||||
config.set_boolean(self.app_name, "pre_launch_wait", self.prelaunch_check.isChecked())
|
||||
|
||||
|
||||
LaunchSettingsType = TypeVar("LaunchSettingsType", bound=LaunchSettingsBase)
|
|
@ -1,88 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QFileDialog, QWidget
|
||||
|
||||
from rare.components.tabs.settings.widgets.dxvk import DxvkSettings
|
||||
from rare.components.tabs.settings.widgets.mangohud import MangoHudSettings
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
from rare.ui.components.tabs.settings.linux import Ui_LinuxSettings
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
from rare.utils import config_helper
|
||||
|
||||
logger = getLogger("LinuxSettings")
|
||||
|
||||
|
||||
class LinuxSettings(QWidget):
|
||||
# str: option key
|
||||
environ_changed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, name=None, parent=None):
|
||||
super(LinuxSettings, self).__init__(parent=parent)
|
||||
self.ui = Ui_LinuxSettings()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.signals = GlobalSignalsSingleton()
|
||||
|
||||
self.name = name if name is not None else "default"
|
||||
|
||||
# Wine prefix
|
||||
self.wine_prefix = PathEdit(
|
||||
self.load_prefix(),
|
||||
file_mode=QFileDialog.DirectoryOnly,
|
||||
edit_func=lambda path: (os.path.isdir(path) or not path, path, IndicatorReasonsCommon.DIR_NOT_EXISTS),
|
||||
save_func=self.save_prefix,
|
||||
)
|
||||
self.ui.prefix_layout.addWidget(self.wine_prefix)
|
||||
|
||||
# Wine executable
|
||||
self.wine_exec = PathEdit(
|
||||
self.load_setting(self.name, "wine_executable"),
|
||||
file_mode=QFileDialog.ExistingFile,
|
||||
name_filters=["wine", "wine64"],
|
||||
edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS),
|
||||
save_func=lambda text: self.save_setting(
|
||||
text, section=self.name, setting="wine_executable"
|
||||
),
|
||||
)
|
||||
self.ui.exec_layout.addWidget(self.wine_exec)
|
||||
|
||||
# dxvk
|
||||
self.dxvk = DxvkSettings()
|
||||
self.dxvk.environ_changed.connect(self.environ_changed)
|
||||
self.ui.linux_layout.addWidget(self.dxvk)
|
||||
self.dxvk.load_settings(self.name)
|
||||
|
||||
self.mangohud = MangoHudSettings()
|
||||
self.mangohud.environ_changed.connect(self.environ_changed)
|
||||
self.ui.linux_layout.addWidget(self.mangohud)
|
||||
self.mangohud.load_settings(self.name)
|
||||
|
||||
|
||||
def load_prefix(self) -> str:
|
||||
return self.load_setting(
|
||||
f"{self.name}.env",
|
||||
"WINEPREFIX",
|
||||
fallback=self.load_setting(self.name, "wine_prefix"),
|
||||
)
|
||||
|
||||
def save_prefix(self, text: str):
|
||||
self.save_setting(text, f"{self.name}.env", "WINEPREFIX")
|
||||
self.environ_changed.emit("WINEPREFIX")
|
||||
self.save_setting(text, self.name, "wine_prefix")
|
||||
self.signals.application.prefix_updated.emit()
|
||||
|
||||
def load_setting(self, section: str, setting: str, fallback: str = ""):
|
||||
return self.core.lgd.config.get(section, setting, fallback=fallback)
|
||||
|
||||
def save_setting(self, text: str, section: str, setting: str):
|
||||
if text:
|
||||
config_helper.add_option(section, setting, text)
|
||||
logger.debug(f"Set {setting} in {f'[{section}]'} to {text}")
|
||||
else:
|
||||
config_helper.remove_option(section, setting)
|
||||
logger.debug(f"Unset {setting} from {f'[{section}]'}")
|
||||
config_helper.save_config()
|
|
@ -1,108 +0,0 @@
|
|||
import shutil
|
||||
from enum import Enum
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, pyqtSignal
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from .overlay_settings import OverlaySettings, CustomOption, ActivationStates
|
||||
from rare.utils import config_helper
|
||||
|
||||
position_values = ["default", "top-left", "top-right", "middle-left", "middle-right", "bottom-left",
|
||||
"bottom-right", "top-center"]
|
||||
|
||||
|
||||
class MangoHudSettings(OverlaySettings):
|
||||
|
||||
set_wrapper_activated = pyqtSignal(bool)
|
||||
|
||||
def __init__(self):
|
||||
super(MangoHudSettings, self).__init__(
|
||||
[
|
||||
("fps", QCoreApplication.translate("MangoSettings", "FPS")),
|
||||
("frame_timing", QCoreApplication.translate("MangoSettings", "Frame Time")),
|
||||
("cpu_stats", QCoreApplication.translate("MangoSettings", "CPU Load")),
|
||||
("gpu_stats", QCoreApplication.translate("MangoSettings", "GPU Load")),
|
||||
("cpu_temp", QCoreApplication.translate("MangoSettings", "CPU Temp")),
|
||||
("gpu_temp", QCoreApplication.translate("MangoSettings", "GPU Temp")),
|
||||
("ram", QCoreApplication.translate("MangoSettings", "Memory usage")),
|
||||
("vram", QCoreApplication.translate("MangoSettings", "VRAM usage")),
|
||||
("time", QCoreApplication.translate("MangoSettings", "Local Time")),
|
||||
("version", QCoreApplication.translate("MangoSettings", "MangoHud Version")),
|
||||
("arch", QCoreApplication.translate("MangoSettings", "System architecture")),
|
||||
("histogram", QCoreApplication.translate("MangoSettings", "FPS Graph")),
|
||||
("gpu_name", QCoreApplication.translate("MangoSettings", "GPU Name")),
|
||||
("cpu_power", QCoreApplication.translate("MangoSettings", "CPU Power consumption")),
|
||||
("gpu_power", QCoreApplication.translate("MangoSettings", "GPU Power consumption")),
|
||||
],
|
||||
[
|
||||
(
|
||||
CustomOption.number_input("font_size", 24, is_float=False),
|
||||
QCoreApplication.translate("MangoSettings", "Font size")
|
||||
),
|
||||
(
|
||||
CustomOption.select_input("position", position_values),
|
||||
QCoreApplication.translate("MangoSettings", "Position")
|
||||
)
|
||||
],
|
||||
"MANGOHUD_CONFIG", "no_display", set_activation_state=self.set_activation_state
|
||||
)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.setTitle(self.tr("MangoHud Settings"))
|
||||
self.gb_options.setTitle(self.tr("Custom options"))
|
||||
|
||||
def load_settings(self, name: str):
|
||||
self.settings_updatable = False
|
||||
self.name = name
|
||||
# override
|
||||
cfg = self.core.lgd.config.get(f"{name}.env", "MANGOHUD_CONFIG", fallback="")
|
||||
activated = "mangohud" in self.core.lgd.config.get(name, "wrapper", fallback="")
|
||||
if not activated:
|
||||
self.settings_updatable = False
|
||||
self.gb_options.setDisabled(True)
|
||||
for i, checkbox in enumerate(list(self.checkboxes.values())):
|
||||
checkbox.setChecked(i < 4)
|
||||
self.show_overlay_combo.setCurrentIndex(0)
|
||||
self.settings_updatable = True
|
||||
return
|
||||
super(MangoHudSettings, self).load_settings(name)
|
||||
self.settings_updatable = False
|
||||
self.show_overlay_combo.setCurrentIndex(2)
|
||||
self.gb_options.setDisabled(False)
|
||||
for var_name, checkbox in list(self.checkboxes.items())[:4]:
|
||||
checkbox.setChecked(f"{var_name}=0" not in cfg)
|
||||
self.settings_updatable = True
|
||||
|
||||
def set_activation_state(self, state: Enum): # pylint: disable=E0202
|
||||
if state in [ActivationStates.DEFAULT, ActivationStates.HIDDEN]:
|
||||
self.set_wrapper_activated.emit(False)
|
||||
self.gb_options.setDisabled(True)
|
||||
|
||||
elif state == ActivationStates.ACTIVATED:
|
||||
if not shutil.which("mangohud"):
|
||||
self.show_overlay_combo.setCurrentIndex(0)
|
||||
QMessageBox.warning(self, "Error", self.tr("Mangohud is not installed or not in path"))
|
||||
return
|
||||
|
||||
cfg = self.core.lgd.config.get(f"{self.name}.env", "MANGOHUD_CONFIG", fallback="")
|
||||
|
||||
split_config = cfg.split(",")
|
||||
for name in list(self.checkboxes.keys())[:4]:
|
||||
if name in split_config:
|
||||
split_config.remove(name)
|
||||
cfg = ",".join(split_config)
|
||||
|
||||
for var_name, checkbox in list(self.checkboxes.items())[:4]: # first three are by default activated
|
||||
if not checkbox.isChecked():
|
||||
if cfg:
|
||||
cfg += f",{var_name}=0"
|
||||
else:
|
||||
cfg = f"{var_name}=0"
|
||||
if cfg:
|
||||
config_helper.add_option(f"{self.name}.env", "MANGOHUD_CONFIG", cfg)
|
||||
self.environ_changed.emit(self.config_env_var_name)
|
||||
else:
|
||||
config_helper.remove_option(f"{self.name}.env", "MANGOHUD_CONFIG")
|
||||
self.environ_changed.emit(self.config_env_var_name)
|
||||
|
||||
self.set_wrapper_activated.emit(True)
|
337
rare/components/tabs/settings/widgets/overlay.py
Normal file
|
@ -0,0 +1,337 @@
|
|||
from abc import abstractmethod
|
||||
from enum import IntEnum
|
||||
from logging import getLogger
|
||||
from typing import List, Dict, Tuple, Union, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt
|
||||
from PyQt5.QtGui import QIntValidator, QDoubleValidator, QShowEvent
|
||||
from PyQt5.QtWidgets import QGroupBox, QCheckBox, QLineEdit, QComboBox
|
||||
|
||||
from rare.ui.components.tabs.settings.widgets.overlay import Ui_OverlaySettings
|
||||
from rare.utils import config_helper as config
|
||||
|
||||
logger = getLogger("GameOverlays")
|
||||
|
||||
|
||||
class OverlayLineEdit(QLineEdit):
|
||||
def __init__(self, option: str, placeholder: str, parent=None):
|
||||
self.option = option
|
||||
super(OverlayLineEdit, self).__init__(parent=parent)
|
||||
self.valueChanged = self.textChanged
|
||||
|
||||
self.setPlaceholderText(placeholder)
|
||||
|
||||
def setDefault(self):
|
||||
self.setText("")
|
||||
|
||||
def getValue(self) -> Optional[str]:
|
||||
return f"{self.option}={text}" if (text := self.text()) else None
|
||||
|
||||
def setValue(self, options: Dict[str, str]):
|
||||
if (value := options.get(self.option, None)) is not None:
|
||||
self.setText(value)
|
||||
options.pop(self.option)
|
||||
else:
|
||||
self.setDefault()
|
||||
|
||||
|
||||
class OverlayComboBox(QComboBox):
|
||||
def __init__(self, option: str, parent=None):
|
||||
self.option = option
|
||||
super(OverlayComboBox, self).__init__(parent=parent)
|
||||
self.valueChanged = self.currentIndexChanged
|
||||
|
||||
def setDefault(self):
|
||||
self.setCurrentIndex(0)
|
||||
|
||||
def getValue(self) -> Optional[str]:
|
||||
return f"{self.option}={self.currentText()}" if self.currentIndex() > 0 else None
|
||||
|
||||
def setValue(self, options: Dict[str, str]):
|
||||
if (value := options.get(self.option, None)) is not None:
|
||||
self.setCurrentText(value)
|
||||
options.pop(self.option)
|
||||
else:
|
||||
self.setDefault()
|
||||
|
||||
|
||||
class OverlayCheckBox(QCheckBox):
|
||||
def __init__(self, option: str, title: str, desc: str = "", default_enabled: bool = False, parent=None):
|
||||
self.option = option
|
||||
super().__init__(title, parent=parent)
|
||||
self.setChecked(default_enabled)
|
||||
self.default_enabled = default_enabled
|
||||
self.setToolTip(desc)
|
||||
|
||||
def setDefault(self):
|
||||
self.setChecked(self.default_enabled)
|
||||
|
||||
def getValue(self) -> Optional[str]:
|
||||
# lk: return the check state in case of non-default, otherwise None
|
||||
checked = self.isChecked()
|
||||
value = f"{self.option}={int(checked)}" if self.default_enabled else self.option
|
||||
return value if checked ^ self.default_enabled else None
|
||||
|
||||
def setValue(self, options: Dict[str, str]):
|
||||
if options.get(self.option, None) is not None:
|
||||
self.setChecked(not self.default_enabled)
|
||||
options.pop(self.option)
|
||||
else:
|
||||
self.setChecked(self.default_enabled)
|
||||
|
||||
|
||||
class OverlayStringInput(OverlayLineEdit):
|
||||
def __init__(self, option: str, placeholder: str, parent=None):
|
||||
super().__init__(option, placeholder, parent=parent)
|
||||
|
||||
|
||||
class OverlayNumberInput(OverlayLineEdit):
|
||||
def __init__(self, option: str, placeholder: Union[int, float], parent=None):
|
||||
super().__init__(option, str(placeholder), parent=parent)
|
||||
validator = QDoubleValidator(self) if isinstance(placeholder, float) else QIntValidator(self)
|
||||
self.setValidator(validator)
|
||||
|
||||
|
||||
class OverlaySelectInput(OverlayComboBox):
|
||||
def __init__(self, option: str, values: List, parent=None):
|
||||
super().__init__(option, parent=parent)
|
||||
# self.addItems([str(v) for v in values])
|
||||
self.addItems(map(str, values))
|
||||
|
||||
|
||||
class ActivationStates(IntEnum):
|
||||
GLOBAL = -1
|
||||
DISABLED = 0
|
||||
DEFAULTS = 1
|
||||
CUSTOM = 2
|
||||
|
||||
|
||||
class OverlaySettings(QGroupBox):
|
||||
# str: option key
|
||||
environ_changed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(OverlaySettings, self).__init__(parent=parent)
|
||||
self.ui = Ui_OverlaySettings()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.ui.show_overlay_combo.addItem(self.tr("Global"), ActivationStates.GLOBAL)
|
||||
self.ui.show_overlay_combo.addItem(self.tr("Disabled"), ActivationStates.DISABLED)
|
||||
self.ui.show_overlay_combo.addItem(self.tr("Enabled (defaults)"), ActivationStates.DEFAULTS)
|
||||
self.ui.show_overlay_combo.addItem(self.tr("Enabled (custom)"), ActivationStates.CUSTOM)
|
||||
|
||||
self.envvar: str = None
|
||||
self.force_disabled: str = None
|
||||
self.force_defaults: str = None
|
||||
self.app_name: str = "default"
|
||||
|
||||
self.option_widgets: List[Union[OverlayCheckBox, OverlayLineEdit, OverlayComboBox]] = []
|
||||
# self.checkboxes: Dict[str, OverlayCheckBox] = {}
|
||||
# self.values: Dict[str, Union[OverlayLineEdit, OverlayComboBox]] = {}
|
||||
|
||||
self.ui.options_group.setTitle(self.tr("Custom options"))
|
||||
self.ui.show_overlay_combo.currentIndexChanged.connect(self.update_settings)
|
||||
|
||||
def setupWidget(
|
||||
self,
|
||||
grid_map: List[OverlayCheckBox],
|
||||
form_map: List[Tuple[Union[OverlayLineEdit, OverlayComboBox], str]],
|
||||
envvar: str,
|
||||
force_disabled: str,
|
||||
force_defaults: str,
|
||||
):
|
||||
self.envvar = envvar
|
||||
self.force_disabled = force_disabled
|
||||
self.force_defaults = force_defaults
|
||||
|
||||
for i, widget in enumerate(grid_map):
|
||||
widget.setParent(self.ui.options_group)
|
||||
self.ui.options_grid.addWidget(widget, i // 4, i % 4)
|
||||
# self.checkboxes[widget.option] = widget
|
||||
self.option_widgets.append(widget)
|
||||
widget.stateChanged.connect(self.update_settings)
|
||||
|
||||
for widget, label in form_map:
|
||||
widget.setParent(self.ui.options_group)
|
||||
self.ui.options_form.addRow(label, widget)
|
||||
# self.values[widget.option] = widget
|
||||
self.option_widgets.append(widget)
|
||||
widget.valueChanged.connect(self.update_settings)
|
||||
|
||||
@abstractmethod
|
||||
def update_settings_override(self, state: ActivationStates):
|
||||
raise NotImplementedError
|
||||
|
||||
def update_settings(self):
|
||||
current_state = self.ui.show_overlay_combo.currentData(Qt.UserRole)
|
||||
self.ui.options_group.setEnabled(current_state == ActivationStates.CUSTOM)
|
||||
|
||||
if current_state == ActivationStates.GLOBAL:
|
||||
# System default (don't add any env variables)
|
||||
config.remove_envvar(self.app_name, self.envvar)
|
||||
|
||||
elif current_state == ActivationStates.DISABLED:
|
||||
# hidden
|
||||
config.set_envvar(self.app_name, self.envvar, self.force_disabled)
|
||||
|
||||
elif current_state == ActivationStates.DEFAULTS:
|
||||
config.set_envvar(self.app_name, self.envvar, self.force_defaults)
|
||||
|
||||
elif current_state == ActivationStates.CUSTOM:
|
||||
self.ui.options_group.setDisabled(False)
|
||||
# custom options
|
||||
options = (name for widget in self.option_widgets if (name := widget.getValue()) is not None)
|
||||
|
||||
config.set_envvar(self.app_name, self.envvar, ",".join(options))
|
||||
|
||||
self.environ_changed.emit(self.envvar)
|
||||
self.update_settings_override(current_state)
|
||||
|
||||
def setCurrentState(self, state: ActivationStates):
|
||||
self.ui.show_overlay_combo.setCurrentIndex(self.ui.show_overlay_combo.findData(state, Qt.UserRole))
|
||||
self.ui.options_group.setEnabled(state == ActivationStates.CUSTOM)
|
||||
|
||||
def showEvent(self, a0: QShowEvent):
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.ui.options_group.blockSignals(True)
|
||||
|
||||
config_options = config.get_envvar(self.app_name, self.envvar, fallback=None)
|
||||
if config_options is None:
|
||||
logger.debug("Overlay setting %s is not present", self.envvar)
|
||||
self.setCurrentState(ActivationStates.GLOBAL)
|
||||
|
||||
elif config_options == self.force_disabled:
|
||||
self.setCurrentState(ActivationStates.DISABLED)
|
||||
|
||||
elif config_options == self.force_defaults:
|
||||
self.setCurrentState(ActivationStates.DEFAULTS)
|
||||
|
||||
else:
|
||||
self.setCurrentState(ActivationStates.CUSTOM)
|
||||
opts = {}
|
||||
for o in config_options.split(","):
|
||||
if "=" in o:
|
||||
k, v = o.split("=")
|
||||
opts[k] = v
|
||||
else:
|
||||
# lk: The value doesn't matter other than not being None
|
||||
opts[o] = "enable"
|
||||
|
||||
for widget in self.option_widgets:
|
||||
widget.setValue(opts)
|
||||
if opts:
|
||||
logger.info("Remaining options without a gui switch: %s", ",".join(opts.keys()))
|
||||
|
||||
self.ui.options_group.blockSignals(False)
|
||||
return super().showEvent(a0)
|
||||
|
||||
|
||||
class DxvkSettings(OverlaySettings):
|
||||
def __init__(self, parent=None):
|
||||
super(DxvkSettings, self).__init__(parent=parent)
|
||||
self.setTitle(self.tr("DXVK settings"))
|
||||
grid = [
|
||||
OverlayCheckBox("fps", self.tr("FPS")),
|
||||
OverlayCheckBox("frametime", self.tr("Frametime")),
|
||||
OverlayCheckBox("memory", self.tr("Memory usage")),
|
||||
OverlayCheckBox("gpuload", self.tr("GPU usage")),
|
||||
OverlayCheckBox("devinfo", self.tr("Device info")),
|
||||
OverlayCheckBox("version", self.tr("DXVK version")),
|
||||
OverlayCheckBox("api", self.tr("D3D feature level")),
|
||||
OverlayCheckBox("compiler", self.tr("Compiler activity")),
|
||||
]
|
||||
form = [(OverlayNumberInput("scale", 1.0), self.tr("Scale"))]
|
||||
self.setupWidget(grid, form, "DXVK_HUD", "0", "1")
|
||||
|
||||
def update_settings_override(self, state: ActivationStates):
|
||||
pass
|
||||
|
||||
|
||||
mangohud_position = [
|
||||
"default",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"middle-left",
|
||||
"middle-right",
|
||||
"bottom-left",
|
||||
"bottom-right",
|
||||
"top-center",
|
||||
]
|
||||
|
||||
|
||||
class MangoHudSettings(OverlaySettings):
|
||||
def __init__(self, parent=None):
|
||||
super(MangoHudSettings, self).__init__(parent=parent)
|
||||
self.setTitle(self.tr("MangoHud settings"))
|
||||
grid = [
|
||||
OverlayCheckBox("read_cfg", self.tr("Read config")),
|
||||
OverlayCheckBox("fps", self.tr("FPS"), default_enabled=True),
|
||||
OverlayCheckBox("frame_timing", self.tr("Frame time"), default_enabled=True),
|
||||
OverlayCheckBox("cpu_stats", self.tr("CPU load"), default_enabled=True),
|
||||
OverlayCheckBox("gpu_stats", self.tr("GPU load"), default_enabled=True),
|
||||
OverlayCheckBox("cpu_temp", self.tr("CPU temperature")),
|
||||
OverlayCheckBox("gpu_temp", self.tr("GPU temperature")),
|
||||
OverlayCheckBox("ram", self.tr("Memory usage")),
|
||||
OverlayCheckBox("vram", self.tr("VRAM usage")),
|
||||
OverlayCheckBox("time", self.tr("Local time")),
|
||||
OverlayCheckBox("version", self.tr("MangoHud version")),
|
||||
OverlayCheckBox("arch", self.tr("System architecture")),
|
||||
OverlayCheckBox("histogram", self.tr("FPS graph")),
|
||||
OverlayCheckBox("gpu_name", self.tr("GPU name")),
|
||||
OverlayCheckBox("cpu_power", self.tr("CPU power consumption")),
|
||||
OverlayCheckBox("gpu_power", self.tr("GPU power consumption")),
|
||||
]
|
||||
form = [
|
||||
(OverlayNumberInput("font_size", 24), self.tr("Font size")),
|
||||
(OverlaySelectInput("position", mangohud_position), self.tr("Position")),
|
||||
]
|
||||
|
||||
self.setupWidget(grid, form, "MANGOHUD_CONFIG", "no_display", "read_cfg")
|
||||
|
||||
def showEvent(self, a0: QShowEvent):
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.ui.options_group.blockSignals(True)
|
||||
self.ui.options_group.blockSignals(False)
|
||||
return super().showEvent(a0)
|
||||
|
||||
def update_settings_override(self, state: IntEnum): # pylint: disable=E0202
|
||||
if state == ActivationStates.GLOBAL:
|
||||
config.remove_envvar(self.app_name, "MANGOHUD")
|
||||
|
||||
elif state == ActivationStates.DISABLED:
|
||||
config.set_envvar(self.app_name, "MANGOHUD", "0")
|
||||
|
||||
elif state == ActivationStates.DEFAULTS:
|
||||
config.set_envvar(self.app_name, "MANGOHUD", "1")
|
||||
|
||||
elif state == ActivationStates.CUSTOM:
|
||||
config.set_envvar(self.app_name, "MANGOHUD", "1")
|
||||
|
||||
self.environ_changed.emit("MANGOHUD")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout
|
||||
|
||||
from legendary.core import LegendaryCore
|
||||
|
||||
core = LegendaryCore()
|
||||
config.init_config_handler(core)
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
dlg = QDialog()
|
||||
|
||||
dxvk = DxvkSettings(dlg)
|
||||
mangohud = MangoHudSettings(dlg)
|
||||
|
||||
layout = QVBoxLayout(dlg)
|
||||
layout.addWidget(dxvk)
|
||||
layout.addWidget(mangohud)
|
||||
|
||||
dlg.show()
|
||||
ret = app.exec()
|
||||
config.save_config()
|
||||
sys.exit(ret)
|
|
@ -1,190 +0,0 @@
|
|||
from enum import Enum
|
||||
from logging import getLogger
|
||||
from typing import List, Dict, Tuple, Any, Callable
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtGui import QIntValidator, QDoubleValidator
|
||||
from PyQt5.QtWidgets import QGroupBox, QCheckBox, QWidget, QLineEdit, QLabel, QComboBox
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.ui.components.tabs.settings.widgets.overlay import Ui_OverlaySettings
|
||||
from rare.utils import config_helper
|
||||
|
||||
logger = getLogger("GameOverlays")
|
||||
|
||||
|
||||
class TextInputField(QLineEdit):
|
||||
def __init__(self):
|
||||
super(TextInputField, self).__init__()
|
||||
self.value_changed = self.textChanged
|
||||
self.set_value = self.setText
|
||||
self.set_default = lambda: self.setText("")
|
||||
|
||||
def get_value(self):
|
||||
return self.text()
|
||||
|
||||
|
||||
class ComboBox(QComboBox):
|
||||
def __init__(self):
|
||||
super(ComboBox, self).__init__()
|
||||
self.value_changed = self.currentIndexChanged
|
||||
self.get_value = self.currentText
|
||||
self.set_value = self.setCurrentText
|
||||
self.set_default = lambda: self.setCurrentIndex(0)
|
||||
|
||||
|
||||
class CustomOption:
|
||||
input_field: QWidget
|
||||
var_name: str
|
||||
|
||||
@classmethod
|
||||
def string_input(cls, var_name: str, placeholder: str):
|
||||
tmp = cls()
|
||||
tmp.input_field = TextInputField()
|
||||
tmp.var_name = var_name
|
||||
tmp.input_field.setPlaceholderText(placeholder)
|
||||
return tmp
|
||||
|
||||
@classmethod
|
||||
def number_input(cls, var_name: str, placeholder: Any, is_float: bool = False):
|
||||
tmp = cls()
|
||||
tmp.input_field = TextInputField()
|
||||
tmp.var_name = var_name
|
||||
tmp.input_field.setPlaceholderText(str(placeholder))
|
||||
if is_float:
|
||||
validator = QDoubleValidator()
|
||||
else:
|
||||
validator = QIntValidator()
|
||||
tmp.input_field.setValidator(validator)
|
||||
return tmp
|
||||
|
||||
@classmethod
|
||||
def select_input(cls, var_name: str, options: List[str]):
|
||||
"""options: default value in options[0]"""
|
||||
tmp = cls()
|
||||
tmp.input_field = ComboBox()
|
||||
tmp.var_name = var_name
|
||||
tmp.input_field.addItems(options)
|
||||
return tmp
|
||||
|
||||
|
||||
class ActivationStates(Enum):
|
||||
DEFAULT = 0
|
||||
HIDDEN = 1
|
||||
ACTIVATED = 2
|
||||
|
||||
|
||||
class OverlaySettings(QGroupBox, Ui_OverlaySettings):
|
||||
# str: option key
|
||||
environ_changed = pyqtSignal(str)
|
||||
name: str = "default"
|
||||
settings_updatable = True
|
||||
|
||||
def __init__(self, checkboxes_map: List[Tuple[str, str]], value_map: List[Tuple[CustomOption, str]],
|
||||
config_env_var_name: str, no_display_value: str,
|
||||
set_activation_state: Callable[[Enum], None] = lambda x: None):
|
||||
super(OverlaySettings, self).__init__()
|
||||
self.setupUi(self)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.config_env_var_name = config_env_var_name
|
||||
self.no_display_value = no_display_value
|
||||
self.set_activation_state = set_activation_state
|
||||
|
||||
self.checkboxes: Dict[str, QCheckBox] = {}
|
||||
|
||||
for i, (var_name, translated_text) in enumerate(checkboxes_map):
|
||||
cb = QCheckBox(translated_text)
|
||||
self.options_grid.addWidget(cb, i // 4, i % 4)
|
||||
self.checkboxes[var_name] = cb
|
||||
cb.stateChanged.connect(self.update_settings)
|
||||
|
||||
self.values: Dict[str, QWidget] = {}
|
||||
|
||||
num_rows = len(checkboxes_map) // 4
|
||||
for custom_option, translated_text in value_map:
|
||||
input_field = custom_option.input_field
|
||||
self.options_form.addRow(QLabel(translated_text), input_field)
|
||||
self.values[custom_option.var_name] = input_field
|
||||
input_field.value_changed.connect(self.update_settings)
|
||||
num_rows += 1
|
||||
|
||||
self.show_overlay_combo.currentIndexChanged.connect(self.update_settings)
|
||||
|
||||
def update_settings(self):
|
||||
if not self.settings_updatable:
|
||||
return
|
||||
if self.show_overlay_combo.currentIndex() == 0:
|
||||
# System default
|
||||
config_helper.remove_option(f"{self.name}.env", self.config_env_var_name)
|
||||
self.environ_changed.emit(self.config_env_var_name)
|
||||
self.gb_options.setDisabled(True)
|
||||
self.set_activation_state(ActivationStates.DEFAULT)
|
||||
return
|
||||
|
||||
elif self.show_overlay_combo.currentIndex() == 1:
|
||||
# hidden
|
||||
config_helper.add_option(f"{self.name}.env", self.config_env_var_name, self.no_display_value)
|
||||
self.environ_changed.emit(self.config_env_var_name)
|
||||
self.gb_options.setDisabled(True)
|
||||
self.set_activation_state(ActivationStates.HIDDEN)
|
||||
return
|
||||
elif self.show_overlay_combo.currentIndex() == 2:
|
||||
self.gb_options.setDisabled(False)
|
||||
# custom options
|
||||
var_names = []
|
||||
for var_name, cb in self.checkboxes.items():
|
||||
if cb.isChecked():
|
||||
var_names.append(var_name)
|
||||
|
||||
for var_name, input_field in self.values.items():
|
||||
text = input_field.get_value()
|
||||
if text not in ["default", ""]:
|
||||
var_names.append(f"{var_name}={text}")
|
||||
|
||||
if not var_names:
|
||||
list(self.checkboxes.values())[0].setChecked(True)
|
||||
var_names.append(list(self.checkboxes.keys())[0])
|
||||
|
||||
config_helper.add_option(f"{self.name}.env", self.config_env_var_name, ",".join(var_names))
|
||||
self.environ_changed.emit(self.config_env_var_name)
|
||||
self.set_activation_state(ActivationStates.ACTIVATED)
|
||||
|
||||
def load_settings(self, name: str):
|
||||
self.settings_updatable = False
|
||||
# load game specific
|
||||
self.name = name
|
||||
|
||||
for checkbox in self.checkboxes.values():
|
||||
checkbox.setChecked(False)
|
||||
for input_field in self.values.values():
|
||||
input_field.set_default()
|
||||
|
||||
options = self.core.lgd.config.get(f"{self.name}.env", self.config_env_var_name, fallback=None)
|
||||
if options is None:
|
||||
logger.debug(f"No Overlay settings found {self.config_env_var_name}")
|
||||
self.show_overlay_combo.setCurrentIndex(0)
|
||||
self.gb_options.setDisabled(True)
|
||||
|
||||
elif options == self.no_display_value:
|
||||
# not visible
|
||||
self.gb_options.setDisabled(True)
|
||||
self.show_overlay_combo.setCurrentIndex(1)
|
||||
|
||||
else:
|
||||
self.show_overlay_combo.setCurrentIndex(2)
|
||||
for option in options.split(","):
|
||||
try:
|
||||
if "=" in option:
|
||||
var_name, value = option.split("=")
|
||||
if var_name in self.checkboxes.keys():
|
||||
self.checkboxes[var_name].setChecked(False)
|
||||
else:
|
||||
self.values[var_name].set_value(value)
|
||||
else:
|
||||
self.checkboxes[option].setChecked(True)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
self.gb_options.setDisabled(False)
|
||||
|
||||
self.settings_updatable = True
|
|
@ -1,61 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
from typing import Tuple
|
||||
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QCheckBox, QFileDialog
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.utils import config_helper
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
|
||||
|
||||
class PreLaunchSettings(QHBoxLayout):
|
||||
app_name: str
|
||||
|
||||
def __init__(self):
|
||||
super(PreLaunchSettings, self).__init__()
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.edit = PathEdit(
|
||||
path="",
|
||||
placeholder=self.tr("Path to script"),
|
||||
file_mode=QFileDialog.ExistingFile,
|
||||
edit_func=self.edit_command,
|
||||
save_func=self.save_pre_launch_command,
|
||||
)
|
||||
self.layout().addWidget(self.edit)
|
||||
|
||||
self.wait_check = QCheckBox(self.tr("Wait for finish"))
|
||||
self.layout().addWidget(self.wait_check)
|
||||
self.wait_check.stateChanged.connect(self.save_wait_finish)
|
||||
|
||||
def edit_command(self, text: str) -> Tuple[bool, str, int]:
|
||||
if not text.strip():
|
||||
return True, text, IndicatorReasonsCommon.VALID
|
||||
|
||||
if not os.path.isfile(text.split()[0]) and not shutil.which(text.split()[0]):
|
||||
return False, text, IndicatorReasonsCommon.FILE_NOT_EXISTS
|
||||
else:
|
||||
return True, text, IndicatorReasonsCommon.VALID
|
||||
|
||||
def save_pre_launch_command(self, text):
|
||||
if text:
|
||||
config_helper.add_option(self.app_name, "pre_launch_command", text)
|
||||
self.wait_check.setDisabled(False)
|
||||
else:
|
||||
config_helper.remove_option(self.app_name, "pre_launch_command")
|
||||
self.wait_check.setDisabled(True)
|
||||
config_helper.remove_option(self.app_name, "pre_launch_wait")
|
||||
|
||||
def save_wait_finish(self):
|
||||
config_helper.add_option(self.app_name, "pre_launch_wait", str(self.wait_check.isChecked()).lower())
|
||||
|
||||
def load_settings(self, app_name):
|
||||
self.app_name = app_name
|
||||
|
||||
command = self.core.lgd.config.get(app_name, "pre_launch_command", fallback="")
|
||||
self.edit.setText(command)
|
||||
|
||||
wait = self.core.lgd.config.getboolean(app_name, "pre_launch_wait", fallback=False)
|
||||
self.wait_check.setChecked(wait)
|
||||
|
||||
self.wait_check.setEnabled(bool(command))
|
|
@ -1,85 +1,122 @@
|
|||
import os
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Union, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QGroupBox, QFileDialog
|
||||
from PyQt5.QtCore import pyqtSignal, Qt
|
||||
from PyQt5.QtGui import QShowEvent
|
||||
from PyQt5.QtWidgets import QGroupBox, QFileDialog, QFormLayout, QComboBox
|
||||
|
||||
from rare.components.tabs.settings import LinuxSettings
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings
|
||||
from rare.utils import config_helper, proton
|
||||
from rare.models.wrapper import Wrapper, WrapperType
|
||||
from rare.shared import RareCore
|
||||
from rare.shared.wrappers import Wrappers
|
||||
from rare.utils import config_helper as config
|
||||
from rare.utils.compat import steam
|
||||
from rare.utils.paths import proton_compat_dir
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
from .wrapper import WrapperSettings
|
||||
|
||||
logger = getLogger("Proton")
|
||||
logger = getLogger("ProtonSettings")
|
||||
|
||||
|
||||
class ProtonSettings(QGroupBox):
|
||||
# str: option key
|
||||
environ_changed = pyqtSignal(str)
|
||||
app_name: str
|
||||
changeable = True
|
||||
environ_changed: pyqtSignal = pyqtSignal(str)
|
||||
# bool: state
|
||||
tool_enabled: pyqtSignal = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, linux_settings: LinuxSettings, wrapper_settings: WrapperSettings):
|
||||
super(ProtonSettings, self).__init__()
|
||||
self.ui = Ui_ProtonSettings()
|
||||
self.ui.setupUi(self)
|
||||
self._linux_settings = linux_settings
|
||||
self._wrapper_settings = wrapper_settings
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.possible_proton_combos = proton.find_proton_combos()
|
||||
def __init__(self, parent=None):
|
||||
super(ProtonSettings, self).__init__(parent=parent)
|
||||
self.setTitle(self.tr("Proton settings"))
|
||||
|
||||
self.ui.proton_combo.addItems(self.possible_proton_combos)
|
||||
self.ui.proton_combo.currentIndexChanged.connect(self.change_proton)
|
||||
self.tool_combo = QComboBox(self)
|
||||
self.tool_combo.currentIndexChanged.connect(self.__on_proton_changed)
|
||||
|
||||
self.proton_prefix = PathEdit(
|
||||
self.tool_prefix = PathEdit(
|
||||
file_mode=QFileDialog.DirectoryOnly,
|
||||
edit_func=self.proton_prefix_edit,
|
||||
save_func=self.proton_prefix_save,
|
||||
placeholder=self.tr("Please select path for proton prefix")
|
||||
placeholder=self.tr("Please select path for proton prefix"),
|
||||
parent=self
|
||||
)
|
||||
self.ui.prefix_layout.addWidget(self.proton_prefix)
|
||||
|
||||
def change_proton(self, i):
|
||||
if not self.changeable:
|
||||
return
|
||||
# First combo box entry: Don't use Proton
|
||||
if i == 0:
|
||||
self._wrapper_settings.delete_wrapper("proton")
|
||||
config_helper.remove_option(self.app_name, "no_wine")
|
||||
config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH")
|
||||
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
|
||||
config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_CLIENT_INSTALL_PATH")
|
||||
self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
|
||||
layout = QFormLayout(self)
|
||||
layout.addRow(self.tr("Proton tool"), self.tool_combo)
|
||||
layout.addRow(self.tr("Compat data"), self.tool_prefix)
|
||||
layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
||||
layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
layout.setFormAlignment(Qt.AlignLeading | Qt.AlignTop)
|
||||
|
||||
self.proton_prefix.setEnabled(False)
|
||||
self.proton_prefix.setText("")
|
||||
self.app_name: str = "default"
|
||||
self.core = RareCore.instance().core()
|
||||
self.wrappers: Wrappers = RareCore.instance().wrappers()
|
||||
self.tool_wrapper: Optional[Wrapper] = None
|
||||
|
||||
self._linux_settings.ui.wine_groupbox.setEnabled(True)
|
||||
else:
|
||||
self.proton_prefix.setEnabled(True)
|
||||
self._linux_settings.ui.wine_groupbox.setEnabled(False)
|
||||
wrapper = self.possible_proton_combos[i - 1]
|
||||
self._wrapper_settings.add_wrapper(wrapper)
|
||||
config_helper.add_option(self.app_name, "no_wine", "true")
|
||||
config_helper.add_option(
|
||||
f"{self.app_name}.env",
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
||||
str(Path.home().joinpath(".steam", "steam"))
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
|
||||
self.tool_combo.blockSignals(True)
|
||||
self.tool_combo.clear()
|
||||
self.tool_combo.addItem(self.tr("Don't use a compatibility tool"), None)
|
||||
tools = steam.find_tools()
|
||||
for tool in tools:
|
||||
self.tool_combo.addItem(tool.name, tool)
|
||||
try:
|
||||
wrapper = next(
|
||||
filter(lambda w: w.is_compat_tool, self.wrappers.get_game_wrapper_list(self.app_name))
|
||||
)
|
||||
self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
|
||||
self.tool_wrapper = wrapper
|
||||
tool = next(filter(lambda t: t.checksum == wrapper.checksum, tools))
|
||||
index = self.tool_combo.findData(tool)
|
||||
except StopIteration:
|
||||
index = 0
|
||||
self.tool_combo.setCurrentIndex(index)
|
||||
self.tool_combo.blockSignals(False)
|
||||
|
||||
self.proton_prefix.setText(os.path.expanduser("~/.proton"))
|
||||
enabled = bool(self.tool_combo.currentData(Qt.UserRole))
|
||||
self.tool_prefix.blockSignals(True)
|
||||
self.tool_prefix.setText(config.get_proton_compatdata(self.app_name, fallback=""))
|
||||
self.tool_prefix.setEnabled(enabled)
|
||||
self.tool_prefix.blockSignals(False)
|
||||
|
||||
# Don't use Wine
|
||||
self._linux_settings.wine_exec.setText("")
|
||||
self._linux_settings.wine_prefix.setText("")
|
||||
super().showEvent(a0)
|
||||
|
||||
config_helper.save_config()
|
||||
def __on_proton_changed(self, index):
|
||||
steam_tool: Union[steam.ProtonTool, steam.CompatibilityTool] = self.tool_combo.itemData(index)
|
||||
|
||||
def proton_prefix_edit(self, text: str) -> Tuple[bool, str, int]:
|
||||
steam_environ = steam.get_steam_environment(steam_tool, self.tool_prefix.text())
|
||||
for key, value in steam_environ.items():
|
||||
config.save_envvar(self.app_name, key, value)
|
||||
self.environ_changed.emit(key)
|
||||
|
||||
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
|
||||
if self.tool_wrapper and self.tool_wrapper in wrappers:
|
||||
wrappers.remove(self.tool_wrapper)
|
||||
if steam_tool is None:
|
||||
self.tool_wrapper = None
|
||||
else:
|
||||
wrapper = Wrapper(
|
||||
command=steam_tool.command(), name=steam_tool.name, wtype=WrapperType.COMPAT_TOOL
|
||||
)
|
||||
wrappers.append(wrapper)
|
||||
self.tool_wrapper = wrapper
|
||||
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
|
||||
|
||||
self.tool_prefix.setEnabled(steam_tool is not None)
|
||||
if steam_tool:
|
||||
if not (compatdata_path := config.get_proton_compatdata(self.app_name, fallback="")):
|
||||
compatdata_path = proton_compat_dir(self.app_name)
|
||||
config.save_proton_compatdata(self.app_name, str(compatdata_path))
|
||||
target = compatdata_path.joinpath("pfx")
|
||||
if not target.is_dir():
|
||||
os.makedirs(target, exist_ok=True)
|
||||
self.tool_prefix.setText(str(compatdata_path))
|
||||
else:
|
||||
self.tool_prefix.setText("")
|
||||
|
||||
self.tool_enabled.emit(steam_tool is not None)
|
||||
|
||||
@staticmethod
|
||||
def proton_prefix_edit(text: str) -> Tuple[bool, str, int]:
|
||||
if not text:
|
||||
return False, text, IndicatorReasonsCommon.EMPTY
|
||||
parent_dir = os.path.dirname(text)
|
||||
|
@ -88,28 +125,6 @@ class ProtonSettings(QGroupBox):
|
|||
def proton_prefix_save(self, text: str):
|
||||
if not text:
|
||||
return
|
||||
config_helper.add_option(
|
||||
f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH", text
|
||||
)
|
||||
config.save_proton_compatdata(self.app_name, text)
|
||||
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
|
||||
config_helper.save_config()
|
||||
|
||||
def load_settings(self, app_name: str, proton: str):
|
||||
self.changeable = False
|
||||
self.app_name = app_name
|
||||
proton = proton.replace('"', "")
|
||||
self.proton_prefix.setEnabled(bool(proton))
|
||||
if proton:
|
||||
self.ui.proton_combo.setCurrentText(
|
||||
f'"{proton.replace(" run", "")}" run'
|
||||
)
|
||||
else:
|
||||
self.ui.proton_combo.setCurrentIndex(0)
|
||||
|
||||
proton_prefix = self.core.lgd.config.get(
|
||||
f"{app_name}.env",
|
||||
"STEAM_COMPAT_DATA_PATH",
|
||||
fallback="",
|
||||
)
|
||||
self.proton_prefix.setText(proton_prefix)
|
||||
self.changeable = True
|
||||
|
|
|
@ -2,33 +2,35 @@ from PyQt5.QtCore import QSettings
|
|||
from PyQt5.QtWidgets import QGroupBox
|
||||
|
||||
from rare.shared import GlobalSignalsSingleton
|
||||
from rare.models.options import options
|
||||
from rare.ui.components.tabs.settings.widgets.rpc import Ui_RPCSettings
|
||||
|
||||
|
||||
class RPCSettings(QGroupBox, Ui_RPCSettings):
|
||||
class RPCSettings(QGroupBox):
|
||||
def __init__(self, parent):
|
||||
super(RPCSettings, self).__init__(parent=parent)
|
||||
self.setupUi(self)
|
||||
self.ui = Ui_RPCSettings()
|
||||
self.ui.setupUi(self)
|
||||
self.signals = GlobalSignalsSingleton()
|
||||
|
||||
self.settings = QSettings()
|
||||
|
||||
self.enable.setCurrentIndex(self.settings.value("rpc_enable", 0, int))
|
||||
self.enable.currentIndexChanged.connect(self.changed)
|
||||
self.ui.enable.setCurrentIndex(self.settings.value(*options.rpc_enable))
|
||||
self.ui.enable.currentIndexChanged.connect(self.__enable_changed)
|
||||
|
||||
self.show_game.setChecked((self.settings.value("rpc_name", True, bool)))
|
||||
self.show_game.stateChanged.connect(
|
||||
lambda: self.settings.setValue("rpc_game", self.show_game.isChecked())
|
||||
self.ui.show_game.setChecked((self.settings.value(*options.rpc_name)))
|
||||
self.ui.show_game.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.rpc_name.key, self.ui.show_game.isChecked())
|
||||
)
|
||||
|
||||
self.show_os.setChecked((self.settings.value("rpc_os", True, bool)))
|
||||
self.show_os.stateChanged.connect(
|
||||
lambda: self.settings.setValue("rpc_os", self.show_os.isChecked())
|
||||
self.ui.show_os.setChecked((self.settings.value(*options.rpc_os)))
|
||||
self.ui.show_os.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.rpc_os.key, self.ui.show_os.isChecked())
|
||||
)
|
||||
|
||||
self.show_time.setChecked((self.settings.value("rpc_time", True, bool)))
|
||||
self.show_time.stateChanged.connect(
|
||||
lambda: self.settings.setValue("rpc_time", self.show_time.isChecked())
|
||||
self.ui.show_time.setChecked((self.settings.value(*options.rpc_time)))
|
||||
self.ui.show_time.stateChanged.connect(
|
||||
lambda: self.settings.setValue(options.rpc_time.key, self.ui.show_time.isChecked())
|
||||
)
|
||||
|
||||
try:
|
||||
|
@ -37,6 +39,6 @@ class RPCSettings(QGroupBox, Ui_RPCSettings):
|
|||
self.setDisabled(True)
|
||||
self.setToolTip(self.tr("Pypresence is not installed"))
|
||||
|
||||
def changed(self, i):
|
||||
self.settings.setValue("rpc_enable", i)
|
||||
def __enable_changed(self, i):
|
||||
self.settings.setValue(options.rpc_enable.key, i)
|
||||
self.signals.discord_rpc.apply_settings.emit()
|
||||
|
|
91
rare/components/tabs/settings/widgets/wine.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
import os
|
||||
from logging import getLogger
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QSignalBlocker
|
||||
from PyQt5.QtGui import QShowEvent
|
||||
from PyQt5.QtWidgets import QFileDialog, QFormLayout, QGroupBox
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
from rare.utils import config_helper as config
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
|
||||
logger = getLogger("WineSettings")
|
||||
|
||||
|
||||
class WineSettings(QGroupBox):
|
||||
# str: option key
|
||||
environ_changed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(WineSettings, self).__init__(parent=parent)
|
||||
self.setTitle(self.tr("Wine settings"))
|
||||
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.signals = GlobalSignalsSingleton()
|
||||
|
||||
self.app_name: Optional[str] = "default"
|
||||
|
||||
# Wine prefix
|
||||
self.wine_prefix = PathEdit(
|
||||
path="",
|
||||
file_mode=QFileDialog.DirectoryOnly,
|
||||
edit_func=lambda path: (os.path.isdir(path) or not path, path, IndicatorReasonsCommon.DIR_NOT_EXISTS),
|
||||
save_func=self.save_prefix,
|
||||
)
|
||||
|
||||
# Wine executable
|
||||
self.wine_exec = PathEdit(
|
||||
path="",
|
||||
file_mode=QFileDialog.ExistingFile,
|
||||
name_filters=["wine", "wine64"],
|
||||
edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS),
|
||||
save_func=self.save_exec,
|
||||
)
|
||||
|
||||
layout = QFormLayout(self)
|
||||
layout.addRow(self.tr("Executable"), self.wine_exec)
|
||||
layout.addRow(self.tr("Prefix"), self.wine_prefix)
|
||||
layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
||||
layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
layout.setFormAlignment(Qt.AlignLeading | Qt.AlignTop)
|
||||
|
||||
def showEvent(self, a0: QShowEvent):
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
|
||||
_ = QSignalBlocker(self.wine_prefix)
|
||||
self.wine_prefix.setText(self.load_prefix())
|
||||
_ = QSignalBlocker(self.wine_exec)
|
||||
self.wine_exec.setText(self.load_exec())
|
||||
self.setDisabled(config.get_boolean(self.app_name, "no_wine", fallback=False))
|
||||
|
||||
return super().showEvent(a0)
|
||||
|
||||
def tool_enabled(self, enabled: bool):
|
||||
if enabled:
|
||||
config.set_boolean(self.app_name, "no_wine", True)
|
||||
else:
|
||||
config.remove_option(self.app_name, "no_wine")
|
||||
self.setDisabled(enabled)
|
||||
|
||||
def load_prefix(self) -> str:
|
||||
if self.app_name is None:
|
||||
raise RuntimeError
|
||||
return config.get_wine_prefix(self.app_name, "")
|
||||
|
||||
def save_prefix(self, path: str) -> None:
|
||||
if self.app_name is None:
|
||||
raise RuntimeError
|
||||
config.save_wine_prefix(self.app_name, path)
|
||||
self.environ_changed.emit("WINEPREFIX")
|
||||
|
||||
def load_exec(self) -> str:
|
||||
if self.app_name is None:
|
||||
raise RuntimeError
|
||||
return config.get_option(self.app_name, "wine_executable", "")
|
||||
|
||||
def save_exec(self, text: str) -> None:
|
||||
if self.app_name is None:
|
||||
raise RuntimeError
|
||||
config.save_option(self.app_name, "wine_executable", text)
|
|
@ -1,356 +0,0 @@
|
|||
import re
|
||||
import shutil
|
||||
from logging import getLogger
|
||||
from typing import Dict, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QSettings, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication
|
||||
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QInputDialog,
|
||||
QFrame,
|
||||
QMessageBox,
|
||||
QSizePolicy,
|
||||
QWidget,
|
||||
QScrollArea,
|
||||
QAction,
|
||||
QToolButton,
|
||||
QMenu,
|
||||
)
|
||||
|
||||
from rare.shared import RareCore
|
||||
from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings
|
||||
from rare.utils import config_helper
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = getLogger("WrapperSettings")
|
||||
|
||||
extra_wrapper_regex = {
|
||||
"proton": "\".*proton\" run", # proton
|
||||
"mangohud": "mangohud" # mangohud
|
||||
}
|
||||
|
||||
|
||||
class Wrapper:
|
||||
pass
|
||||
|
||||
|
||||
class WrapperWidget(QFrame):
|
||||
update_wrapper = pyqtSignal(str, str)
|
||||
delete_wrapper = pyqtSignal(str)
|
||||
|
||||
def __init__(self, text: str, show_text=None, parent=None):
|
||||
super(WrapperWidget, self).__init__(parent=parent)
|
||||
if not show_text:
|
||||
show_text = text.split()[0]
|
||||
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
|
||||
self.text = text
|
||||
self.setToolTip(text)
|
||||
|
||||
unmanaged = show_text in extra_wrapper_regex.keys()
|
||||
|
||||
text_lbl = QLabel(show_text, parent=self)
|
||||
text_lbl.setFont(QFont("monospace"))
|
||||
text_lbl.setDisabled(unmanaged)
|
||||
|
||||
image_lbl = QLabel(parent=self)
|
||||
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
|
||||
|
||||
edit_action = QAction("Edit", parent=self)
|
||||
edit_action.triggered.connect(self.__edit)
|
||||
delete_action = QAction("Delete", parent=self)
|
||||
delete_action.triggered.connect(self.__delete)
|
||||
|
||||
manage_menu = QMenu(parent=self)
|
||||
manage_menu.addActions([edit_action, delete_action])
|
||||
|
||||
manage_button = QToolButton(parent=self)
|
||||
manage_button.setIcon(icon("mdi.menu"))
|
||||
manage_button.setMenu(manage_menu)
|
||||
manage_button.setPopupMode(QToolButton.InstantPopup)
|
||||
manage_button.setDisabled(unmanaged)
|
||||
if unmanaged:
|
||||
manage_button.setToolTip(self.tr("Manage through settings"))
|
||||
else:
|
||||
manage_button.setToolTip(self.tr("Manage"))
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(image_lbl)
|
||||
layout.addWidget(text_lbl)
|
||||
layout.addWidget(manage_button)
|
||||
self.setLayout(layout)
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName(type(self).__name__)
|
||||
manage_button.setObjectName(f"{self.objectName()}Button")
|
||||
|
||||
@pyqtSlot()
|
||||
def __delete(self):
|
||||
self.delete_wrapper.emit(self.text)
|
||||
|
||||
def __edit(self) -> None:
|
||||
dialog = QInputDialog(self)
|
||||
dialog.setWindowTitle(f"{self.tr('Edit wrapper')} - {QCoreApplication.instance().applicationName()}")
|
||||
dialog.setLabelText(self.tr("Edit wrapper command"))
|
||||
dialog.setTextValue(self.text)
|
||||
accepted = dialog.exec()
|
||||
wrapper = dialog.textValue()
|
||||
dialog.deleteLater()
|
||||
if accepted and wrapper:
|
||||
self.update_wrapper.emit(self.text, wrapper)
|
||||
|
||||
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
|
||||
if a0.buttons() == Qt.LeftButton:
|
||||
a0.accept()
|
||||
drag = QDrag(self)
|
||||
mime = QMimeData()
|
||||
drag.setMimeData(mime)
|
||||
drag.exec_(Qt.MoveAction)
|
||||
|
||||
|
||||
class WrapperSettings(QWidget):
|
||||
def __init__(self):
|
||||
super(WrapperSettings, self).__init__()
|
||||
self.ui = Ui_WrapperSettings()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.wrappers: Dict[str, WrapperWidget] = {}
|
||||
self.app_name: str = "default"
|
||||
|
||||
self.wrapper_scroll = QScrollArea(self.ui.widget_stack)
|
||||
self.wrapper_scroll.setWidgetResizable(True)
|
||||
self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
|
||||
self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.wrapper_scroll.setProperty("no_kinetic_scroll", True)
|
||||
self.scroll_content = WrapperContainer(
|
||||
save_cb=self.save, parent=self.wrapper_scroll
|
||||
)
|
||||
self.wrapper_scroll.setWidget(self.scroll_content)
|
||||
self.ui.widget_stack.insertWidget(0, self.wrapper_scroll)
|
||||
|
||||
self.core = RareCore.instance().core()
|
||||
|
||||
self.ui.add_button.clicked.connect(self.add_button_pressed)
|
||||
self.settings = QSettings()
|
||||
|
||||
self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName(type(self).__name__)
|
||||
self.ui.no_wrapper_label.setObjectName(f"{self.objectName()}Label")
|
||||
self.wrapper_scroll.setObjectName(f"{self.objectName()}Scroll")
|
||||
self.wrapper_scroll.horizontalScrollBar().setObjectName(
|
||||
f"{self.wrapper_scroll.objectName()}Bar")
|
||||
self.wrapper_scroll.verticalScrollBar().setObjectName(
|
||||
f"{self.wrapper_scroll.objectName()}Bar")
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def adjust_scrollarea(self, min: int, max: int):
|
||||
wrapper_widget = self.scroll_content.findChild(WrapperWidget)
|
||||
if not wrapper_widget:
|
||||
return
|
||||
# lk: when the scrollbar is not visible, min and max are 0
|
||||
if max > min:
|
||||
self.wrapper_scroll.setMaximumHeight(
|
||||
wrapper_widget.sizeHint().height()
|
||||
+ self.wrapper_scroll.rect().height() // 2
|
||||
- self.wrapper_scroll.contentsRect().height() // 2
|
||||
+ self.scroll_content.layout().spacing()
|
||||
+ self.wrapper_scroll.horizontalScrollBar().sizeHint().height()
|
||||
)
|
||||
else:
|
||||
self.wrapper_scroll.setMaximumHeight(
|
||||
wrapper_widget.sizeHint().height()
|
||||
+ self.wrapper_scroll.rect().height()
|
||||
- self.wrapper_scroll.contentsRect().height()
|
||||
)
|
||||
|
||||
def get_wrapper_string(self):
|
||||
return " ".join(self.get_wrapper_list())
|
||||
|
||||
def get_wrapper_list(self):
|
||||
wrappers = list(self.wrappers.values())
|
||||
wrappers.sort(key=lambda x: self.scroll_content.layout().indexOf(x))
|
||||
return [w.text for w in wrappers]
|
||||
|
||||
def add_button_pressed(self):
|
||||
dialog = QInputDialog(self)
|
||||
dialog.setWindowTitle(f"{self.tr('Add wrapper')} - {QCoreApplication.instance().applicationName()}")
|
||||
dialog.setLabelText(self.tr("Enter wrapper command"))
|
||||
accepted = dialog.exec()
|
||||
wrapper = dialog.textValue()
|
||||
dialog.deleteLater()
|
||||
if accepted:
|
||||
self.add_wrapper(wrapper)
|
||||
|
||||
def add_wrapper(self, text: str, position: int = -1, from_load: bool = False):
|
||||
if text == "mangohud" and self.wrappers.get("mangohud"):
|
||||
return
|
||||
show_text = ""
|
||||
for key, extra_wrapper in extra_wrapper_regex.items():
|
||||
if re.match(extra_wrapper, text):
|
||||
show_text = key
|
||||
if not show_text:
|
||||
show_text = text.split()[0]
|
||||
|
||||
# validate
|
||||
if not text.strip(): # is empty
|
||||
return
|
||||
if not from_load:
|
||||
if self.wrappers.get(text):
|
||||
QMessageBox.warning(
|
||||
self, self.tr("Warning"), self.tr("Wrapper <b>{0}</b> is already in the list").format(text)
|
||||
)
|
||||
return
|
||||
|
||||
if show_text != "proton" and not shutil.which(text.split()[0]):
|
||||
if (
|
||||
QMessageBox.question(
|
||||
self,
|
||||
self.tr("Warning"),
|
||||
self.tr("Wrapper <b>{0}</b> is not in $PATH. Add it anyway?").format(show_text),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
== QMessageBox.No
|
||||
):
|
||||
return
|
||||
|
||||
if text == "proton":
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
self.tr("Warning"),
|
||||
self.tr("Do not insert <b>proton</b> manually. Add it through Proton settings"),
|
||||
)
|
||||
return
|
||||
|
||||
self.ui.widget_stack.setCurrentIndex(0)
|
||||
|
||||
if widget := self.wrappers.get(show_text, None):
|
||||
widget.deleteLater()
|
||||
|
||||
widget = WrapperWidget(text, show_text, self.scroll_content)
|
||||
if position < 0:
|
||||
self.scroll_content.layout().addWidget(widget)
|
||||
else:
|
||||
self.scroll_content.layout().insertWidget(position, widget)
|
||||
self.adjust_scrollarea(
|
||||
self.wrapper_scroll.horizontalScrollBar().minimum(),
|
||||
self.wrapper_scroll.horizontalScrollBar().maximum(),
|
||||
)
|
||||
widget.update_wrapper.connect(self.update_wrapper)
|
||||
widget.delete_wrapper.connect(self.delete_wrapper)
|
||||
|
||||
self.wrappers[show_text] = widget
|
||||
|
||||
if not from_load:
|
||||
self.save()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def delete_wrapper(self, text: str):
|
||||
text = text.split()[0]
|
||||
widget = self.wrappers.get(text, None)
|
||||
if widget:
|
||||
self.wrappers.pop(text)
|
||||
widget.deleteLater()
|
||||
|
||||
if not self.wrappers:
|
||||
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
|
||||
self.ui.widget_stack.setCurrentIndex(1)
|
||||
|
||||
self.save()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def update_wrapper(self, old: str, new: str):
|
||||
key = old.split()[0]
|
||||
idx = self.scroll_content.layout().indexOf(self.wrappers[key])
|
||||
self.delete_wrapper(key)
|
||||
self.add_wrapper(new, position=idx)
|
||||
|
||||
def save(self):
|
||||
# save wrappers twice, to support wrappers with spaces
|
||||
if len(self.wrappers) == 0:
|
||||
config_helper.remove_option(self.app_name, "wrapper")
|
||||
self.settings.remove(f"{self.app_name}/wrapper")
|
||||
else:
|
||||
config_helper.add_option(self.app_name, "wrapper", self.get_wrapper_string())
|
||||
self.settings.setValue(f"{self.app_name}/wrapper", self.get_wrapper_list())
|
||||
|
||||
def load_settings(self, app_name: str):
|
||||
self.app_name = app_name
|
||||
for i in self.wrappers.values():
|
||||
i.deleteLater()
|
||||
self.wrappers.clear()
|
||||
|
||||
wrappers = self.settings.value(f"{self.app_name}/wrapper", [], str)
|
||||
|
||||
if not wrappers and (cfg := self.core.lgd.config.get(self.app_name, "wrapper", fallback="")):
|
||||
logger.info("Loading wrappers from legendary config")
|
||||
# no qt wrapper, but legendary wrapper, to have backward compatibility
|
||||
pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''')
|
||||
wrappers = pattern.split(cfg)[1::2]
|
||||
|
||||
for wrapper in wrappers:
|
||||
self.add_wrapper(wrapper, from_load=True)
|
||||
|
||||
if not self.wrappers:
|
||||
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
|
||||
self.ui.widget_stack.setCurrentIndex(1)
|
||||
else:
|
||||
self.ui.widget_stack.setCurrentIndex(0)
|
||||
|
||||
self.save()
|
||||
|
||||
|
||||
class WrapperContainer(QWidget):
|
||||
|
||||
def __init__(self, save_cb, parent=None):
|
||||
super(WrapperContainer, self).__init__(parent=parent)
|
||||
self.setAcceptDrops(True)
|
||||
self.save = save_cb
|
||||
layout = QHBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.drag_widget: Optional[QWidget] = None
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName(type(self).__name__)
|
||||
|
||||
def dragEnterEvent(self, e: QDragEnterEvent):
|
||||
widget = e.source()
|
||||
self.drag_widget = widget
|
||||
e.accept()
|
||||
|
||||
def _get_drop_index(self, x):
|
||||
drag_idx = self.layout().indexOf(self.drag_widget)
|
||||
|
||||
if drag_idx > 0:
|
||||
prev_widget = self.layout().itemAt(drag_idx - 1).widget()
|
||||
if x < self.drag_widget.x() - prev_widget.width() // 2:
|
||||
return drag_idx - 1
|
||||
if drag_idx < self.layout().count() - 1:
|
||||
next_widget = self.layout().itemAt(drag_idx + 1).widget()
|
||||
if x > self.drag_widget.x() + self.drag_widget.width() + next_widget.width() // 2:
|
||||
return drag_idx + 1
|
||||
|
||||
return drag_idx
|
||||
|
||||
def dragMoveEvent(self, e: QDragMoveEvent) -> None:
|
||||
i = self._get_drop_index(e.pos().x())
|
||||
self.layout().insertWidget(i, self.drag_widget)
|
||||
|
||||
def dropEvent(self, e: QDropEvent):
|
||||
pos = e.pos()
|
||||
widget = e.source()
|
||||
index = self._get_drop_index(pos.x())
|
||||
self.layout().insertWidget(index, widget)
|
||||
self.drag_widget = None
|
||||
e.accept()
|
||||
self.save()
|
434
rare/components/tabs/settings/widgets/wrappers.py
Normal file
|
@ -0,0 +1,434 @@
|
|||
import platform as pf
|
||||
import shlex
|
||||
import shutil
|
||||
from logging import getLogger
|
||||
from typing import Optional, Tuple, Iterable
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QSize, Qt, QMimeData, pyqtSlot, QObject, QEvent
|
||||
from PyQt5.QtGui import (
|
||||
QDrag,
|
||||
QDropEvent,
|
||||
QDragEnterEvent,
|
||||
QDragMoveEvent,
|
||||
QFont,
|
||||
QMouseEvent,
|
||||
QShowEvent,
|
||||
QResizeEvent,
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QFrame,
|
||||
QMessageBox,
|
||||
QSizePolicy,
|
||||
QWidget,
|
||||
QScrollArea,
|
||||
QAction,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QLineEdit,
|
||||
QVBoxLayout,
|
||||
QComboBox,
|
||||
)
|
||||
|
||||
from rare.models.wrapper import Wrapper
|
||||
from rare.shared import RareCore
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import ButtonDialog, game_title
|
||||
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
from rare.utils.compat import steam
|
||||
|
||||
logger = getLogger("WrapperSettings")
|
||||
|
||||
|
||||
class WrapperEditDialog(ButtonDialog):
|
||||
result_ready = pyqtSignal(bool, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(WrapperEditDialog, self).__init__(parent=parent)
|
||||
|
||||
self.line_edit = QLineEdit(self)
|
||||
self.line_edit.textChanged.connect(self.__on_text_changed)
|
||||
|
||||
self.widget_layout = QVBoxLayout()
|
||||
self.widget_layout.addWidget(self.line_edit)
|
||||
|
||||
self.setCentralLayout(self.widget_layout)
|
||||
|
||||
self.accept_button.setText(self.tr("Save"))
|
||||
self.accept_button.setIcon(qta_icon("fa.edit"))
|
||||
self.accept_button.setEnabled(False)
|
||||
|
||||
self.result: Tuple = ()
|
||||
|
||||
def setup(self, wrapper: Wrapper):
|
||||
header = self.tr("Edit wrapper")
|
||||
self.setWindowTitle(header)
|
||||
self.setSubtitle(game_title(header, wrapper.name))
|
||||
self.line_edit.setText(wrapper.as_str)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def __on_text_changed(self, text: str):
|
||||
self.accept_button.setEnabled(bool(text))
|
||||
|
||||
def done_handler(self):
|
||||
self.result_ready.emit(*self.result)
|
||||
|
||||
def accept_handler(self):
|
||||
self.result = (True, self.line_edit.text())
|
||||
|
||||
def reject_handler(self):
|
||||
self.result = (False, self.line_edit.text())
|
||||
|
||||
|
||||
class WrapperAddDialog(WrapperEditDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(WrapperAddDialog, self).__init__(parent=parent)
|
||||
self.combo_box = QComboBox(self)
|
||||
self.combo_box.addItem("None", "")
|
||||
self.combo_box.currentIndexChanged.connect(self.__on_index_changed)
|
||||
self.widget_layout.insertWidget(0, self.combo_box)
|
||||
|
||||
def setup(self, wrappers: Iterable[Wrapper]):
|
||||
header = self.tr("Add wrapper")
|
||||
self.setWindowTitle(header)
|
||||
self.setSubtitle(header)
|
||||
for wrapper in wrappers:
|
||||
self.combo_box.addItem(f"{wrapper.name} ({wrapper.as_str})", wrapper.as_str)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def __on_index_changed(self, index: int):
|
||||
command = self.combo_box.itemData(index, Qt.UserRole)
|
||||
self.line_edit.setText(command)
|
||||
|
||||
|
||||
class WrapperWidget(QFrame):
|
||||
# object: current, object: new
|
||||
update_wrapper = pyqtSignal(object, object)
|
||||
# object: current
|
||||
delete_wrapper = pyqtSignal(object)
|
||||
|
||||
def __init__(self, wrapper: Wrapper, parent=None):
|
||||
super(WrapperWidget, self).__init__(parent=parent)
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
self.setToolTip(wrapper.as_str)
|
||||
|
||||
text_lbl = QLabel(wrapper.name, parent=self)
|
||||
text_lbl.setFont(QFont("monospace"))
|
||||
text_lbl.setEnabled(wrapper.is_editable)
|
||||
|
||||
image_lbl = QLabel(parent=self)
|
||||
image_lbl.setPixmap(qta_icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
|
||||
|
||||
edit_action = QAction("Edit", parent=self)
|
||||
edit_action.triggered.connect(self.__on_edit)
|
||||
delete_action = QAction("Delete", parent=self)
|
||||
delete_action.triggered.connect(self.__on_delete)
|
||||
|
||||
manage_menu = QMenu(parent=self)
|
||||
manage_menu.addActions([edit_action, delete_action])
|
||||
|
||||
manage_button = QPushButton(parent=self)
|
||||
manage_button.setIcon(qta_icon("mdi.menu", fallback="fa.align-justify"))
|
||||
manage_button.setMenu(manage_menu)
|
||||
manage_button.setEnabled(wrapper.is_editable)
|
||||
if not wrapper.is_editable:
|
||||
manage_button.setToolTip(self.tr("Manage through settings"))
|
||||
else:
|
||||
manage_button.setToolTip(self.tr("Manage"))
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(image_lbl)
|
||||
layout.addWidget(text_lbl)
|
||||
layout.addWidget(manage_button)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.wrapper = wrapper
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName(type(self).__name__)
|
||||
manage_button.setObjectName(f"{self.objectName()}Button")
|
||||
|
||||
def data(self) -> Wrapper:
|
||||
return self.wrapper
|
||||
|
||||
@pyqtSlot()
|
||||
def __on_delete(self) -> None:
|
||||
self.delete_wrapper.emit(self.wrapper)
|
||||
self.deleteLater()
|
||||
|
||||
@pyqtSlot()
|
||||
def __on_edit(self) -> None:
|
||||
dialog = WrapperEditDialog(self)
|
||||
dialog.setup(self.wrapper)
|
||||
dialog.result_ready.connect(self.__on_edit_result)
|
||||
dialog.show()
|
||||
|
||||
@pyqtSlot(bool, str)
|
||||
def __on_edit_result(self, accepted: bool, command: str):
|
||||
if accepted and command:
|
||||
new_wrapper = Wrapper(command=shlex.split(command))
|
||||
self.update_wrapper.emit(self.wrapper, new_wrapper)
|
||||
self.deleteLater()
|
||||
|
||||
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
|
||||
if a0.buttons() == Qt.LeftButton:
|
||||
a0.accept()
|
||||
if self.wrapper.is_compat_tool:
|
||||
return
|
||||
drag = QDrag(self)
|
||||
mime = QMimeData()
|
||||
drag.setMimeData(mime)
|
||||
drag.exec_(Qt.MoveAction)
|
||||
|
||||
|
||||
class WrapperSettingsScroll(QScrollArea):
|
||||
def __init__(self, parent=None):
|
||||
super(WrapperSettingsScroll, self).__init__(parent=parent)
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
self.setWidgetResizable(True)
|
||||
self.setProperty("no_kinetic_scroll", True)
|
||||
|
||||
self.setObjectName(type(self).__name__)
|
||||
self.horizontalScrollBar().setObjectName(f"{self.objectName()}Bar")
|
||||
self.verticalScrollBar().setObjectName(f"{self.objectName()}Bar")
|
||||
|
||||
def setWidget(self, w):
|
||||
super().setWidget(w)
|
||||
w.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
|
||||
if a0 is self.widget() and a1.type() == QEvent.Resize:
|
||||
self.__resize(a0)
|
||||
return a0.event(a1)
|
||||
return False
|
||||
|
||||
def __resize(self, e: QResizeEvent):
|
||||
minh = self.horizontalScrollBar().minimum()
|
||||
maxh = self.horizontalScrollBar().maximum()
|
||||
# lk: when the scrollbar is not visible, min and max are 0
|
||||
if maxh > minh:
|
||||
height = (
|
||||
e.size().height()
|
||||
+ self.rect().height() // 2
|
||||
- self.contentsRect().height() // 2
|
||||
+ self.widget().layout().spacing()
|
||||
+ self.horizontalScrollBar().sizeHint().height()
|
||||
)
|
||||
else:
|
||||
height = e.size().height() + self.rect().height() - self.contentsRect().height()
|
||||
self.setMaximumHeight(max(height, self.minimumHeight()))
|
||||
|
||||
|
||||
class WrapperSettings(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(WrapperSettings, self).__init__(parent=parent)
|
||||
|
||||
self.wrapper_label = QLabel(self.tr("No wrappers defined"), self)
|
||||
self.wrapper_label.setFrameStyle(QLabel.StyledPanel | QLabel.Plain)
|
||||
self.wrapper_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
|
||||
self.add_button = QPushButton(self.tr("Add wrapper"), self)
|
||||
self.add_button.clicked.connect(self.__on_add)
|
||||
|
||||
self.wrapper_scroll = WrapperSettingsScroll(self)
|
||||
self.wrapper_scroll.setMinimumHeight(self.add_button.minimumSizeHint().height())
|
||||
|
||||
self.wrapper_container = WrapperContainer(self.wrapper_label, self.wrapper_scroll)
|
||||
self.wrapper_container.orderChanged.connect(self.__on_order_changed)
|
||||
self.wrapper_scroll.setWidget(self.wrapper_container)
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName("WrapperSettings")
|
||||
self.wrapper_label.setObjectName(f"{self.objectName()}Label")
|
||||
|
||||
main_layout = QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(self.wrapper_scroll, alignment=Qt.AlignTop)
|
||||
main_layout.addWidget(self.add_button, alignment=Qt.AlignTop)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
|
||||
self.app_name: str = "default"
|
||||
self.core = RareCore.instance().core()
|
||||
self.wrappers = RareCore.instance().wrappers()
|
||||
|
||||
def showEvent(self, a0: QShowEvent):
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.update_state()
|
||||
return super().showEvent(a0)
|
||||
|
||||
@pyqtSlot(QWidget, int)
|
||||
def __on_order_changed(self, widget: WrapperWidget, new_index: int):
|
||||
wrapper = widget.data()
|
||||
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
|
||||
wrappers.remove(wrapper)
|
||||
wrappers.insert(new_index, wrapper)
|
||||
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
|
||||
|
||||
@pyqtSlot()
|
||||
def __on_add(self) -> None:
|
||||
dialog = WrapperAddDialog(self)
|
||||
dialog.setup(self.wrappers.user_wrappers)
|
||||
dialog.result_ready.connect(self.__on_add_result)
|
||||
dialog.show()
|
||||
|
||||
@pyqtSlot(bool, str)
|
||||
def __on_add_result(self, accepted: bool, command: str):
|
||||
if accepted and command:
|
||||
wrapper = Wrapper(shlex.split(command))
|
||||
self.add_user_wrapper(wrapper)
|
||||
|
||||
def __add_wrapper(self, wrapper: Wrapper, position: int = -1):
|
||||
self.wrapper_label.setVisible(False)
|
||||
widget = WrapperWidget(wrapper, self.wrapper_container)
|
||||
if position < 0:
|
||||
self.wrapper_container.addWidget(widget)
|
||||
else:
|
||||
self.wrapper_container.insertWidget(position, widget)
|
||||
widget.update_wrapper.connect(self.__update_wrapper)
|
||||
widget.delete_wrapper.connect(self.__delete_wrapper)
|
||||
|
||||
def add_wrapper(self, wrapper: Wrapper, position: int = -1):
|
||||
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
|
||||
if position < 0 or wrapper.is_compat_tool:
|
||||
wrappers.append(wrapper)
|
||||
else:
|
||||
wrappers.insert(position, wrapper)
|
||||
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
|
||||
self.__add_wrapper(wrapper, position)
|
||||
|
||||
def add_user_wrapper(self, wrapper: Wrapper, position: int = -1):
|
||||
if not wrapper:
|
||||
return
|
||||
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
compat_cmds = [tool.command() for tool in steam.find_tools()]
|
||||
if wrapper.as_str in compat_cmds:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
self.tr("Warning"),
|
||||
self.tr("Do not insert compatibility tools manually. Add them through Proton settings"),
|
||||
)
|
||||
return
|
||||
|
||||
if wrapper.checksum in self.wrappers.get_game_md5sum_list(self.app_name):
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
self.tr("Warning"),
|
||||
self.tr("Wrapper <b>{0}</b> is already in the list").format(wrapper.as_str),
|
||||
)
|
||||
return
|
||||
|
||||
if not shutil.which(wrapper.executable):
|
||||
ans = QMessageBox.question(
|
||||
self,
|
||||
self.tr("Warning"),
|
||||
self.tr("Wrapper <b>{0}</b> is not in $PATH. Add it anyway?").format(wrapper.executable),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if ans == QMessageBox.No:
|
||||
return
|
||||
|
||||
self.add_wrapper(wrapper, position)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def __delete_wrapper(self, wrapper: Wrapper):
|
||||
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
|
||||
wrappers.remove(wrapper)
|
||||
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
|
||||
if not wrappers:
|
||||
self.wrapper_label.setVisible(True)
|
||||
|
||||
@pyqtSlot(object, object)
|
||||
def __update_wrapper(self, old: Wrapper, new: Wrapper):
|
||||
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
|
||||
index = wrappers.index(old)
|
||||
wrappers.remove(old)
|
||||
wrappers.insert(index, new)
|
||||
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
|
||||
self.__add_wrapper(new, index)
|
||||
|
||||
@pyqtSlot()
|
||||
def update_state(self):
|
||||
for w in self.wrapper_container.findChildren(WrapperWidget, options=Qt.FindDirectChildrenOnly):
|
||||
w.deleteLater()
|
||||
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
|
||||
if not wrappers:
|
||||
self.wrapper_label.setVisible(True)
|
||||
for wrapper in wrappers:
|
||||
self.__add_wrapper(wrapper)
|
||||
|
||||
|
||||
class WrapperContainer(QWidget):
|
||||
# QWidget: moving widget, int: new index
|
||||
orderChanged: pyqtSignal = pyqtSignal(QWidget, int)
|
||||
|
||||
def __init__(self, label: QLabel, parent=None):
|
||||
super(WrapperContainer, self).__init__(parent=parent)
|
||||
self.setAcceptDrops(True)
|
||||
self.__layout = QHBoxLayout()
|
||||
self.__drag_widget: Optional[QWidget] = None
|
||||
|
||||
main_layout = QHBoxLayout(self)
|
||||
main_layout.addWidget(label)
|
||||
main_layout.addLayout(self.__layout)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
main_layout.setSizeConstraint(QHBoxLayout.SetFixedSize)
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName(type(self).__name__)
|
||||
|
||||
# def count(self) -> int:
|
||||
# return self.__layout.count()
|
||||
#
|
||||
# def itemData(self, index: int) -> Any:
|
||||
# widget: WrapperWidget = self.__layout.itemAt(index).widget()
|
||||
# return widget.data()
|
||||
|
||||
def addWidget(self, widget: WrapperWidget):
|
||||
self.__layout.addWidget(widget)
|
||||
|
||||
def insertWidget(self, index: int, widget: WrapperWidget):
|
||||
self.__layout.insertWidget(index, widget)
|
||||
|
||||
def dragEnterEvent(self, e: QDragEnterEvent):
|
||||
widget = e.source()
|
||||
self.__drag_widget = widget
|
||||
e.accept()
|
||||
|
||||
def __get_drop_index(self, x) -> int:
|
||||
drag_idx = self.__layout.indexOf(self.__drag_widget)
|
||||
|
||||
if drag_idx > 0:
|
||||
prev_widget = self.__layout.itemAt(drag_idx - 1).widget()
|
||||
if x < self.__drag_widget.x() - prev_widget.width() // 2:
|
||||
return drag_idx - 1
|
||||
if drag_idx < self.__layout.count() - 1:
|
||||
next_widget = self.__layout.itemAt(drag_idx + 1).widget()
|
||||
if x > self.__drag_widget.x() + self.__drag_widget.width() + next_widget.width() // 2:
|
||||
return drag_idx + 1
|
||||
|
||||
return drag_idx
|
||||
|
||||
def dragMoveEvent(self, e: QDragMoveEvent) -> None:
|
||||
new_x = self.__get_drop_index(e.pos().x())
|
||||
self.__layout.insertWidget(new_x, self.__drag_widget)
|
||||
|
||||
def dropEvent(self, e: QDropEvent):
|
||||
pos = e.pos()
|
||||
widget = e.source()
|
||||
new_x = self.__get_drop_index(pos.x())
|
||||
self.__layout.insertWidget(new_x, widget)
|
||||
self.__drag_widget = None
|
||||
self.orderChanged.emit(widget, new_x)
|
||||
e.accept()
|
|
@ -1,61 +1,41 @@
|
|||
from PyQt5.QtGui import QShowEvent, QHideEvent
|
||||
from PyQt5.QtWidgets import QStackedWidget, QTabWidget
|
||||
from legendary.core import LegendaryCore
|
||||
|
||||
from rare.shared.rare_core import RareCore
|
||||
from rare.utils.paths import cache_dir
|
||||
from .game_info import ShopGameInfo
|
||||
from .search_results import SearchResults
|
||||
from .shop_api_core import ShopApiCore
|
||||
from .shop_widget import ShopWidget
|
||||
from .wishlist import WishlistWidget, Wishlist
|
||||
from rare.widgets.side_tab import SideTabWidget
|
||||
from .api.models.response import CatalogOfferModel
|
||||
from .landing import LandingWidget, LandingPage
|
||||
from .search import SearchPage
|
||||
from .store_api import StoreAPI
|
||||
from .wishlist import WishlistPage
|
||||
|
||||
|
||||
class Shop(QStackedWidget):
|
||||
init = False
|
||||
class StoreTab(SideTabWidget):
|
||||
|
||||
def __init__(self, core: LegendaryCore, parent=None):
|
||||
super(StoreTab, self).__init__(parent=parent)
|
||||
self.init = False
|
||||
|
||||
def __init__(self, core: LegendaryCore):
|
||||
super(Shop, self).__init__()
|
||||
self.core = core
|
||||
self.rcore = RareCore.instance()
|
||||
self.api_core = ShopApiCore(
|
||||
# self.rcore = RareCore.instance()
|
||||
self.api = StoreAPI(
|
||||
self.core.egs.session.headers["Authorization"],
|
||||
self.core.language_code,
|
||||
self.core.country_code,
|
||||
[] # [i.asset_infos["Windows"].namespace for i in self.rcore.game_list if bool(i.asset_infos)]
|
||||
)
|
||||
|
||||
self.shop = ShopWidget(cache_dir(), self.core, self.api_core)
|
||||
self.wishlist_widget = Wishlist(self.api_core)
|
||||
self.landing = LandingPage(self.api, parent=self)
|
||||
self.landing_index = self.addTab(self.landing, self.tr("Store"))
|
||||
|
||||
self.store_tabs = QTabWidget(parent=self)
|
||||
self.store_tabs.addTab(self.shop, self.tr("Games"))
|
||||
self.store_tabs.addTab(self.wishlist_widget, self.tr("Wishlist"))
|
||||
self.search = SearchPage(self.api, parent=self)
|
||||
self.search_index = self.addTab(self.search, self.tr("Search"))
|
||||
|
||||
self.addWidget(self.store_tabs)
|
||||
|
||||
self.search_results = SearchResults(self.api_core)
|
||||
self.addWidget(self.search_results)
|
||||
self.search_results.show_info.connect(self.show_game_info)
|
||||
self.info = ShopGameInfo(
|
||||
[i.asset_infos["Windows"].namespace for i in self.rcore.game_list if bool(i.asset_infos)],
|
||||
self.api_core,
|
||||
)
|
||||
self.addWidget(self.info)
|
||||
self.info.back_button.clicked.connect(lambda: self.setCurrentIndex(0))
|
||||
|
||||
self.search_results.back_button.clicked.connect(lambda: self.setCurrentIndex(0))
|
||||
self.shop.show_info.connect(self.show_search_results)
|
||||
|
||||
self.wishlist_widget.show_game_info.connect(self.show_game_info)
|
||||
self.shop.show_game.connect(self.show_game_info)
|
||||
self.api_core.update_wishlist.connect(self.update_wishlist)
|
||||
self.wishlist_widget.update_wishlist_signal.connect(self.update_wishlist)
|
||||
self.wishlist = WishlistPage(self.api, parent=self)
|
||||
self.wishlist_index = self.addTab(self.wishlist, self.tr("Wishlist"))
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous() or self.init:
|
||||
return super().showEvent(a0)
|
||||
self.shop.load()
|
||||
self.wishlist_widget.update_wishlist()
|
||||
self.init = True
|
||||
return super().showEvent(a0)
|
||||
|
||||
|
@ -64,14 +44,3 @@ class Shop(QStackedWidget):
|
|||
return super().hideEvent(a0)
|
||||
# TODO: Implement store unloading
|
||||
return super().hideEvent(a0)
|
||||
|
||||
def update_wishlist(self):
|
||||
self.shop.update_wishlist()
|
||||
|
||||
def show_game_info(self, data):
|
||||
self.info.update_game(data)
|
||||
self.setCurrentIndex(2)
|
||||
|
||||
def show_search_results(self, text: str):
|
||||
self.search_results.load_results(text)
|
||||
self.setCurrentIndex(1)
|
||||
|
|
39
rare/components/tabs/store/__main__.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
import sys
|
||||
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import QDialog, QApplication, QVBoxLayout
|
||||
from legendary.core import LegendaryCore
|
||||
|
||||
from . import StoreTab
|
||||
|
||||
|
||||
class StoreWindow(QDialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.core = LegendaryCore()
|
||||
self.core.login()
|
||||
self.store_tab = StoreTab(self.core, self)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.store_tab)
|
||||
|
||||
self.store_tab.show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import rare.resources.static_css
|
||||
# import rare.resources.stylesheets.RareStyle
|
||||
from rare.utils.misc import set_style_sheet
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Rare")
|
||||
app.setOrganizationName("Rare")
|
||||
|
||||
set_style_sheet("")
|
||||
set_style_sheet("RareStyle")
|
||||
window = StoreWindow()
|
||||
window.setWindowTitle(f"{app.applicationName()} - Store")
|
||||
window.resize(QSize(1280, 800))
|
||||
window.show()
|
||||
app.exec()
|
0
rare/components/tabs/store/api/__init__.py
Normal file
568
rare/components/tabs/store/api/constants/queries.py
Normal file
|
@ -0,0 +1,568 @@
|
|||
|
||||
FEED_QUERY = '''
|
||||
query feedQuery(
|
||||
$locale: String!
|
||||
$countryCode: String
|
||||
$offset: Int
|
||||
$postsPerPage: Int
|
||||
$category: String
|
||||
) {
|
||||
TransientStream {
|
||||
myTransientFeed(countryCode: $countryCode, locale: $locale) {
|
||||
id
|
||||
activity {
|
||||
... on LinkAccountActivity {
|
||||
type
|
||||
created_at
|
||||
platforms
|
||||
}
|
||||
... on SuggestedFriendsActivity {
|
||||
type
|
||||
created_at
|
||||
platform
|
||||
suggestions {
|
||||
epicId
|
||||
epicDisplayName
|
||||
platformFullName
|
||||
platformAvatar
|
||||
}
|
||||
}
|
||||
... on IncomingInvitesActivity {
|
||||
type
|
||||
created_at
|
||||
invites {
|
||||
epicId
|
||||
epicDisplayName
|
||||
}
|
||||
}
|
||||
... on RecentPlayersActivity {
|
||||
type
|
||||
created_at
|
||||
players {
|
||||
epicId
|
||||
epicDisplayName
|
||||
playedGameName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Blog {
|
||||
dieselBlogPosts: getPosts(
|
||||
locale: $locale
|
||||
offset: $offset
|
||||
postsPerPage: $postsPerPage
|
||||
category: $category
|
||||
) {
|
||||
blogList {
|
||||
_id
|
||||
author
|
||||
category
|
||||
content
|
||||
urlPattern
|
||||
slug
|
||||
sticky
|
||||
title
|
||||
date
|
||||
image
|
||||
shareImage
|
||||
trendingImage
|
||||
url
|
||||
featured
|
||||
link
|
||||
externalLink
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
REVIEWS_QUERY = '''
|
||||
query productReviewsQuery($sku: String!) {
|
||||
OpenCritic {
|
||||
productReviews(sku: $sku) {
|
||||
id
|
||||
name
|
||||
openCriticScore
|
||||
reviewCount
|
||||
percentRecommended
|
||||
openCriticUrl
|
||||
award
|
||||
topReviews {
|
||||
publishedDate
|
||||
externalUrl
|
||||
snippet
|
||||
language
|
||||
score
|
||||
author
|
||||
ScoreFormat {
|
||||
id
|
||||
description
|
||||
}
|
||||
OutletId
|
||||
outletName
|
||||
displayScore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
MEDIA_QUERY = '''
|
||||
query fetchMediaRef($mediaRefId: String!) {
|
||||
Media {
|
||||
getMediaRef(mediaRefId: $mediaRefId) {
|
||||
accountId
|
||||
outputs {
|
||||
duration
|
||||
url
|
||||
width
|
||||
height
|
||||
key
|
||||
contentType
|
||||
}
|
||||
namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
ADDONS_QUERY = '''
|
||||
query getAddonsByNamespace(
|
||||
$categories: String!
|
||||
$count: Int!
|
||||
$country: String!
|
||||
$locale: String!
|
||||
$namespace: String!
|
||||
$sortBy: String!
|
||||
$sortDir: String!
|
||||
) {
|
||||
Catalog {
|
||||
catalogOffers(
|
||||
namespace: $namespace
|
||||
locale: $locale
|
||||
params: {
|
||||
category: $categories
|
||||
count: $count
|
||||
country: $country
|
||||
sortBy: $sortBy
|
||||
sortDir: $sortDir
|
||||
}
|
||||
) {
|
||||
elements {
|
||||
countriesBlacklist
|
||||
customAttributes {
|
||||
key
|
||||
value
|
||||
}
|
||||
description
|
||||
developer
|
||||
effectiveDate
|
||||
id
|
||||
isFeatured
|
||||
keyImages {
|
||||
type
|
||||
url
|
||||
}
|
||||
lastModifiedDate
|
||||
longDescription
|
||||
namespace
|
||||
offerType
|
||||
productSlug
|
||||
releaseDate
|
||||
status
|
||||
technicalDetails
|
||||
title
|
||||
urlSlug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
CATALOG_QUERY = '''
|
||||
query catalogQuery(
|
||||
$category: String
|
||||
$count: Int
|
||||
$country: String!
|
||||
$keywords: String
|
||||
$locale: String
|
||||
$namespace: String!
|
||||
$sortBy: String
|
||||
$sortDir: String
|
||||
$start: Int
|
||||
$tag: String
|
||||
) {
|
||||
Catalog {
|
||||
catalogOffers(
|
||||
namespace: $namespace
|
||||
locale: $locale
|
||||
params: {
|
||||
count: $count
|
||||
country: $country
|
||||
category: $category
|
||||
keywords: $keywords
|
||||
sortBy: $sortBy
|
||||
sortDir: $sortDir
|
||||
start: $start
|
||||
tag: $tag
|
||||
}
|
||||
) {
|
||||
elements {
|
||||
isFeatured
|
||||
collectionOfferIds
|
||||
title
|
||||
id
|
||||
namespace
|
||||
description
|
||||
keyImages {
|
||||
type
|
||||
url
|
||||
}
|
||||
seller {
|
||||
id
|
||||
name
|
||||
}
|
||||
productSlug
|
||||
urlSlug
|
||||
items {
|
||||
id
|
||||
namespace
|
||||
}
|
||||
customAttributes {
|
||||
key
|
||||
value
|
||||
}
|
||||
categories {
|
||||
path
|
||||
}
|
||||
price(country: $country) {
|
||||
totalPrice {
|
||||
discountPrice
|
||||
originalPrice
|
||||
voucherDiscount
|
||||
discount
|
||||
fmtPrice(locale: $locale) {
|
||||
originalPrice
|
||||
discountPrice
|
||||
intermediatePrice
|
||||
}
|
||||
}
|
||||
lineOffers {
|
||||
appliedRules {
|
||||
id
|
||||
endDate
|
||||
}
|
||||
}
|
||||
}
|
||||
linkedOfferId
|
||||
linkedOffer {
|
||||
effectiveDate
|
||||
customAttributes {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
paging {
|
||||
count
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
CATALOG_TAGS_QUERY = '''
|
||||
query catalogTags($namespace: String!) {
|
||||
Catalog {
|
||||
tags(namespace: $namespace, start: 0, count: 999) {
|
||||
elements {
|
||||
aliases
|
||||
id
|
||||
name
|
||||
referenceCount
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
PREREQUISITES_QUERY = '''
|
||||
query fetchPrerequisites($offerParams: [OfferParams]) {
|
||||
Launcher {
|
||||
prerequisites(offerParams: $offerParams) {
|
||||
namespace
|
||||
offerId
|
||||
missingPrerequisiteItems
|
||||
satisfiesPrerequisites
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
PROMOTIONS_QUERY = '''
|
||||
query promotionsQuery(
|
||||
$namespace: String!
|
||||
$country: String!
|
||||
$locale: String!
|
||||
) {
|
||||
Catalog {
|
||||
catalogOffers(
|
||||
namespace: $namespace
|
||||
locale: $locale
|
||||
params: {
|
||||
category: "freegames"
|
||||
country: $country
|
||||
sortBy: "effectiveDate"
|
||||
sortDir: "asc"
|
||||
}
|
||||
) {
|
||||
elements {
|
||||
title
|
||||
description
|
||||
id
|
||||
namespace
|
||||
categories {
|
||||
path
|
||||
}
|
||||
linkedOfferNs
|
||||
linkedOfferId
|
||||
keyImages {
|
||||
type
|
||||
url
|
||||
}
|
||||
productSlug
|
||||
promotions {
|
||||
promotionalOffers {
|
||||
promotionalOffers {
|
||||
startDate
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
discountPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
upcomingPromotionalOffers {
|
||||
promotionalOffers {
|
||||
startDate
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
discountPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
OFFERS_QUERY = '''
|
||||
query catalogQuery(
|
||||
$productNamespace: String!
|
||||
$offerId: String!
|
||||
$locale: String
|
||||
$country: String!
|
||||
$includeSubItems: Boolean!
|
||||
) {
|
||||
Catalog {
|
||||
catalogOffer(namespace: $productNamespace, id: $offerId, locale: $locale) {
|
||||
title
|
||||
id
|
||||
namespace
|
||||
description
|
||||
effectiveDate
|
||||
expiryDate
|
||||
isCodeRedemptionOnly
|
||||
keyImages {
|
||||
type
|
||||
url
|
||||
}
|
||||
seller {
|
||||
id
|
||||
name
|
||||
}
|
||||
productSlug
|
||||
urlSlug
|
||||
url
|
||||
tags {
|
||||
id
|
||||
}
|
||||
items {
|
||||
id
|
||||
namespace
|
||||
}
|
||||
customAttributes {
|
||||
key
|
||||
value
|
||||
}
|
||||
categories {
|
||||
path
|
||||
}
|
||||
price(country: $country) {
|
||||
totalPrice {
|
||||
discountPrice
|
||||
originalPrice
|
||||
voucherDiscount
|
||||
discount
|
||||
currencyCode
|
||||
currencyInfo {
|
||||
decimals
|
||||
}
|
||||
fmtPrice(locale: $locale) {
|
||||
originalPrice
|
||||
discountPrice
|
||||
intermediatePrice
|
||||
}
|
||||
}
|
||||
lineOffers {
|
||||
appliedRules {
|
||||
id
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
offerSubItems(namespace: $productNamespace, id: $offerId)
|
||||
@include(if: $includeSubItems) {
|
||||
namespace
|
||||
id
|
||||
releaseInfo {
|
||||
appId
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
SEARCH_STORE_QUERY = '''
|
||||
query searchStoreQuery(
|
||||
$allowCountries: String
|
||||
$category: String
|
||||
$count: Int
|
||||
$country: String!
|
||||
$keywords: String
|
||||
$locale: String
|
||||
$namespace: String
|
||||
$itemNs: String
|
||||
$sortBy: String
|
||||
$sortDir: String
|
||||
$start: Int
|
||||
$tag: String
|
||||
$releaseDate: String
|
||||
$withPrice: Boolean = false
|
||||
$withPromotions: Boolean = false
|
||||
) {
|
||||
Catalog {
|
||||
searchStore(
|
||||
allowCountries: $allowCountries
|
||||
category: $category
|
||||
count: $count
|
||||
country: $country
|
||||
keywords: $keywords
|
||||
locale: $locale
|
||||
namespace: $namespace
|
||||
itemNs: $itemNs
|
||||
sortBy: $sortBy
|
||||
sortDir: $sortDir
|
||||
releaseDate: $releaseDate
|
||||
start: $start
|
||||
tag: $tag
|
||||
) {
|
||||
elements {
|
||||
title
|
||||
id
|
||||
namespace
|
||||
description
|
||||
effectiveDate
|
||||
keyImages {
|
||||
type
|
||||
url
|
||||
}
|
||||
seller {
|
||||
id
|
||||
name
|
||||
}
|
||||
productSlug
|
||||
urlSlug
|
||||
url
|
||||
tags {
|
||||
id
|
||||
}
|
||||
items {
|
||||
id
|
||||
namespace
|
||||
}
|
||||
customAttributes {
|
||||
key
|
||||
value
|
||||
}
|
||||
categories {
|
||||
path
|
||||
}
|
||||
price(country: $country) @include(if: $withPrice) {
|
||||
totalPrice {
|
||||
discountPrice
|
||||
originalPrice
|
||||
voucherDiscount
|
||||
discount
|
||||
currencyCode
|
||||
currencyInfo {
|
||||
decimals
|
||||
}
|
||||
fmtPrice(locale: $locale) {
|
||||
originalPrice
|
||||
discountPrice
|
||||
intermediatePrice
|
||||
}
|
||||
}
|
||||
lineOffers {
|
||||
appliedRules {
|
||||
id
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
promotions(category: $category) @include(if: $withPromotions) {
|
||||
promotionalOffers {
|
||||
promotionalOffers {
|
||||
startDate
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
discountPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
upcomingPromotionalOffers {
|
||||
promotionalOffers {
|
||||
startDate
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
discountPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
paging {
|
||||
count
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
29
rare/components/tabs/store/api/debug.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QTreeView, QDialog, QVBoxLayout
|
||||
|
||||
from rare.utils.json_formatter import QJsonModel
|
||||
|
||||
|
||||
class DebugView(QTreeView):
|
||||
def __init__(self, data, parent=None):
|
||||
super(DebugView, self).__init__(parent=parent)
|
||||
self.setColumnWidth(0, 300)
|
||||
self.setWordWrap(True)
|
||||
self.model = QJsonModel(self)
|
||||
self.setModel(self.model)
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
try:
|
||||
self.model.load(data)
|
||||
except Exception as e:
|
||||
pass
|
||||
self.resizeColumnToContents(0)
|
||||
|
||||
|
||||
class DebugDialog(QDialog):
|
||||
def __init__(self, data, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.resize(800, 600)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
view = DebugView(data, self)
|
||||
layout.addWidget(view)
|
15
rare/components/tabs/store/api/graphql/.graphqlconfig
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "EGS GraphQL Schema",
|
||||
"schemaPath": "schema.graphql",
|
||||
"extensions": {
|
||||
"endpoints": {
|
||||
"Default GraphQL Endpoint": {
|
||||
"url": "http://localhost:8080/graphql",
|
||||
"headers": {
|
||||
"user-agent": "JS GraphQL"
|
||||
},
|
||||
"introspect": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
rare/components/tabs/store/api/graphql/schema.graphql
Normal file
|
@ -0,0 +1,76 @@
|
|||
scalar Date
|
||||
|
||||
type Currency {
|
||||
decimals: Int
|
||||
symbol: String
|
||||
}
|
||||
|
||||
type FormattedPrice {
|
||||
originalPrice: String
|
||||
discountPrice: String
|
||||
intermediatePrice: String
|
||||
}
|
||||
|
||||
type TotalPrice {
|
||||
discountPrice: Int
|
||||
originalPrice: Int
|
||||
voucherDiscount: Int
|
||||
discount: Int
|
||||
currencyCode: String
|
||||
currencyInfo: Currency
|
||||
fmtPrice(locale: String): FormattedPrice
|
||||
}
|
||||
|
||||
type DiscountSetting {
|
||||
discountType: String
|
||||
}
|
||||
|
||||
type AppliedRules {
|
||||
id: ID
|
||||
endDate: Date
|
||||
discountSetting: DiscountSetting
|
||||
}
|
||||
|
||||
type LineOfferRes {
|
||||
appliedRules: [AppliedRules]
|
||||
}
|
||||
|
||||
type GetPriceRes {
|
||||
totalPrice: TotalPrice
|
||||
lineOffers: [LineOfferRes]
|
||||
}
|
||||
|
||||
type Image {
|
||||
type: String
|
||||
url: String
|
||||
alt: String
|
||||
}
|
||||
|
||||
type StorePageMapping {
|
||||
cmsSlug: String
|
||||
offerId: ID
|
||||
prePurchaseOfferId: ID
|
||||
}
|
||||
|
||||
type PageSandboxModel {
|
||||
pageSlug: String
|
||||
pageType: String
|
||||
productId: ID
|
||||
sandboxId: ID
|
||||
createdDate: Date
|
||||
updatedDate: Date
|
||||
deletedDate: Date
|
||||
mappings: [StorePageMapping]
|
||||
}
|
||||
|
||||
type CatalogNamespace {
|
||||
parent: ID
|
||||
displayName: String
|
||||
store: String
|
||||
mappings: [PageSandboxModel]
|
||||
}
|
||||
|
||||
type CatalogItem {
|
||||
id: ID
|
||||
namespace: ID
|
||||
}
|
0
rare/components/tabs/store/api/models/__init__.py
Normal file
164
rare/components/tabs/store/api/models/diesel.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Type, Optional
|
||||
|
||||
logger = logging.getLogger("DieselModels")
|
||||
|
||||
# lk: Typing overloads for unimplemented types
|
||||
DieselSocialLinks = Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class DieselSystemDetailItem:
|
||||
_type: Optional[str] = None
|
||||
minimum: Optional[str] = None
|
||||
recommended: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["DieselSystemDetailItem"], src: Dict[str, Any]) -> "DieselSystemDetailItem":
|
||||
d = src.copy()
|
||||
tmp = cls(
|
||||
_type=d.pop("_type", ""),
|
||||
minimum=d.pop("minimum", ""),
|
||||
recommended=d.pop("recommended", ""),
|
||||
title=d.pop("title", ""),
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class DieselSystemDetail:
|
||||
_type: Optional[str] = None
|
||||
details: Optional[List[DieselSystemDetailItem]] = None
|
||||
systemType: Optional[str] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["DieselSystemDetail"], src: Dict[str, Any]) -> "DieselSystemDetail":
|
||||
d = src.copy()
|
||||
_details = d.pop("details", [])
|
||||
details = [] if _details else None
|
||||
for item in _details:
|
||||
detail = DieselSystemDetailItem.from_dict(item)
|
||||
details.append(detail)
|
||||
tmp = cls(
|
||||
_type=d.pop("_type", ""),
|
||||
details=details,
|
||||
systemType=d.pop("systemType", ""),
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class DieselSystemDetails:
|
||||
_type: Optional[str] = None
|
||||
languages: Optional[List[str]] = None
|
||||
rating: Optional[Dict] = None
|
||||
systems: Optional[List[DieselSystemDetail]] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["DieselSystemDetails"], src: Dict[str, Any]) -> "DieselSystemDetails":
|
||||
d = src.copy()
|
||||
_systems = d.pop("systems", [])
|
||||
systems = [] if _systems else None
|
||||
for item in _systems:
|
||||
system = DieselSystemDetail.from_dict(item)
|
||||
systems.append(system)
|
||||
tmp = cls(
|
||||
_type=d.pop("_type", ""),
|
||||
languages=d.pop("languages", []),
|
||||
rating=d.pop("rating", {}),
|
||||
systems=systems,
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class DieselProductAbout:
|
||||
_type: Optional[str] = None
|
||||
desciption: Optional[str] = None
|
||||
developerAttribution: Optional[str] = None
|
||||
publisherAttribution: Optional[str] = None
|
||||
shortDescription: Optional[str] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["DieselProductAbout"], src: Dict[str, Any]) -> "DieselProductAbout":
|
||||
d = src.copy()
|
||||
tmp = cls(
|
||||
_type=d.pop("_type", ""),
|
||||
desciption=d.pop("description", ""),
|
||||
developerAttribution=d.pop("developerAttribution", ""),
|
||||
publisherAttribution=d.pop("publisherAttribution", ""),
|
||||
shortDescription=d.pop("shortDescription", ""),
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class DieselProductDetail:
|
||||
_type: Optional[str] = None
|
||||
about: Optional[DieselProductAbout] = None
|
||||
requirements: Optional[DieselSystemDetails] = None
|
||||
socialLinks: Optional[DieselSocialLinks] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["DieselProductDetail"], src: Dict[str, Any]) -> "DieselProductDetail":
|
||||
d = src.copy()
|
||||
about = DieselProductAbout.from_dict(x) if (x := d.pop("about"), {}) else None
|
||||
requirements = DieselSystemDetails.from_dict(x) if (x := d.pop("requirements", {})) else None
|
||||
tmp = cls(
|
||||
_type=d.pop("_type", ""),
|
||||
about=about,
|
||||
requirements=requirements,
|
||||
socialLinks=d.pop("socialLinks", {}),
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class DieselProduct:
|
||||
_id: Optional[str] = None
|
||||
_images_: Optional[List[str]] = None
|
||||
_locale: Optional[str] = None
|
||||
_slug: Optional[str] = None
|
||||
_title: Optional[str] = None
|
||||
_urlPattern: Optional[str] = None
|
||||
namespace: Optional[str] = None
|
||||
pages: Optional[List["DieselProduct"]] = None
|
||||
data: Optional[DieselProductDetail] = None
|
||||
productName: Optional[str] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["DieselProduct"], src: Dict[str, Any]) -> "DieselProduct":
|
||||
d = src.copy()
|
||||
_pages = d.pop("pages", [])
|
||||
pages = [] if _pages else None
|
||||
for item in _pages:
|
||||
page = DieselProduct.from_dict(item)
|
||||
pages.append(page)
|
||||
data = DieselProductDetail.from_dict(x) if (x := d.pop("data", {})) else None
|
||||
tmp = cls(
|
||||
_id=d.pop("_id", ""),
|
||||
_images_=d.pop("_images_", []),
|
||||
_locale=d.pop("_locale", ""),
|
||||
_slug=d.pop("_slug", ""),
|
||||
_title=d.pop("_title", ""),
|
||||
_urlPattern=d.pop("_urlPattern", ""),
|
||||
namespace=d.pop("namespace", ""),
|
||||
pages=pages,
|
||||
data=data,
|
||||
productName=d.pop("productName", ""),
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
80
rare/components/tabs/store/api/models/query.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchDateRange:
|
||||
start_date: datetime = datetime(year=1990, month=1, day=1, tzinfo=timezone.utc)
|
||||
end_date: datetime = datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||
|
||||
def __str__(self):
|
||||
def fmt_date(date: datetime) -> str:
|
||||
# lk: The formatting accepted by the GraphQL API is either '%Y-%m-%dT%H:%M:%S.000Z' or '%Y-%m-%d'
|
||||
return datetime.strftime(date, '%Y-%m-%dT%H:%M:%S.000Z')
|
||||
return f"[{fmt_date(self.start_date)},{fmt_date(self.end_date)}]"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchStoreQuery:
|
||||
country: str = "US"
|
||||
category: str = "games/edition/base|bundles/games|editors|software/edition/base"
|
||||
count: int = 30
|
||||
keywords: str = ""
|
||||
language: str = "en"
|
||||
namespace: str = ""
|
||||
with_mapping: bool = True
|
||||
item_ns: str = ""
|
||||
sort_by: str = "releaseDate"
|
||||
sort_dir: str = "DESC"
|
||||
start: int = 0
|
||||
tag: List[str] = ""
|
||||
release_date: SearchDateRange = field(default_factory=SearchDateRange)
|
||||
with_price: bool = True
|
||||
with_promotions: bool = True
|
||||
price_range: str = ""
|
||||
free_game: bool = None
|
||||
on_sale: bool = None
|
||||
effective_date: SearchDateRange = field(default_factory=SearchDateRange)
|
||||
|
||||
def __post_init__(self):
|
||||
self.locale = f"{self.language}-{self.country}"
|
||||
|
||||
def to_dict(self):
|
||||
payload = {
|
||||
"allowCountries": self.country,
|
||||
"category": self.category,
|
||||
"count": self.count,
|
||||
"country": self.country,
|
||||
"keywords": self.keywords,
|
||||
"locale": self.locale,
|
||||
"namespace": self.namespace,
|
||||
"withMapping": self.with_mapping,
|
||||
"itemNs": self.item_ns,
|
||||
"sortBy": self.sort_by,
|
||||
"sortDir": self.sort_dir,
|
||||
"start": self.start,
|
||||
"tag": self.tag,
|
||||
"releaseDate": str(self.release_date),
|
||||
"withPrice": self.with_price,
|
||||
"withPromotions": self.with_promotions,
|
||||
"priceRange": self.price_range,
|
||||
"freeGame": self.free_game,
|
||||
"onSale": self.on_sale,
|
||||
"effectiveDate": str(self.effective_date),
|
||||
}
|
||||
# payload.pop("withPromotions")
|
||||
payload.pop("onSale")
|
||||
if self.price_range == "free":
|
||||
payload["freeGame"] = True
|
||||
payload.pop("priceRange")
|
||||
elif self.price_range.startswith("<price>"):
|
||||
payload["priceRange"] = self.price_range.replace("<price>", "")
|
||||
if self.on_sale:
|
||||
payload["onSale"] = True
|
||||
|
||||
if self.price_range:
|
||||
payload["effectiveDate"] = self.effective_date
|
||||
else:
|
||||
payload.pop("priceRange")
|
||||
return payload
|
480
rare/components/tabs/store/api/models/response.py
Normal file
|
@ -0,0 +1,480 @@
|
|||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Type, Optional, Tuple
|
||||
|
||||
from .utils import parse_date
|
||||
|
||||
logger = logging.getLogger("StoreApiModels")
|
||||
|
||||
# lk: Typing overloads for unimplemented types
|
||||
DieselSocialLinks = Dict
|
||||
|
||||
CatalogNamespaceModel = Dict
|
||||
CategoryModel = Dict
|
||||
CustomAttributeModel = Dict
|
||||
ItemModel = Dict
|
||||
SellerModel = Dict
|
||||
PageSandboxModel = Dict
|
||||
TagModel = Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageUrlModel:
|
||||
type: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
tmp: Dict[str, Any] = {}
|
||||
tmp.update({})
|
||||
if self.type is not None:
|
||||
tmp["type"] = self.type
|
||||
if self.url is not None:
|
||||
tmp["url"] = self.url
|
||||
return tmp
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["ImageUrlModel"], src: Dict[str, Any]) -> "ImageUrlModel":
|
||||
d = src.copy()
|
||||
type = d.pop("type", None)
|
||||
url = d.pop("url", None)
|
||||
tmp = cls(type=type, url=url)
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class KeyImagesModel:
|
||||
key_images: Optional[List[ImageUrlModel]] = None
|
||||
tall_types = ("DieselStoreFrontTall", "OfferImageTall", "Thumbnail", "ProductLogo", "DieselGameBoxLogo")
|
||||
wide_types = ("DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo")
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.key_images[item]
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.key_images)
|
||||
|
||||
def to_list(self) -> List[Dict[str, Any]]:
|
||||
items: Optional[List[Dict[str, Any]]] = None
|
||||
if self.key_images is not None:
|
||||
items = []
|
||||
for image_url in self.key_images:
|
||||
item = image_url.to_dict()
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
@classmethod
|
||||
def from_list(cls: Type["KeyImagesModel"], src: List[Dict]):
|
||||
d = src.copy()
|
||||
key_images = []
|
||||
for item in d:
|
||||
image_url = ImageUrlModel.from_dict(item)
|
||||
key_images.append(image_url)
|
||||
tmp = cls(key_images)
|
||||
return tmp
|
||||
|
||||
def available_tall(self) -> List[ImageUrlModel]:
|
||||
tall_images = filter(lambda img: img.type in KeyImagesModel.tall_types, self.key_images)
|
||||
tall_images = sorted(tall_images, key=lambda x: KeyImagesModel.tall_types.index(x.type))
|
||||
return tall_images
|
||||
|
||||
def available_wide(self) -> List[ImageUrlModel]:
|
||||
wide_images = filter(lambda img: img.type in KeyImagesModel.wide_types, self.key_images)
|
||||
wide_images = sorted(wide_images, key=lambda x: KeyImagesModel.wide_types.index(x.type))
|
||||
return wide_images
|
||||
|
||||
def for_dimensions(self, w: int, h: int) -> ImageUrlModel:
|
||||
try:
|
||||
if w > h:
|
||||
model = self.available_wide()[0]
|
||||
else:
|
||||
model = self.available_tall()[0]
|
||||
_ = model.url
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.error(self.to_list())
|
||||
else:
|
||||
return model
|
||||
|
||||
|
||||
CurrencyModel = Dict
|
||||
FormattedPriceModel = Dict
|
||||
LineOffersModel = Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class TotalPriceModel:
|
||||
discountPrice: Optional[int] = None
|
||||
originalPrice: Optional[int] = None
|
||||
voucherDiscount: Optional[int] = None
|
||||
discount: Optional[int] = None
|
||||
currencyCode: Optional[str] = None
|
||||
currencyInfo: Optional[CurrencyModel] = None
|
||||
fmtPrice: Optional[FormattedPriceModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["TotalPriceModel"], src: Dict[str, Any]) -> "TotalPriceModel":
|
||||
d = src.copy()
|
||||
tmp = cls(
|
||||
discountPrice=d.pop("discountPrice", None),
|
||||
originalPrice=d.pop("originalPrice", None),
|
||||
voucherDiscount=d.pop("voucherDiscount", None),
|
||||
discount=d.pop("discount", None),
|
||||
currencyCode=d.pop("currencyCode", None),
|
||||
currencyInfo=d.pop("currrencyInfo", {}),
|
||||
fmtPrice=d.pop("fmtPrice", {}),
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetPriceResModel:
|
||||
totalPrice: Optional[TotalPriceModel] = None
|
||||
lineOffers: Optional[LineOffersModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["GetPriceResModel"], src: Dict[str, Any]) -> "GetPriceResModel":
|
||||
d = src.copy()
|
||||
total_price = TotalPriceModel.from_dict(x) if (x := d.pop("totalPrice", {})) else None
|
||||
tmp = cls(totalPrice=total_price, lineOffers=d.pop("lineOffers", {}))
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
DiscountSettingModel = Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromotionalOfferModel:
|
||||
startDate: Optional[datetime] = None
|
||||
endDate: Optional[datetime] = None
|
||||
discountSetting: Optional[DiscountSettingModel] = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["PromotionalOfferModel"], src: Dict[str, Any]) -> "PromotionalOfferModel":
|
||||
d = src.copy()
|
||||
start_date = parse_date(x) if (x := d.pop("startDate", "")) else None
|
||||
end_date = parse_date(x) if (x := d.pop("endDate", "")) else None
|
||||
tmp = cls(startDate=start_date, endDate=end_date, discountSetting=d.pop("discountSetting", {}))
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromotionalOffersModel:
|
||||
promotionalOffers: Optional[Tuple[PromotionalOfferModel]] = None
|
||||
|
||||
@classmethod
|
||||
def from_list(cls: Type["PromotionalOffersModel"], src: Dict[str, List]) -> "PromotionalOffersModel":
|
||||
d = src.copy()
|
||||
promotional_offers = (
|
||||
tuple([PromotionalOfferModel.from_dict(y) for y in x]) if (x := d.pop("promotionalOffers", [])) else None
|
||||
)
|
||||
tmp = cls(promotionalOffers=promotional_offers)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromotionsModel:
|
||||
promotionalOffers: Optional[Tuple[PromotionalOffersModel]] = None
|
||||
upcomingPromotionalOffers: Optional[Tuple[PromotionalOffersModel]] = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["PromotionsModel"], src: Dict[str, Any]) -> "PromotionsModel":
|
||||
d = src.copy()
|
||||
promotional_offers = (
|
||||
tuple([PromotionalOffersModel.from_list(y) for y in x]) if (x := d.pop("promotionalOffers", [])) else None
|
||||
)
|
||||
upcoming_promotional_offers = (
|
||||
tuple([PromotionalOffersModel.from_list(y) for y in x])
|
||||
if (x := d.pop("upcomingPromotionalOffers", []))
|
||||
else None
|
||||
)
|
||||
tmp = cls(promotionalOffers=promotional_offers, upcomingPromotionalOffers=upcoming_promotional_offers)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class CatalogOfferModel:
|
||||
catalogNs: Optional[CatalogNamespaceModel] = None
|
||||
categories: Optional[List[CategoryModel]] = None
|
||||
customAttributes: Optional[List[CustomAttributeModel]] = None
|
||||
description: Optional[str] = None
|
||||
effectiveDate: Optional[datetime] = None
|
||||
expiryDate: Optional[datetime] = None
|
||||
id: Optional[str] = None
|
||||
isCodeRedemptionOnly: Optional[bool] = None
|
||||
items: Optional[List[ItemModel]] = None
|
||||
keyImages: Optional[KeyImagesModel] = None
|
||||
namespace: Optional[str] = None
|
||||
offerMappings: Optional[List[PageSandboxModel]] = None
|
||||
offerType: Optional[str] = None
|
||||
price: Optional[GetPriceResModel] = None
|
||||
productSlug: Optional[str] = None
|
||||
promotions: Optional[PromotionsModel] = None
|
||||
seller: Optional[SellerModel] = None
|
||||
status: Optional[str] = None
|
||||
tags: Optional[List[TagModel]] = None
|
||||
title: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
urlSlug: Optional[str] = None
|
||||
viewableDate: Optional[datetime] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["CatalogOfferModel"], src: Dict[str, Any]) -> "CatalogOfferModel":
|
||||
d = src.copy()
|
||||
effective_date = parse_date(x) if (x := d.pop("effectiveDate", "")) else None
|
||||
expiry_date = parse_date(x) if (x := d.pop("expiryDate", "")) else None
|
||||
key_images = KeyImagesModel.from_list(d.pop("keyImages", []))
|
||||
price = GetPriceResModel.from_dict(x) if (x := d.pop("price", {})) else None
|
||||
promotions = PromotionsModel.from_dict(x) if (x := d.pop("promotions", {})) else None
|
||||
viewable_date = parse_date(x) if (x := d.pop("viewableDate", "")) else None
|
||||
tmp = cls(
|
||||
catalogNs=d.pop("catalogNs", {}),
|
||||
categories=d.pop("categories", []),
|
||||
customAttributes=d.pop("customAttributes", []),
|
||||
description=d.pop("description", ""),
|
||||
effectiveDate=effective_date,
|
||||
expiryDate=expiry_date,
|
||||
id=d.pop("id", ""),
|
||||
isCodeRedemptionOnly=d.pop("isCodeRedemptionOnly", None),
|
||||
items=d.pop("items", []),
|
||||
keyImages=key_images,
|
||||
namespace=d.pop("namespace", ""),
|
||||
offerMappings=d.pop("offerMappings", []),
|
||||
offerType=d.pop("offerType", ""),
|
||||
price=price,
|
||||
productSlug=d.pop("productSlug", ""),
|
||||
promotions=promotions,
|
||||
seller=d.pop("seller", {}),
|
||||
status=d.pop("status", ""),
|
||||
tags=d.pop("tags", []),
|
||||
title=d.pop("title", ""),
|
||||
url=d.pop("url", ""),
|
||||
urlSlug=d.pop("urlSlug", ""),
|
||||
viewableDate=viewable_date,
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class WishlistItemModel:
|
||||
created: Optional[datetime] = None
|
||||
id: Optional[str] = None
|
||||
namespace: Optional[str] = None
|
||||
isFirstTime: Optional[bool] = None
|
||||
offerId: Optional[str] = None
|
||||
order: Optional[Any] = None
|
||||
updated: Optional[datetime] = None
|
||||
offer: Optional[CatalogOfferModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["WishlistItemModel"], src: Dict[str, Any]) -> "WishlistItemModel":
|
||||
d = src.copy()
|
||||
created = parse_date(x) if (x := d.pop("created", "")) else None
|
||||
offer = CatalogOfferModel.from_dict(x) if (x := d.pop("offer", {})) else None
|
||||
updated = parse_date(x) if (x := d.pop("updated", "")) else None
|
||||
tmp = cls(
|
||||
created=created,
|
||||
id=d.pop("id", ""),
|
||||
namespace=d.pop("namespace", ""),
|
||||
isFirstTime=d.pop("isFirstTime", None),
|
||||
offerId=d.pop("offerId", ""),
|
||||
order=d.pop("order", ""),
|
||||
updated=updated,
|
||||
offer=offer,
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class PagingModel:
|
||||
count: Optional[int] = None
|
||||
total: Optional[int] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["PagingModel"], src: Dict[str, Any]) -> "PagingModel":
|
||||
d = src.copy()
|
||||
count = d.pop("count", None)
|
||||
total = d.pop("total", None)
|
||||
tmp = cls(count=count, total=total)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchStoreModel:
|
||||
elements: Optional[List[CatalogOfferModel]] = None
|
||||
paging: Optional[PagingModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["SearchStoreModel"], src: Dict[str, Any]) -> "SearchStoreModel":
|
||||
d = src.copy()
|
||||
_elements = d.pop("elements", [])
|
||||
elements = [] if _elements else None
|
||||
for item in _elements:
|
||||
elem = CatalogOfferModel.from_dict(item)
|
||||
elements.append(elem)
|
||||
paging = PagingModel.from_dict(x) if (x := d.pop("paging", {})) else None
|
||||
tmp = cls(elements=elements, paging=paging)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class CatalogModel:
|
||||
searchStore: Optional[SearchStoreModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["CatalogModel"], src: Dict[str, Any]) -> "CatalogModel":
|
||||
d = src.copy()
|
||||
search_store = SearchStoreModel.from_dict(x) if (x := d.pop("searchStore", {})) else None
|
||||
tmp = cls(searchStore=search_store)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class WishlistItemsModel:
|
||||
elements: Optional[List[WishlistItemModel]] = None
|
||||
paging: Optional[PagingModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["WishlistItemsModel"], src: Dict[str, Any]) -> "WishlistItemsModel":
|
||||
d = src.copy()
|
||||
_elements = d.pop("elements", [])
|
||||
elements = [] if _elements else None
|
||||
for item in _elements:
|
||||
elem = WishlistItemModel.from_dict(item)
|
||||
elements.append(elem)
|
||||
paging = PagingModel.from_dict(x) if (x := d.pop("paging", {})) else None
|
||||
tmp = cls(elements=elements, paging=paging)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class RemoveFromWishlistModel:
|
||||
success: Optional[bool] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["RemoveFromWishlistModel"], src: Dict[str, Any]) -> "RemoveFromWishlistModel":
|
||||
d = src.copy()
|
||||
tmp = cls(success=d.pop("success", None))
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddToWishlistModel:
|
||||
wishlistItem: Optional[WishlistItemModel] = None
|
||||
success: Optional[bool] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["AddToWishlistModel"], src: Dict[str, Any]) -> "AddToWishlistModel":
|
||||
d = src.copy()
|
||||
wishlist_item = WishlistItemModel.from_dict(x) if (x := d.pop("wishlistItem", {})) else None
|
||||
tmp = cls(wishlistItem=wishlist_item, success=d.pop("success", None))
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class WishlistModel:
|
||||
wishlistItems: Optional[WishlistItemsModel] = None
|
||||
removeFromWishlist: Optional[RemoveFromWishlistModel] = None
|
||||
addToWishlist: Optional[AddToWishlistModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["WishlistModel"], src: Dict[str, Any]) -> "WishlistModel":
|
||||
d = src.copy()
|
||||
wishlist_items = WishlistItemsModel.from_dict(x) if (x := d.pop("wishlistItems", {})) else None
|
||||
remove_from_wishlist = RemoveFromWishlistModel.from_dict(x) if (x := d.pop("removeFromWishlist", {})) else None
|
||||
add_to_wishlist = AddToWishlistModel.from_dict(x) if (x := d.pop("addToWishlist", {})) else None
|
||||
tmp = cls(
|
||||
wishlistItems=wishlist_items, removeFromWishlist=remove_from_wishlist, addToWishlist=add_to_wishlist
|
||||
)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
ProductModel = Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataModel:
|
||||
product: Optional[ProductModel] = None
|
||||
catalog: Optional[CatalogModel] = None
|
||||
wishlist: Optional[WishlistModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["DataModel"], src: Dict[str, Any]) -> "DataModel":
|
||||
d = src.copy()
|
||||
catalog = CatalogModel.from_dict(x) if (x := d.pop("Catalog", {})) else None
|
||||
wishlist = WishlistModel.from_dict(x) if (x := d.pop("Wishlist", {})) else None
|
||||
tmp = cls(product=d.pop("Product", {}), catalog=catalog, wishlist=wishlist)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorModel:
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["ErrorModel"], src: Dict[str, Any]) -> "ErrorModel":
|
||||
d = src.copy()
|
||||
tmp = cls()
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtensionsModel:
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["ExtensionsModel"], src: Dict[str, Any]) -> "ExtensionsModel":
|
||||
d = src.copy()
|
||||
tmp = cls()
|
||||
tmp.unmapped = d
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResponseModel:
|
||||
data: Optional[DataModel] = None
|
||||
errors: Optional[List[ErrorModel]] = None
|
||||
extensions: Optional[ExtensionsModel] = None
|
||||
unmapped: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["ResponseModel"], src: Dict[str, Any]) -> "ResponseModel":
|
||||
d = src.copy()
|
||||
data = DataModel.from_dict(x) if (x := d.pop("data", {})) else None
|
||||
_errors = d.pop("errors", [])
|
||||
errors = [] if _errors else None
|
||||
for item in _errors:
|
||||
error = ErrorModel.from_dict(item)
|
||||
errors.append(error)
|
||||
extensions = ExtensionsModel.from_dict(x) if (x := d.pop("extensions", {})) else None
|
||||
tmp = cls(data=data, errors=errors, extensions=extensions)
|
||||
tmp.unmapped = d
|
||||
return tmp
|
5
rare/components/tabs/store/api/models/utils.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def parse_date(date: str):
|
||||
return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc)
|
|
@ -44,74 +44,411 @@ class Constants(QObject):
|
|||
]
|
||||
|
||||
|
||||
game_query = (
|
||||
"query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, "
|
||||
"$keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, "
|
||||
"$sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean "
|
||||
"= false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, "
|
||||
"$effectiveDate: String) {\n Catalog {\n searchStore(\n allowCountries: $allowCountries\n "
|
||||
"category: $category\n count: $count\n country: $country\n keywords: $keywords\n "
|
||||
"locale: $locale\n namespace: $namespace\n itemNs: $itemNs\n sortBy: $sortBy\n "
|
||||
"sortDir: $sortDir\n releaseDate: $releaseDate\n start: $start\n tag: $tag\n "
|
||||
"priceRange: $priceRange\n freeGame: $freeGame\n onSale: $onSale\n effectiveDate: "
|
||||
"$effectiveDate\n ) {\n elements {\n title\n id\n namespace\n "
|
||||
"description\n effectiveDate\n keyImages {\n type\n url\n }\n "
|
||||
" currentPrice\n seller {\n id\n name\n }\n productSlug\n "
|
||||
" urlSlug\n url\n tags {\n id\n }\n items {\n id\n "
|
||||
" namespace\n }\n customAttributes {\n key\n value\n }\n "
|
||||
"categories {\n path\n }\n catalogNs @include(if: $withMapping) {\n "
|
||||
'mappings(pageType: "productHome") {\n pageSlug\n pageType\n }\n '
|
||||
"}\n offerMappings @include(if: $withMapping) {\n pageSlug\n pageType\n "
|
||||
"}\n price(country: $country) @include(if: $withPrice) {\n totalPrice {\n "
|
||||
"discountPrice\n originalPrice\n voucherDiscount\n discount\n "
|
||||
" currencyCode\n currencyInfo {\n decimals\n }\n fmtPrice("
|
||||
"locale: $locale) {\n originalPrice\n discountPrice\n "
|
||||
"intermediatePrice\n }\n }\n lineOffers {\n appliedRules {\n "
|
||||
" id\n endDate\n discountSetting {\n discountType\n "
|
||||
" }\n }\n }\n }\n promotions(category: $category) @include(if: "
|
||||
"$withPromotions) {\n promotionalOffers {\n promotionalOffers {\n "
|
||||
"startDate\n endDate\n discountSetting {\n discountType\n "
|
||||
" discountPercentage\n }\n }\n }\n "
|
||||
"upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n "
|
||||
"endDate\n discountSetting {\n discountType\n "
|
||||
"discountPercentage\n }\n }\n }\n }\n }\n paging {\n "
|
||||
" count\n total\n }\n }\n }\n}\n "
|
||||
)
|
||||
__Image = '''
|
||||
type
|
||||
url
|
||||
alt
|
||||
'''
|
||||
|
||||
search_query = (
|
||||
"query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, "
|
||||
"$keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, "
|
||||
"$sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean = "
|
||||
"false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, "
|
||||
"$effectiveDate: String) {\n Catalog {\n searchStore(\n allowCountries: $allowCountries\n "
|
||||
"category: $category\n count: $count\n country: $country\n keywords: $keywords\n locale: "
|
||||
"$locale\n namespace: $namespace\n itemNs: $itemNs\n sortBy: $sortBy\n sortDir: "
|
||||
"$sortDir\n releaseDate: $releaseDate\n start: $start\n tag: $tag\n priceRange: "
|
||||
"$priceRange\n freeGame: $freeGame\n onSale: $onSale\n effectiveDate: $effectiveDate\n ) {"
|
||||
"\n elements {\n title\n id\n namespace\n description\n "
|
||||
"effectiveDate\n keyImages {\n type\n url\n }\n currentPrice\n "
|
||||
"seller {\n id\n name\n }\n productSlug\n urlSlug\n url\n "
|
||||
" tags {\n id\n }\n items {\n id\n namespace\n }\n "
|
||||
"customAttributes {\n key\n value\n }\n categories {\n path\n "
|
||||
'}\n catalogNs @include(if: $withMapping) {\n mappings(pageType: "productHome") {\n '
|
||||
" pageSlug\n pageType\n }\n }\n offerMappings @include(if: $withMapping) "
|
||||
"{\n pageSlug\n pageType\n }\n price(country: $country) @include(if: "
|
||||
"$withPrice) {\n totalPrice {\n discountPrice\n originalPrice\n "
|
||||
"voucherDiscount\n discount\n currencyCode\n currencyInfo {\n "
|
||||
"decimals\n }\n fmtPrice(locale: $locale) {\n originalPrice\n "
|
||||
"discountPrice\n intermediatePrice\n }\n }\n lineOffers {\n "
|
||||
" appliedRules {\n id\n endDate\n discountSetting {\n "
|
||||
"discountType\n }\n }\n }\n }\n promotions(category: "
|
||||
"$category) @include(if: $withPromotions) {\n promotionalOffers {\n promotionalOffers {\n "
|
||||
" startDate\n endDate\n discountSetting {\n "
|
||||
"discountType\n discountPercentage\n }\n }\n }\n "
|
||||
"upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n "
|
||||
"endDate\n discountSetting {\n discountType\n discountPercentage\n "
|
||||
" }\n }\n }\n }\n }\n paging {\n count\n "
|
||||
"total\n }\n }\n }\n}\n "
|
||||
)
|
||||
__StorePageMapping = '''
|
||||
cmsSlug
|
||||
offerId
|
||||
prePurchaseOfferId
|
||||
'''
|
||||
|
||||
wishlist_query = '\n query wishlistQuery($country:String!, $locale:String) {\n Wishlist {\n wishlistItems {\n elements {\n id\n order\n created\n offerId\n updated\n namespace\n \n offer {\n productSlug\n urlSlug\n title\n id\n namespace\n offerType\n expiryDate\n status\n isCodeRedemptionOnly\n description\n effectiveDate\n keyImages {\n type\n url\n }\n seller {\n id\n name\n }\n productSlug\n urlSlug\n items {\n id\n namespace\n }\n customAttributes {\n key\n value\n }\n catalogNs {\n mappings(pageType: "productHome") {\n pageSlug\n pageType\n }\n }\n offerMappings {\n pageSlug\n pageType\n }\n categories {\n path\n }\n price(country: $country) {\n totalPrice {\n discountPrice\n originalPrice\n voucherDiscount\n discount\n fmtPrice(locale: $locale) {\n originalPrice\n discountPrice\n intermediatePrice\n }\n currencyCode\n currencyInfo {\n decimals\n symbol\n }\n }\n lineOffers {\n appliedRules {\n id\n endDate\n }\n }\n }\n }\n\n }\n }\n }\n }\n'
|
||||
add_to_wishlist_query = "\n mutation removeFromWishlistMutation($namespace: String!, $offerId: String!, $operation: RemoveOperation!) {\n Wishlist {\n removeFromWishlist(namespace: $namespace, offerId: $offerId, operation: $operation) {\n success\n }\n }\n }\n"
|
||||
remove_from_wishlist_query = "\n mutation removeFromWishlistMutation($namespace: String!, $offerId: String!, $operation: RemoveOperation!) {\n Wishlist {\n removeFromWishlist(namespace: $namespace, offerId: $offerId, operation: $operation) {\n success\n }\n }\n }\n"
|
||||
coupon_query = "\n query getCoupons($currencyCountry: String!, $identityId: String!, $locale: String) {\n CodeRedemption {\n coupons(currencyCountry: $currencyCountry, identityId: $identityId, includeSalesEventInfo: true) {\n code\n codeStatus\n codeType\n consumptionMetadata {\n amountDisplay {\n amount\n currency\n placement\n symbol\n }\n minSalesPriceDisplay {\n amount\n currency\n placement\n symbol\n }\n }\n endDate\n namespace\n salesEvent(locale: $locale) {\n eventName\n eventSlug\n voucherImages {\n type\n url\n }\n voucherLink\n }\n startDate\n }\n }\n }\n"
|
||||
__PageSandboxModel = '''
|
||||
pageSlug
|
||||
pageType
|
||||
productId
|
||||
sandboxId
|
||||
createdDate
|
||||
updatedDate
|
||||
deletedDate
|
||||
mappings {
|
||||
%s
|
||||
}
|
||||
''' % (__StorePageMapping)
|
||||
|
||||
__CatalogNamespace = '''
|
||||
parent
|
||||
displayName
|
||||
store
|
||||
home: mappings(pageType: "productHome") {
|
||||
%s
|
||||
}
|
||||
addons: mappings(pageType: "addon--cms-hybrid") {
|
||||
%s
|
||||
}
|
||||
offers: mappings(pageType: "offer") {
|
||||
%s
|
||||
}
|
||||
''' % (__PageSandboxModel, __PageSandboxModel, __PageSandboxModel)
|
||||
|
||||
__CatalogItem = '''
|
||||
id
|
||||
namespace
|
||||
'''
|
||||
|
||||
__GetPriceRes = '''
|
||||
totalPrice {
|
||||
discountPrice
|
||||
originalPrice
|
||||
voucherDiscount
|
||||
discount
|
||||
currencyCode
|
||||
currencyInfo {
|
||||
decimals
|
||||
symbol
|
||||
}
|
||||
fmtPrice(locale: $locale) {
|
||||
originalPrice
|
||||
discountPrice
|
||||
intermediatePrice
|
||||
}
|
||||
}
|
||||
lineOffers {
|
||||
appliedRules {
|
||||
id
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
__Promotions = '''
|
||||
promotionalOffers {
|
||||
promotionalOffers {
|
||||
startDate
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
discountPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
upcomingPromotionalOffers {
|
||||
promotionalOffers {
|
||||
startDate
|
||||
endDate
|
||||
discountSetting {
|
||||
discountType
|
||||
discountPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
__CatalogOffer = '''
|
||||
title
|
||||
id
|
||||
namespace
|
||||
offerType
|
||||
expiryDate
|
||||
status
|
||||
isCodeRedemptionOnly
|
||||
description
|
||||
effectiveDate
|
||||
keyImages {
|
||||
%(image)s
|
||||
}
|
||||
currentPrice
|
||||
seller {
|
||||
id
|
||||
name
|
||||
}
|
||||
productSlug
|
||||
urlSlug
|
||||
url
|
||||
tags {
|
||||
id
|
||||
name
|
||||
groupName
|
||||
}
|
||||
items {
|
||||
%(catalog_item)s
|
||||
}
|
||||
customAttributes {
|
||||
key
|
||||
value
|
||||
}
|
||||
categories {
|
||||
path
|
||||
}
|
||||
catalogNs @include(if: $withMapping) {
|
||||
%(catalog_namespace)s
|
||||
}
|
||||
offerMappings @include(if: $withMapping) {
|
||||
%(page_sandbox_model)s
|
||||
}
|
||||
price(country: $country) @include(if: $withPrice) {
|
||||
%(get_price_res)s
|
||||
}
|
||||
promotions(category: $category) @include(if: $withPromotions) {
|
||||
%(promotions)s
|
||||
}
|
||||
''' % {
|
||||
"image": __Image,
|
||||
"catalog_item": __CatalogItem,
|
||||
"catalog_namespace": __CatalogNamespace,
|
||||
"page_sandbox_model": __PageSandboxModel,
|
||||
"get_price_res": __GetPriceRes,
|
||||
"promotions": __Promotions,
|
||||
}
|
||||
|
||||
__Pagination = '''
|
||||
count
|
||||
total
|
||||
'''
|
||||
|
||||
SEARCH_STORE_QUERY = '''
|
||||
query searchStoreQuery(
|
||||
$allowCountries: String
|
||||
$category: String
|
||||
$count: Int
|
||||
$country: String!
|
||||
$keywords: String
|
||||
$locale: String
|
||||
$namespace: String
|
||||
$withMapping: Boolean = false
|
||||
$itemNs: String
|
||||
$sortBy: String
|
||||
$sortDir: String
|
||||
$start: Int
|
||||
$tag: String
|
||||
$releaseDate: String
|
||||
$withPrice: Boolean = false
|
||||
$withPromotions: Boolean = false
|
||||
$priceRange: String
|
||||
$freeGame: Boolean
|
||||
$onSale: Boolean
|
||||
$effectiveDate: String
|
||||
) {
|
||||
Catalog {
|
||||
searchStore(
|
||||
allowCountries: $allowCountries
|
||||
category: $category
|
||||
count: $count
|
||||
country: $country
|
||||
keywords: $keywords
|
||||
locale: $locale
|
||||
namespace: $namespace
|
||||
itemNs: $itemNs
|
||||
sortBy: $sortBy
|
||||
sortDir: $sortDir
|
||||
releaseDate: $releaseDate
|
||||
start: $start
|
||||
tag: $tag
|
||||
priceRange: $priceRange
|
||||
freeGame: $freeGame
|
||||
onSale: $onSale
|
||||
effectiveDate: $effectiveDate
|
||||
) {
|
||||
elements {
|
||||
%s
|
||||
}
|
||||
paging {
|
||||
%s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
''' % (__CatalogOffer, __Pagination)
|
||||
|
||||
__WISHLIST_ITEM = '''
|
||||
id
|
||||
order
|
||||
created
|
||||
offerId
|
||||
updated
|
||||
namespace
|
||||
isFirstTime
|
||||
offer(locale: $locale) {
|
||||
%s
|
||||
}
|
||||
''' % __CatalogOffer
|
||||
|
||||
WISHLIST_QUERY = '''
|
||||
query wishlistQuery(
|
||||
$country: String!
|
||||
$locale: String
|
||||
$category: String
|
||||
$withMapping: Boolean = false
|
||||
$withPrice: Boolean = false
|
||||
$withPromotions: Boolean = false
|
||||
) {
|
||||
Wishlist {
|
||||
wishlistItems {
|
||||
elements {
|
||||
%s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
''' % __WISHLIST_ITEM
|
||||
|
||||
WISHLIST_ADD_QUERY = '''
|
||||
mutation addWishlistMutation(
|
||||
$namespace: String!
|
||||
$offerId: String!
|
||||
$country: String!
|
||||
$locale: String
|
||||
$category: String
|
||||
$withMapping: Boolean = false
|
||||
$withPrice: Boolean = false
|
||||
$withPromotions: Boolean = false
|
||||
) {
|
||||
Wishlist {
|
||||
addToWishlist(
|
||||
namespace: $namespace
|
||||
offerId: $offerId
|
||||
) {
|
||||
wishlistItem {
|
||||
%s
|
||||
}
|
||||
success
|
||||
}
|
||||
}
|
||||
}
|
||||
''' % __WISHLIST_ITEM
|
||||
|
||||
WISHLIST_REMOVE_QUERY = '''
|
||||
mutation removeFromWishlistMutation(
|
||||
$namespace: String!
|
||||
$offerId: String!
|
||||
$operation: RemoveOperation!
|
||||
) {
|
||||
Wishlist {
|
||||
removeFromWishlist(
|
||||
namespace: $namespace
|
||||
offerId: $offerId
|
||||
operation: $operation
|
||||
) {
|
||||
success
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
COUPONS_QUERY = '''
|
||||
query getCoupons(
|
||||
$currencyCountry: String!
|
||||
$identityId: String!
|
||||
$locale: String
|
||||
) {
|
||||
CodeRedemption {
|
||||
coupons(
|
||||
currencyCountry: $currencyCountry
|
||||
identityId: $identityId
|
||||
includeSalesEventInfo: true
|
||||
) {
|
||||
code
|
||||
codeStatus
|
||||
codeType
|
||||
consumptionMetadata {
|
||||
amountDisplay {
|
||||
amount
|
||||
currency
|
||||
placement
|
||||
symbol
|
||||
}
|
||||
minSalesPriceDisplay {
|
||||
amount
|
||||
currency
|
||||
placement
|
||||
symbol
|
||||
}
|
||||
}
|
||||
endDate
|
||||
namespace
|
||||
salesEvent(locale: $locale) {
|
||||
eventName
|
||||
eventSlug
|
||||
voucherImages {
|
||||
type
|
||||
url
|
||||
}
|
||||
voucherLink
|
||||
}
|
||||
startDate
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
STORE_CONFIG_QUERY = '''
|
||||
query getStoreConfig(
|
||||
$includeCriticReviews: Boolean = false
|
||||
$locale: String!
|
||||
$sandboxId: String!
|
||||
$templateId: String
|
||||
) {
|
||||
Product {
|
||||
sandbox(sandboxId: $sandboxId) {
|
||||
configuration(locale: $locale, templateId: $templateId) {
|
||||
... on StoreConfiguration {
|
||||
configs {
|
||||
shortDescription
|
||||
criticReviews @include(if: $includeCriticReviews) {
|
||||
openCritic
|
||||
}
|
||||
socialLinks {
|
||||
platform
|
||||
url
|
||||
}
|
||||
supportedAudio
|
||||
supportedText
|
||||
tags(locale: $locale) {
|
||||
id
|
||||
name
|
||||
groupName
|
||||
}
|
||||
technicalRequirements {
|
||||
macos {
|
||||
minimum
|
||||
recommended
|
||||
title
|
||||
}
|
||||
windows {
|
||||
minimum
|
||||
recommended
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on HomeConfiguration {
|
||||
configs {
|
||||
keyImages {
|
||||
... on KeyImage {
|
||||
type
|
||||
url
|
||||
alt
|
||||
}
|
||||
}
|
||||
longDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def compress_query(query: str) -> str:
|
||||
return query.replace(" ", "").replace("\n", " ")
|
||||
|
||||
|
||||
game_query = compress_query(SEARCH_STORE_QUERY)
|
||||
search_query = compress_query(SEARCH_STORE_QUERY)
|
||||
wishlist_query = compress_query(WISHLIST_QUERY)
|
||||
wishlist_add_query = compress_query(WISHLIST_ADD_QUERY)
|
||||
wishlist_remove_query = compress_query(WISHLIST_REMOVE_QUERY)
|
||||
coupons_query = compress_query(COUPONS_QUERY)
|
||||
store_config_query = compress_query(STORE_CONFIG_QUERY)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(SEARCH_STORE_QUERY)
|
||||
|
|
|
@ -1,271 +0,0 @@
|
|||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
from PyQt5.QtGui import QPixmap, QFont, QDesktopServices
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QHBoxLayout,
|
||||
QSpacerItem,
|
||||
QGroupBox,
|
||||
QTabWidget,
|
||||
QGridLayout,
|
||||
)
|
||||
|
||||
from rare.components.tabs.store.shop_models import ShopGame
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info
|
||||
from rare.utils.extra_widgets import ImageLabel
|
||||
from rare.utils.misc import icon
|
||||
from rare.widgets.loading_widget import LoadingWidget
|
||||
|
||||
logger = logging.getLogger("ShopInfo")
|
||||
|
||||
|
||||
class ShopGameInfo(QWidget, Ui_shop_info):
|
||||
game: ShopGame
|
||||
data: dict
|
||||
|
||||
# TODO Design
|
||||
def __init__(self, installed_titles: list, api_core):
|
||||
super(ShopGameInfo, self).__init__()
|
||||
self.setupUi(self)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.api_core = api_core
|
||||
self.installed = installed_titles
|
||||
self.open_store_button.clicked.connect(self.button_clicked)
|
||||
self.image = ImageLabel()
|
||||
self.image_stack.addWidget(self.image)
|
||||
self.image_stack.addWidget(LoadingWidget())
|
||||
warn_label = QLabel()
|
||||
warn_label.setPixmap(
|
||||
icon("fa.warning").pixmap(160, 160).scaled(240, 320, Qt.IgnoreAspectRatio)
|
||||
)
|
||||
self.image_stack.addWidget(warn_label)
|
||||
|
||||
self.wishlist_button.clicked.connect(self.add_to_wishlist)
|
||||
self.in_wishlist = False
|
||||
self.wishlist = []
|
||||
|
||||
def handle_wishlist_update(self, data):
|
||||
if data and data[0] == "error":
|
||||
return
|
||||
self.wishlist = [i["offer"]["title"] for i in data]
|
||||
if self.title_str in self.wishlist:
|
||||
self.in_wishlist = True
|
||||
self.wishlist_button.setVisible(True)
|
||||
self.wishlist_button.setText(self.tr("Remove from Wishlist"))
|
||||
else:
|
||||
self.in_wishlist = False
|
||||
self.wishlist_button.setVisible(False)
|
||||
|
||||
def update_game(self, data: dict):
|
||||
self.image_stack.setCurrentIndex(1)
|
||||
self.title.setText(data["title"])
|
||||
self.title_str = data["title"]
|
||||
self.api_core.get_wishlist(self.handle_wishlist_update)
|
||||
for i in reversed(range(self.req_group_box.layout().count())):
|
||||
self.req_group_box.layout().itemAt(i).widget().deleteLater()
|
||||
slug = data["productSlug"]
|
||||
if not slug:
|
||||
for mapping in data["offerMappings"]:
|
||||
if mapping["pageType"] == "productHome":
|
||||
slug = mapping["pageSlug"]
|
||||
break
|
||||
else:
|
||||
logger.error("Could not get page information")
|
||||
slug = ""
|
||||
if "/home" in slug:
|
||||
slug = slug.replace("/home", "")
|
||||
self.slug = slug
|
||||
|
||||
if data["namespace"] in self.installed:
|
||||
self.open_store_button.setText(self.tr("Show Game on Epic Page"))
|
||||
self.owned_label.setVisible(True)
|
||||
else:
|
||||
self.open_store_button.setText(self.tr("Buy Game in Epic Games Store"))
|
||||
self.owned_label.setVisible(False)
|
||||
|
||||
for i in range(self.req_group_box.layout().count()):
|
||||
self.req_group_box.layout().itemAt(i).widget().deleteLater()
|
||||
|
||||
self.price.setText(self.tr("Loading"))
|
||||
self.wishlist_button.setVisible(False)
|
||||
# self.title.setText(self.tr("Loading"))
|
||||
self.image.setPixmap(QPixmap())
|
||||
self.data = data
|
||||
is_bundle = False
|
||||
for i in data["categories"]:
|
||||
if "bundles" in i.get("path", ""):
|
||||
is_bundle = True
|
||||
|
||||
# init API request
|
||||
if slug:
|
||||
self.api_core.get_game(slug, is_bundle, self.data_received)
|
||||
else:
|
||||
self.data_received({})
|
||||
|
||||
def add_to_wishlist(self):
|
||||
if not self.in_wishlist:
|
||||
return
|
||||
# self.api_core.add_to_wishlist(self.game.namespace, self.game.offer_id,
|
||||
# lambda success: self.wishlist_button.setText(self.tr("Remove from wishlist"))
|
||||
# if success else self.wishlist_button.setText("Something goes wrong"))
|
||||
else:
|
||||
self.api_core.remove_from_wishlist(
|
||||
self.game.namespace,
|
||||
self.game.offer_id,
|
||||
lambda success: self.wishlist_button.setVisible(False)
|
||||
if success
|
||||
else self.wishlist_button.setText("Something goes wrong"),
|
||||
)
|
||||
|
||||
def data_received(self, game):
|
||||
try:
|
||||
self.game = ShopGame.from_json(game, self.data)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
self.price.setText("Error")
|
||||
self.req_group_box.setVisible(False)
|
||||
for img in self.data.get("keyImages"):
|
||||
if img["type"] in [
|
||||
"DieselStoreFrontWide",
|
||||
"OfferImageTall",
|
||||
"VaultClosed",
|
||||
"ProductLogo",
|
||||
]:
|
||||
self.image.update_image(img["url"], self.title_str, size=(240, 320))
|
||||
self.image_stack.setCurrentIndex(0)
|
||||
break
|
||||
else:
|
||||
self.image_stack.setCurrentIndex(2)
|
||||
self.price.setText("")
|
||||
self.discount_price.setText("")
|
||||
self.social_link_gb.setVisible(False)
|
||||
self.tags.setText("")
|
||||
self.dev.setText(self.data.get("seller", {}).get("name", ""))
|
||||
return
|
||||
self.title.setText(self.game.title)
|
||||
|
||||
self.price.setFont(QFont())
|
||||
if self.game.price == "0" or self.game.price == 0:
|
||||
self.price.setText(self.tr("Free"))
|
||||
else:
|
||||
self.price.setText(self.game.price)
|
||||
if self.game.price != self.game.discount_price:
|
||||
font = QFont()
|
||||
font.setStrikeOut(True)
|
||||
self.price.setFont(font)
|
||||
self.discount_price.setText(
|
||||
self.game.discount_price
|
||||
if self.game.discount_price != "0"
|
||||
else self.tr("Free")
|
||||
)
|
||||
self.discount_price.setVisible(True)
|
||||
else:
|
||||
self.discount_price.setVisible(False)
|
||||
|
||||
bold_font = QFont()
|
||||
bold_font.setBold(True)
|
||||
|
||||
if self.game.reqs:
|
||||
req_tabs = QTabWidget()
|
||||
for system in self.game.reqs:
|
||||
min_label = QLabel(self.tr("Minimum"))
|
||||
min_label.setFont(bold_font)
|
||||
rec_label = QLabel(self.tr("Recommend"))
|
||||
rec_label.setFont(bold_font)
|
||||
req_widget = QWidget()
|
||||
req_widget.setLayout(QGridLayout())
|
||||
req_widget.layout().addWidget(min_label, 0, 1)
|
||||
req_widget.layout().addWidget(rec_label, 0, 2)
|
||||
for i, (key, value) in enumerate(
|
||||
self.game.reqs.get(system, {}).items()
|
||||
):
|
||||
req_widget.layout().addWidget(QLabel(key), i + 1, 0)
|
||||
min_label = QLabel(value[0])
|
||||
min_label.setWordWrap(True)
|
||||
req_widget.layout().addWidget(min_label, i + 1, 1)
|
||||
rec_label = QLabel(value[1])
|
||||
rec_label.setWordWrap(True)
|
||||
req_widget.layout().addWidget(rec_label, i + 1, 2)
|
||||
req_tabs.addTab(req_widget, system)
|
||||
self.req_group_box.layout().addWidget(req_tabs)
|
||||
else:
|
||||
self.req_group_box.layout().addWidget(
|
||||
QLabel(self.tr("Could not get requirements"))
|
||||
)
|
||||
self.req_group_box.setVisible(True)
|
||||
if self.game.image_urls.front_tall:
|
||||
img_url = self.game.image_urls.front_tall
|
||||
elif self.game.image_urls.offer_image_tall:
|
||||
img_url = self.game.image_urls.offer_image_tall
|
||||
elif self.game.image_urls.product_logo:
|
||||
img_url = self.game.image_urls.product_logo
|
||||
else:
|
||||
img_url = ""
|
||||
self.image.update_image(img_url, self.game.title, (240, 320))
|
||||
|
||||
self.image_stack.setCurrentIndex(0)
|
||||
try:
|
||||
if isinstance(self.game.developer, list):
|
||||
self.dev.setText(", ".join(self.game.developer))
|
||||
else:
|
||||
self.dev.setText(self.game.developer)
|
||||
except KeyError:
|
||||
pass
|
||||
self.tags.setText(", ".join(self.game.tags))
|
||||
|
||||
# clear Layout
|
||||
for widget in (
|
||||
self.social_link_gb.layout().itemAt(i)
|
||||
for i in range(self.social_link_gb.layout().count())
|
||||
):
|
||||
if not isinstance(widget, QSpacerItem):
|
||||
widget.widget().deleteLater()
|
||||
self.social_link_gb.deleteLater()
|
||||
self.social_link_gb = QGroupBox(self.tr("Social Links"))
|
||||
self.social_link_gb.setLayout(QHBoxLayout())
|
||||
|
||||
self.layout().insertWidget(3, self.social_link_gb)
|
||||
|
||||
self.social_link_gb.layout().addStretch(1)
|
||||
link_count = 0
|
||||
for name, url in self.game.links:
|
||||
|
||||
if name.lower() == "homepage":
|
||||
icn = icon("mdi.web", "fa.search", scale_factor=1.5)
|
||||
else:
|
||||
try:
|
||||
icn = icon(f"mdi.{name.lower()}", f"fa.{name.lower()}", scale_factor=1.5)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
continue
|
||||
|
||||
button = SocialButton(icn, url)
|
||||
self.social_link_gb.layout().addWidget(button)
|
||||
link_count += 1
|
||||
self.social_link_gb.layout().addStretch(1)
|
||||
|
||||
if link_count == 0:
|
||||
self.social_link_gb.setVisible(False)
|
||||
else:
|
||||
self.social_link_gb.setVisible(True)
|
||||
self.social_link_gb.layout().addStretch(1)
|
||||
|
||||
def add_wishlist_items(self, wishlist):
|
||||
wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"]
|
||||
for game in wishlist:
|
||||
self.wishlist.append(game["offer"]["title"])
|
||||
|
||||
def button_clicked(self):
|
||||
QDesktopServices.openUrl(QUrl(f"https://www.epicgames.com/store/{self.core.language_code}/p/{self.slug}"))
|
||||
|
||||
|
||||
class SocialButton(QPushButton):
|
||||
def __init__(self, icn, url):
|
||||
super(SocialButton, self).__init__(icn, "")
|
||||
self.url = url
|
||||
self.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url)))
|
||||
self.setToolTip(url)
|
|
@ -1,142 +0,0 @@
|
|||
import logging
|
||||
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout
|
||||
|
||||
from rare.components.tabs.store.shop_models import ImageUrlModel
|
||||
from rare.ui.components.tabs.store.wishlist_widget import Ui_WishlistWidget
|
||||
from rare.utils.extra_widgets import ImageLabel
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = logging.getLogger("GameWidgets")
|
||||
|
||||
|
||||
class GameWidget(QWidget):
|
||||
show_info = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, path, json_info=None, width=300):
|
||||
super(GameWidget, self).__init__()
|
||||
self.manager = QNetworkAccessManager()
|
||||
self.width = width
|
||||
self.path = path
|
||||
if json_info:
|
||||
self.init_ui(json_info)
|
||||
|
||||
def init_ui(self, json_info):
|
||||
self.layout = QVBoxLayout()
|
||||
self.image = ImageLabel()
|
||||
self.layout.addWidget(self.image)
|
||||
mini_layout = QHBoxLayout()
|
||||
self.layout.addLayout(mini_layout)
|
||||
|
||||
if not json_info:
|
||||
self.layout.addWidget(QLabel("An error occurred"))
|
||||
self.setLayout(self.layout)
|
||||
return
|
||||
|
||||
self.title_label = QLabel(json_info.get("title"))
|
||||
self.title_label.setWordWrap(True)
|
||||
mini_layout.addWidget(self.title_label)
|
||||
mini_layout.addStretch(1)
|
||||
|
||||
price = json_info["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
|
||||
discount_price = json_info["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
|
||||
price_label = QLabel(price)
|
||||
if price != discount_price:
|
||||
font = QFont()
|
||||
font.setStrikeOut(True)
|
||||
price_label.setFont(font)
|
||||
mini_layout.addWidget(
|
||||
QLabel(discount_price if discount_price != "0" else self.tr("Free"))
|
||||
)
|
||||
mini_layout.addWidget(price_label)
|
||||
else:
|
||||
if price == "0":
|
||||
price_label.setText(self.tr("Free"))
|
||||
mini_layout.addWidget(price_label)
|
||||
|
||||
for c in r'<>?":|\/*':
|
||||
json_info["title"] = json_info["title"].replace(c, "")
|
||||
|
||||
self.json_info = json_info
|
||||
self.slug = json_info["productSlug"]
|
||||
|
||||
self.title = json_info["title"]
|
||||
for img in json_info["keyImages"]:
|
||||
if img["type"] in [
|
||||
"DieselStoreFrontWide",
|
||||
"OfferImageWide",
|
||||
"VaultClosed",
|
||||
"ProductLogo",
|
||||
]:
|
||||
if img["type"] == "VaultClosed" and self.title != "Mystery Game":
|
||||
continue
|
||||
self.image.update_image(
|
||||
img["url"],
|
||||
json_info["title"],
|
||||
(self.width, int(self.width * 9 / 16)),
|
||||
)
|
||||
break
|
||||
else:
|
||||
logger.info(", ".join([img["type"] for img in json_info["keyImages"]]))
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.setFixedSize(self.width + 10, self.width * 9 // 16 + 50)
|
||||
|
||||
def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None:
|
||||
self.show_info.emit(self.json_info)
|
||||
|
||||
|
||||
class WishlistWidget(QWidget, Ui_WishlistWidget):
|
||||
open_game = pyqtSignal(dict)
|
||||
delete_from_wishlist = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, game: dict):
|
||||
super(WishlistWidget, self).__init__()
|
||||
self.setupUi(self)
|
||||
self.game = game
|
||||
self.title_label.setText(game.get("title"))
|
||||
for attr in game["customAttributes"]:
|
||||
if attr["key"] == "developerName":
|
||||
self.developer.setText(attr["value"])
|
||||
break
|
||||
else:
|
||||
self.developer.setText(game["seller"]["name"])
|
||||
original_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
|
||||
discount_price = game["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
|
||||
|
||||
self.price.setText(original_price if original_price != "0" else self.tr("Free"))
|
||||
# if discount
|
||||
if original_price != discount_price:
|
||||
self.discount = True
|
||||
font = QFont()
|
||||
font.setStrikeOut(True)
|
||||
self.price.setFont(font)
|
||||
self.discount_price.setText(discount_price)
|
||||
else:
|
||||
self.discount = False
|
||||
self.discount_price.setVisible(False)
|
||||
|
||||
self.image = ImageLabel()
|
||||
self.layout().insertWidget(0, self.image)
|
||||
image_model = ImageUrlModel.from_json(game["keyImages"])
|
||||
url = image_model.front_wide
|
||||
if not url:
|
||||
url = image_model.offer_image_wide
|
||||
self.image.update_image(url, game.get("title"), (240, 135))
|
||||
self.delete_button.setIcon(icon("mdi.delete", color="white"))
|
||||
self.delete_button.clicked.connect(
|
||||
lambda: self.delete_from_wishlist.emit(self.game)
|
||||
)
|
||||
|
||||
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
|
||||
# left button
|
||||
if e.button() == 1:
|
||||
self.open_game.emit(self.game)
|
||||
# right
|
||||
elif e.button() == 2:
|
||||
pass # self.showMenu(e)
|
239
rare/components/tabs/store/landing.py
Normal file
|
@ -0,0 +1,239 @@
|
|||
import datetime
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QObject, QEvent
|
||||
from PyQt5.QtGui import QShowEvent, QHideEvent, QResizeEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QWidget,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QSpacerItem,
|
||||
QScrollArea,
|
||||
QFrame,
|
||||
)
|
||||
|
||||
from rare.components.tabs.store.api.models.response import CatalogOfferModel, WishlistItemModel
|
||||
from rare.widgets.flow_layout import FlowLayout
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from rare.widgets.sliding_stack import SlidingStackedWidget
|
||||
from .store_api import StoreAPI
|
||||
from .widgets.details import StoreDetailsWidget
|
||||
from .widgets.groups import StoreGroup
|
||||
from .widgets.items import StoreItemWidget
|
||||
|
||||
logger = logging.getLogger("StoreLanding")
|
||||
|
||||
|
||||
class LandingPage(SlidingStackedWidget, SideTabContents):
|
||||
|
||||
def __init__(self, store_api: StoreAPI, parent=None):
|
||||
super(LandingPage, self).__init__(parent=parent)
|
||||
self.implements_scrollarea = True
|
||||
|
||||
self.landing_widget = LandingWidget(store_api, parent=self)
|
||||
self.landing_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.landing_widget.set_title.connect(self.set_title)
|
||||
self.landing_widget.show_details.connect(self.show_details)
|
||||
|
||||
self.landing_scroll = QScrollArea(self)
|
||||
self.landing_scroll.setWidgetResizable(True)
|
||||
self.landing_scroll.setFrameStyle(QFrame.NoFrame | QFrame.Plain)
|
||||
self.landing_scroll.setWidget(self.landing_widget)
|
||||
self.landing_scroll.widget().setAutoFillBackground(False)
|
||||
self.landing_scroll.viewport().setAutoFillBackground(False)
|
||||
|
||||
self.details_widget = StoreDetailsWidget([], store_api, parent=self)
|
||||
self.details_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.details_widget.set_title.connect(self.set_title)
|
||||
self.details_widget.back_clicked.connect(self.show_main)
|
||||
|
||||
self.setDirection(Qt.Horizontal)
|
||||
self.addWidget(self.landing_scroll)
|
||||
self.addWidget(self.details_widget)
|
||||
|
||||
@pyqtSlot()
|
||||
def show_main(self):
|
||||
self.slideInWidget(self.landing_scroll)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def show_details(self, game: CatalogOfferModel):
|
||||
self.details_widget.update_game(game)
|
||||
self.slideInWidget(self.details_widget)
|
||||
|
||||
|
||||
class FreeGamesScroll(QScrollArea):
|
||||
def __init__(self, parent=None):
|
||||
super(FreeGamesScroll, self).__init__(parent=parent)
|
||||
self.setObjectName(type(self).__name__)
|
||||
|
||||
def setWidget(self, w):
|
||||
super().setWidget(w)
|
||||
w.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
|
||||
if a0 is self.widget() and a1.type() == QEvent.Resize:
|
||||
self.__resize(a0)
|
||||
return a0.event(a1)
|
||||
return False
|
||||
|
||||
def __resize(self, e: QResizeEvent):
|
||||
minh = self.horizontalScrollBar().minimum()
|
||||
maxh = self.horizontalScrollBar().maximum()
|
||||
# lk: when the scrollbar is not visible, min and max are 0
|
||||
if maxh > minh:
|
||||
height = (
|
||||
e.size().height()
|
||||
+ self.rect().height() // 2
|
||||
- self.contentsRect().height() // 2
|
||||
+ self.widget().layout().spacing()
|
||||
+ self.horizontalScrollBar().sizeHint().height()
|
||||
)
|
||||
else:
|
||||
height = e.size().height() + self.rect().height() - self.contentsRect().height()
|
||||
self.setMinimumHeight(max(height, self.minimumHeight()))
|
||||
|
||||
|
||||
class LandingWidget(QWidget, SideTabContents):
|
||||
show_details = pyqtSignal(CatalogOfferModel)
|
||||
|
||||
def __init__(self, api: StoreAPI, parent=None):
|
||||
super(LandingWidget, self).__init__(parent=parent)
|
||||
self.api = api
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 3, 0)
|
||||
self.setLayout(layout)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
|
||||
self.free_games_now = StoreGroup(self.tr("Free now"), layout=QHBoxLayout, parent=self)
|
||||
self.free_games_now.main_layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
self.free_games_now.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
|
||||
self.free_games_next = StoreGroup(self.tr("Free next week"), layout=QHBoxLayout, parent=self)
|
||||
self.free_games_next.main_layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
self.free_games_next.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
|
||||
self.discounts_group = StoreGroup(self.tr("Wishlist discounts"), layout=FlowLayout, parent=self)
|
||||
self.discounts_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
self.games_group = StoreGroup(self.tr("Free to play"), FlowLayout, self)
|
||||
self.games_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.games_group.loading(False)
|
||||
self.games_group.setVisible(False)
|
||||
|
||||
free_scroll = FreeGamesScroll(self)
|
||||
free_container = QWidget(free_scroll)
|
||||
free_scroll.setWidget(free_container)
|
||||
free_container_layout = QHBoxLayout(free_container)
|
||||
|
||||
free_scroll.setWidgetResizable(True)
|
||||
free_scroll.setFrameShape(QScrollArea.NoFrame)
|
||||
free_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
|
||||
free_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
|
||||
free_container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
free_container_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
free_container_layout.setSizeConstraint(QHBoxLayout.SetFixedSize)
|
||||
free_container_layout.addWidget(self.free_games_now)
|
||||
free_container_layout.addWidget(self.free_games_next)
|
||||
|
||||
free_scroll.widget().setAutoFillBackground(False)
|
||||
free_scroll.viewport().setAutoFillBackground(False)
|
||||
|
||||
# layout.addWidget(self.free_games_now, alignment=Qt.AlignTop)
|
||||
# layout.addWidget(self.free_games_next, alignment=Qt.AlignTop)
|
||||
layout.addWidget(free_scroll, alignment=Qt.AlignTop)
|
||||
layout.addWidget(self.discounts_group, alignment=Qt.AlignTop)
|
||||
layout.addWidget(self.games_group, alignment=Qt.AlignTop)
|
||||
layout.addItem(QSpacerItem(0, 0, QSizePolicy.Fixed, QSizePolicy.Expanding))
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.api.get_free(self.__update_free_games)
|
||||
self.api.get_wishlist(self.__update_wishlist_discounts)
|
||||
return super().showEvent(a0)
|
||||
|
||||
def hideEvent(self, a0: QHideEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().hideEvent(a0)
|
||||
# TODO: Implement tab unloading
|
||||
return super().hideEvent(a0)
|
||||
|
||||
def __update_wishlist_discounts(self, wishlist: List[WishlistItemModel]):
|
||||
for w in self.discounts_group.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly):
|
||||
self.discounts_group.layout().removeWidget(w)
|
||||
w.deleteLater()
|
||||
|
||||
for item in filter(lambda x: bool(x.offer.price.totalPrice.discount), wishlist):
|
||||
w = StoreItemWidget(self.api.cached_manager, item.offer)
|
||||
w.show_details.connect(self.show_details)
|
||||
self.discounts_group.layout().addWidget(w)
|
||||
have_discounts = any(map(lambda x: bool(x.offer.price.totalPrice.discount), wishlist))
|
||||
self.discounts_group.setVisible(have_discounts)
|
||||
self.discounts_group.loading(False)
|
||||
|
||||
def __update_free_games(self, free_games: List[CatalogOfferModel]):
|
||||
for w in self.free_games_now.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly):
|
||||
self.free_games_now.layout().removeWidget(w)
|
||||
w.deleteLater()
|
||||
|
||||
for w in self.free_games_next.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly):
|
||||
self.free_games_next.layout().removeWidget(w)
|
||||
w.deleteLater()
|
||||
|
||||
date = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
|
||||
free_now = []
|
||||
free_next = []
|
||||
for item in free_games:
|
||||
try:
|
||||
if item.price.totalPrice.discountPrice == 0:
|
||||
free_now.append(item)
|
||||
continue
|
||||
if item.title == "Mystery Game":
|
||||
free_next.append(item)
|
||||
continue
|
||||
except KeyError as e:
|
||||
logger.warning(str(e))
|
||||
|
||||
if item.promotions is not None:
|
||||
if not item.promotions.promotionalOffers:
|
||||
start_date = item.promotions.upcomingPromotionalOffers[0].promotionalOffers[0].startDate
|
||||
else:
|
||||
start_date = item.promotions.promotionalOffers[0].promotionalOffers[0].startDate
|
||||
|
||||
if start_date > date:
|
||||
free_next.append(item)
|
||||
|
||||
# free games now
|
||||
self.free_games_now.setVisible(bool(free_now))
|
||||
for item in free_now:
|
||||
w = StoreItemWidget(self.api.cached_manager, item)
|
||||
w.show_details.connect(self.show_details)
|
||||
self.free_games_now.layout().addWidget(w)
|
||||
self.free_games_now.loading(False)
|
||||
|
||||
# free games next week
|
||||
self.free_games_next.setVisible(bool(free_next))
|
||||
for item in free_next:
|
||||
w = StoreItemWidget(self.api.cached_manager, item)
|
||||
if item.title != "Mystery Game":
|
||||
w.show_details.connect(self.show_details)
|
||||
self.free_games_next.layout().addWidget(w)
|
||||
self.free_games_next.loading(False)
|
||||
|
||||
def show_games(self, data):
|
||||
if not data:
|
||||
return
|
||||
|
||||
for w in self.games_group.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly):
|
||||
self.games_group.layout().removeWidget(w)
|
||||
w.deleteLater()
|
||||
|
||||
for game in data:
|
||||
w = StoreItemWidget(self.api.cached_manager, game)
|
||||
w.show_details.connect(self.show_details)
|
||||
self.games_group.layout().addWidget(w)
|
||||
self.games_group.loading(False)
|
56
rare/components/tabs/store/results.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QSizePolicy,
|
||||
QLabel,
|
||||
QScrollArea,
|
||||
)
|
||||
|
||||
from rare.widgets.flow_layout import FlowLayout
|
||||
from .api.models.response import CatalogOfferModel
|
||||
from .widgets.items import ResultsItemWidget
|
||||
|
||||
|
||||
class ResultsWidget(QScrollArea):
|
||||
show_details = pyqtSignal(CatalogOfferModel)
|
||||
|
||||
def __init__(self, store_api, parent=None):
|
||||
super(ResultsWidget, self).__init__(parent=parent)
|
||||
self.store_api = store_api
|
||||
|
||||
self.results_container = QWidget(self)
|
||||
self.results_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.results_layout = FlowLayout(self.results_container)
|
||||
self.setWidget(self.results_container)
|
||||
self.setWidgetResizable(True)
|
||||
|
||||
# self.main_layout = QVBoxLayout(self)
|
||||
# self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# self.main_layout.addWidget(self.results_scrollarea)
|
||||
|
||||
self.setEnabled(False)
|
||||
|
||||
def load_results(self, text: str):
|
||||
self.setEnabled(False)
|
||||
if text != "":
|
||||
self.store_api.search_game(text, self.show_results)
|
||||
|
||||
def show_results(self, results: dict):
|
||||
for w in self.results_container.findChildren(QLabel, options=Qt.FindDirectChildrenOnly):
|
||||
self.results_layout.removeWidget(w)
|
||||
w.deleteLater()
|
||||
for w in self.results_container.findChildren(ResultsItemWidget, options=Qt.FindDirectChildrenOnly):
|
||||
self.results_layout.removeWidget(w)
|
||||
w.deleteLater()
|
||||
|
||||
if not results:
|
||||
self.results_layout.addWidget(QLabel(self.tr("No results found")))
|
||||
else:
|
||||
for res in results:
|
||||
w = ResultsItemWidget(self.store_api.cached_manager, res, parent=self.results_container)
|
||||
w.show_details.connect(self.show_details.emit)
|
||||
self.results_layout.addWidget(w)
|
||||
self.results_layout.update()
|
||||
self.setEnabled(True)
|
||||
|
219
rare/components/tabs/store/search.py
Normal file
|
@ -0,0 +1,219 @@
|
|||
import logging
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot
|
||||
from PyQt5.QtWidgets import (
|
||||
QCheckBox,
|
||||
QWidget,
|
||||
QSizePolicy,
|
||||
QScrollArea,
|
||||
QFrame,
|
||||
)
|
||||
from legendary.core import LegendaryCore
|
||||
|
||||
from rare.ui.components.tabs.store.search import Ui_SearchWidget
|
||||
from rare.utils.extra_widgets import ButtonLineEdit
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from rare.widgets.sliding_stack import SlidingStackedWidget
|
||||
from .api.models.query import SearchStoreQuery
|
||||
from .api.models.response import CatalogOfferModel
|
||||
from .constants import Constants
|
||||
from .results import ResultsWidget
|
||||
from .store_api import StoreAPI
|
||||
from .widgets.details import StoreDetailsWidget
|
||||
|
||||
logger = logging.getLogger("Shop")
|
||||
|
||||
|
||||
class SearchPage(SlidingStackedWidget, SideTabContents):
|
||||
def __init__(self, store_api: StoreAPI, parent=None):
|
||||
super(SearchPage, self).__init__(parent=parent)
|
||||
self.implements_scrollarea = True
|
||||
|
||||
self.search_widget = SearchWidget(store_api, parent=self)
|
||||
self.search_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.search_widget.set_title.connect(self.set_title)
|
||||
self.search_widget.show_details.connect(self.show_details)
|
||||
|
||||
self.details_widget = StoreDetailsWidget([], store_api, parent=self)
|
||||
self.details_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.details_widget.set_title.connect(self.set_title)
|
||||
self.details_widget.back_clicked.connect(self.show_main)
|
||||
|
||||
self.setDirection(Qt.Horizontal)
|
||||
self.addWidget(self.search_widget)
|
||||
self.addWidget(self.details_widget)
|
||||
|
||||
@pyqtSlot()
|
||||
def show_main(self):
|
||||
self.slideInWidget(self.search_widget)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def show_details(self, game: CatalogOfferModel):
|
||||
self.details_widget.update_game(game)
|
||||
self.slideInWidget(self.details_widget)
|
||||
|
||||
|
||||
# noinspection PyAttributeOutsideInit,PyBroadException
|
||||
class SearchWidget(QWidget, SideTabContents):
|
||||
show_details = pyqtSignal(CatalogOfferModel)
|
||||
|
||||
def __init__(self, store_api: StoreAPI, parent=None):
|
||||
super(SearchWidget, self).__init__(parent=parent)
|
||||
self.implements_scrollarea = True
|
||||
self.ui = Ui_SearchWidget()
|
||||
self.ui.setupUi(self)
|
||||
self.ui.main_layout.setContentsMargins(0, 0, 3, 0)
|
||||
|
||||
self.ui.filter_scrollarea.widget().setAutoFillBackground(False)
|
||||
self.ui.filter_scrollarea.viewport().setAutoFillBackground(False)
|
||||
|
||||
self.store_api = store_api
|
||||
self.price = ""
|
||||
self.tags = []
|
||||
self.types = []
|
||||
self.update_games_allowed = True
|
||||
|
||||
self.active_search_request = False
|
||||
self.next_search = ""
|
||||
self.wishlist: List = []
|
||||
|
||||
self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search"))
|
||||
self.results_scrollarea = ResultsWidget(self.store_api, self)
|
||||
self.results_scrollarea.show_details.connect(self.show_details)
|
||||
|
||||
self.ui.left_layout.addWidget(self.search_bar)
|
||||
self.ui.left_layout.addWidget(self.results_scrollarea)
|
||||
|
||||
self.search_bar.returnPressed.connect(self.show_search_results)
|
||||
self.search_bar.buttonClicked.connect(self.show_search_results)
|
||||
|
||||
# self.init_filter()
|
||||
|
||||
def load(self):
|
||||
# load browse games
|
||||
self.prepare_request()
|
||||
|
||||
def show_search_results(self):
|
||||
if text := self.search_bar.text():
|
||||
self.results_scrollarea.load_results(text)
|
||||
# self.show_info.emit(self.search_bar.text())
|
||||
|
||||
def init_filter(self):
|
||||
self.ui.none_price.toggled.connect(
|
||||
lambda: self.prepare_request("") if self.ui.none_price.isChecked() else None
|
||||
)
|
||||
self.ui.free_button.toggled.connect(
|
||||
lambda: self.prepare_request("free") if self.ui.free_button.isChecked() else None
|
||||
)
|
||||
self.ui.under10.toggled.connect(
|
||||
lambda: self.prepare_request("<price>[0, 1000)") if self.ui.under10.isChecked() else None
|
||||
)
|
||||
self.ui.under20.toggled.connect(
|
||||
lambda: self.prepare_request("<price>[0, 2000)") if self.ui.under20.isChecked() else None
|
||||
)
|
||||
self.ui.under30.toggled.connect(
|
||||
lambda: self.prepare_request("<price>[0, 3000)") if self.ui.under30.isChecked() else None
|
||||
)
|
||||
self.ui.above.toggled.connect(
|
||||
lambda: self.prepare_request("<price>[1499,]") if self.ui.above.isChecked() else None
|
||||
)
|
||||
# self.on_discount.toggled.connect(lambda: self.prepare_request("sale") if self.on_discount.isChecked() else None)
|
||||
self.ui.on_discount.toggled.connect(lambda: self.prepare_request())
|
||||
constants = Constants()
|
||||
|
||||
self.checkboxes = []
|
||||
|
||||
for groupbox, variables in [
|
||||
(self.ui.genre_group, constants.categories),
|
||||
(self.ui.platform_group, constants.platforms),
|
||||
(self.ui.others_group, constants.others),
|
||||
(self.ui.type_group, constants.types),
|
||||
]:
|
||||
for text, tag in variables:
|
||||
checkbox = CheckBox(text, tag)
|
||||
checkbox.activated.connect(lambda x: self.prepare_request(added_tag=x))
|
||||
checkbox.deactivated.connect(lambda x: self.prepare_request(removed_tag=x))
|
||||
groupbox.layout().addWidget(checkbox)
|
||||
self.checkboxes.append(checkbox)
|
||||
self.ui.reset_button.clicked.connect(self.reset_filters)
|
||||
self.ui.filter_scrollarea.setMinimumWidth(
|
||||
self.ui.filter_container.sizeHint().width()
|
||||
+ self.ui.filter_container.layout().contentsMargins().left()
|
||||
+ self.ui.filter_container.layout().contentsMargins().right()
|
||||
+ self.ui.filter_scrollarea.verticalScrollBar().sizeHint().width()
|
||||
)
|
||||
|
||||
def reset_filters(self):
|
||||
self.update_games_allowed = False
|
||||
for cb in self.checkboxes:
|
||||
cb.setChecked(False)
|
||||
self.ui.none_price.setChecked(True)
|
||||
|
||||
self.tags = []
|
||||
self.types = []
|
||||
self.update_games_allowed = True
|
||||
|
||||
self.ui.on_discount.setChecked(False)
|
||||
|
||||
def prepare_request(
|
||||
self,
|
||||
price: str = None,
|
||||
added_tag: int = 0,
|
||||
removed_tag: int = 0,
|
||||
added_type: str = "",
|
||||
removed_type: str = "",
|
||||
):
|
||||
if not self.update_games_allowed:
|
||||
return
|
||||
if price is not None:
|
||||
self.price = price
|
||||
|
||||
if added_tag != 0:
|
||||
self.tags.append(added_tag)
|
||||
if removed_tag != 0 and removed_tag in self.tags:
|
||||
self.tags.remove(removed_tag)
|
||||
|
||||
if added_type:
|
||||
self.types.append(added_type)
|
||||
if removed_type and removed_type in self.types:
|
||||
self.types.remove(removed_type)
|
||||
if (self.types or self.price) or self.tags or self.ui.on_discount.isChecked():
|
||||
# self.free_scrollarea.setVisible(False)
|
||||
self.discounts_group.setVisible(False)
|
||||
else:
|
||||
# self.free_scrollarea.setVisible(True)
|
||||
if len(self.discounts_group.layout().children()) > 0:
|
||||
self.discounts_group.setVisible(True)
|
||||
|
||||
self.games_group.loading(True)
|
||||
|
||||
browse_model = SearchStoreQuery(
|
||||
language=self.store_api.language_code,
|
||||
country=self.store_api.country_code,
|
||||
count=20,
|
||||
price_range=self.price,
|
||||
on_sale=self.ui.on_discount.isChecked(),
|
||||
)
|
||||
browse_model.tag = "|".join(self.tags)
|
||||
|
||||
if self.types:
|
||||
browse_model.category = "|".join(self.types)
|
||||
self.store_api.browse_games(browse_model, self.show_games)
|
||||
|
||||
|
||||
class CheckBox(QCheckBox):
|
||||
activated = pyqtSignal(str)
|
||||
deactivated = pyqtSignal(str)
|
||||
|
||||
def __init__(self, text, tag):
|
||||
super(CheckBox, self).__init__(text)
|
||||
self.tag = tag
|
||||
|
||||
self.toggled.connect(self.handle_toggle)
|
||||
|
||||
def handle_toggle(self):
|
||||
if self.isChecked():
|
||||
self.activated.emit(self.tag)
|
||||
else:
|
||||
self.deactivated.emit(self.tag)
|
|
@ -1,111 +0,0 @@
|
|||
from PyQt5 import QtGui
|
||||
from PyQt5.QtCore import pyqtSignal, Qt
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QScrollArea,
|
||||
QGroupBox,
|
||||
QPushButton,
|
||||
QStackedWidget,
|
||||
)
|
||||
|
||||
from rare.utils.extra_widgets import ImageLabel, WaitingSpinner
|
||||
from rare.widgets.flow_layout import FlowLayout
|
||||
|
||||
|
||||
class SearchResults(QStackedWidget):
|
||||
show_info = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, api_core):
|
||||
super(SearchResults, self).__init__()
|
||||
self.search_result_widget = QWidget()
|
||||
self.api_core = api_core
|
||||
self.addWidget(self.search_result_widget)
|
||||
self.main_layout = QVBoxLayout()
|
||||
self.back_button = QPushButton(self.tr("Back"))
|
||||
self.main_layout.addWidget(self.back_button)
|
||||
self.main_layout.addWidget(self.back_button)
|
||||
self.result_area = QScrollArea()
|
||||
self.widget = QWidget()
|
||||
self.result_area.setWidgetResizable(True)
|
||||
self.main_layout.addWidget(self.result_area)
|
||||
self.result_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
|
||||
self.result_area.setWidget(self.widget)
|
||||
self.layout = FlowLayout()
|
||||
self.widget.setLayout(self.layout)
|
||||
|
||||
self.search_result_widget.setLayout(self.main_layout)
|
||||
|
||||
self.addWidget(WaitingSpinner())
|
||||
self.setCurrentIndex(1)
|
||||
|
||||
def load_results(self, text: str):
|
||||
self.setCurrentIndex(1)
|
||||
if text != "":
|
||||
self.api_core.search_game(text, self.show_results)
|
||||
|
||||
def show_results(self, results: dict):
|
||||
self.widget.deleteLater()
|
||||
self.widget = QWidget()
|
||||
self.layout = FlowLayout()
|
||||
if not results:
|
||||
self.layout.addWidget(QLabel(self.tr("No results found")))
|
||||
else:
|
||||
for res in results:
|
||||
w = _SearchResultItem(res)
|
||||
w.show_info.connect(self.show_info.emit)
|
||||
self.layout.addWidget(w)
|
||||
self.widget.setLayout(self.layout)
|
||||
self.result_area.setWidget(self.widget)
|
||||
self.setCurrentIndex(0)
|
||||
|
||||
|
||||
class _SearchResultItem(QGroupBox):
|
||||
res: dict
|
||||
show_info = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, result: dict):
|
||||
super(_SearchResultItem, self).__init__()
|
||||
self.layout = QVBoxLayout()
|
||||
self.image = ImageLabel()
|
||||
for img in result["keyImages"]:
|
||||
if img["type"] == "DieselStoreFrontTall":
|
||||
width = 240
|
||||
self.image.update_image(img["url"], result["title"], (width, 360))
|
||||
break
|
||||
else:
|
||||
print("No image found")
|
||||
self.layout.addWidget(self.image)
|
||||
|
||||
self.res = result
|
||||
self.title = QLabel(self.res["title"])
|
||||
title_font = QFont()
|
||||
title_font.setPixelSize(15)
|
||||
self.title.setFont(title_font)
|
||||
self.title.setWordWrap(True)
|
||||
self.layout.addWidget(self.title)
|
||||
price = result["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
|
||||
discount_price = result["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
|
||||
price_layout = QHBoxLayout()
|
||||
price_label = QLabel(price if price != "0" else self.tr("Free"))
|
||||
price_layout.addWidget(price_label)
|
||||
|
||||
if price != discount_price:
|
||||
font = QFont()
|
||||
font.setStrikeOut(True)
|
||||
price_label.setFont(font)
|
||||
price_layout.addWidget(QLabel(discount_price))
|
||||
# self.discount_price = QLabel(f"{self.tr('Discount price: ')}{discount_price}")
|
||||
self.layout.addLayout(price_layout)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.setFixedWidth(260)
|
||||
|
||||
def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None:
|
||||
if a0.button() == 1:
|
||||
self.show_info.emit(self.res)
|