Compare commits
87 commits
1.10.11.14
...
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 |
1
.gitattributes
vendored
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
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
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
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
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
|
||||
|
|
4
.github/workflows/job_version.yml
vendored
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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
643
pylintrc
Normal file
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
|
||||
|
@ -34,15 +35,15 @@ DETACHED_APP_NAMES = {
|
|||
}
|
||||
|
||||
|
||||
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, args: InitArgs, rgame: RareGameSlim, sync_action=None):
|
||||
super(PreLaunchThread, self).__init__()
|
||||
super(PreLaunch, self).__init__()
|
||||
self.signals = self.Signals()
|
||||
self.logger = getLogger(type(self).__name__)
|
||||
self.args = args
|
||||
|
@ -75,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
|
||||
|
@ -171,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")
|
||||
)
|
||||
|
||||
|
@ -310,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):
|
||||
|
@ -323,7 +327,7 @@ class RareLauncher(RareApp):
|
|||
self.stop()
|
||||
|
||||
def start_prepare(self, sync_action=None):
|
||||
worker = PreLaunchThread(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,7 +9,7 @@ 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.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import ButtonDialog, game_title
|
||||
|
||||
logger = getLogger("CloudSyncDialog")
|
||||
|
@ -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,3 +1,4 @@
|
|||
import os
|
||||
from typing import Union
|
||||
|
||||
from PyQt5.QtCore import QProcessEnvironment, pyqtSignal, QSize, Qt
|
||||
|
@ -13,7 +14,7 @@ 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
|
||||
|
||||
|
||||
|
@ -31,8 +32,8 @@ class ConsoleDialog(QDialog):
|
|||
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()
|
||||
|
||||
|
@ -46,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))
|
||||
|
||||
|
@ -102,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"))
|
||||
|
||||
|
@ -113,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
|
||||
|
||||
|
@ -170,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:#aaa;white-space:pre\">{text}</p>"
|
||||
self._cursor_output.insertHtml(html)
|
||||
self.scroll_to_last_line()
|
||||
|
||||
def error(self, text):
|
||||
html = f"<p style=\"color:#a33;white-space:pre\">{text}</p>"
|
||||
self._cursor_output.insertHtml(html)
|
||||
self.scroll_to_last_line()
|
||||
|
||||
def scroll_to_last_line(self):
|
||||
cursor = self.textCursor()
|
||||
|
@ -189,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,7 +13,7 @@ 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, game_title
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
|
@ -63,17 +63,17 @@ 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")
|
||||
bicon = qta_icon("fa.gear")
|
||||
self.setWindowTitle(game_title(header, rgame.app_title))
|
||||
self.setSubtitle(game_title(header, rgame.app_title))
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -9,7 +9,7 @@ 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
|
||||
|
||||
|
@ -34,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],
|
||||
|
|
|
@ -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,7 +10,7 @@ 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.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
|
||||
|
@ -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)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ 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.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import ButtonDialog, game_title
|
||||
from rare.widgets.selective_widget import SelectiveWidget
|
||||
|
||||
|
@ -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,7 +6,7 @@ from PyQt5.QtWidgets import (
|
|||
|
||||
from rare.models.game import RareGame
|
||||
from rare.models.install import UninstallOptionsModel
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import ButtonDialog, game_title
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
@ -38,7 +39,7 @@ class MainTabWidget(QTabWidget):
|
|||
self.setTabEnabled(self.downloads_index, not self.args.offline)
|
||||
|
||||
if not self.args.offline:
|
||||
self.store_tab = Shop(self.core)
|
||||
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"
|
||||
|
|
|
@ -61,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)
|
||||
|
|
|
@ -59,11 +59,11 @@ class DownloadWidget(ImageWidget):
|
|||
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)
|
||||
|
|
|
@ -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,8 +63,9 @@ 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, a0: QKeyEvent):
|
||||
if a0.key() == Qt.Key_Escape:
|
||||
|
@ -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
|
||||
|
|
|
@ -22,7 +22,7 @@ from rare.shared import RareCore
|
|||
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
|
||||
|
@ -45,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)
|
||||
|
@ -73,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)
|
||||
|
|
|
@ -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, style_hyperlink
|
||||
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()
|
|
@ -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()
|
|
@ -19,10 +19,9 @@ 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
|
||||
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")
|
||||
|
||||
|
@ -85,10 +84,10 @@ class GameLaunchSettings(LaunchSettingsBase):
|
|||
|
||||
if self.igame:
|
||||
self.offline_combo.setEnabled(self.igame.can_run_offline)
|
||||
self.override_exe_edit.set_root(self.igame.install_path)
|
||||
self.override_exe_edit.setRootPath(self.igame.install_path)
|
||||
else:
|
||||
self.offline_combo.setEnabled(False)
|
||||
self.override_exe_edit.set_root("")
|
||||
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)
|
||||
|
@ -140,21 +139,18 @@ class GameLaunchSettings(LaunchSettingsBase):
|
|||
|
||||
|
||||
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
|
||||
|
||||
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 GameMangoHudSettings(MangoHudSettings):
|
||||
def load_settings(self, app_name: str):
|
||||
self.app_name = app_name
|
||||
|
||||
|
||||
class GameDxvkSettings(DxvkSettings):
|
||||
|
@ -169,18 +165,19 @@ class GameEnvVars(EnvVars):
|
|||
|
||||
class GameSettings(GameSettingsBase):
|
||||
def __init__(self, parent=None):
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
super(GameSettings, self).__init__(
|
||||
GameLaunchSettings, GameDxvkSettings, GameEnvVars,
|
||||
GameWineSettings, GameProtonSettings, GameMangoHudSettings,
|
||||
parent=parent
|
||||
)
|
||||
elif pf.system() != "Windows":
|
||||
super(GameSettings, self).__init__(
|
||||
GameLaunchSettings, GameDxvkSettings, GameEnvVars,
|
||||
GameWineSettings,
|
||||
parent=parent
|
||||
)
|
||||
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,
|
||||
|
@ -193,8 +190,8 @@ class GameSettings(GameSettingsBase):
|
|||
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)
|
||||
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)
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ class ListGameWidget(GameWidget):
|
|||
|
||||
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
|
||||
|
||||
|
||||
|
@ -40,13 +40,13 @@ 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.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.setIcon(qta_icon("ei.play-alt"))
|
||||
self.launch_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.launch_btn.setFixedWidth(120)
|
||||
|
||||
|
|
|
@ -5,15 +5,14 @@ from PyQt5.QtWidgets import (
|
|||
QWidget,
|
||||
QHBoxLayout,
|
||||
QComboBox,
|
||||
QToolButton,
|
||||
QMenu,
|
||||
QAction,
|
||||
QAction, QSpacerItem, QSizePolicy,
|
||||
)
|
||||
|
||||
from rare.shared import RareCore
|
||||
from rare.models.options import options, LibraryFilter, LibraryOrder
|
||||
from rare.shared import RareCore
|
||||
from rare.utils.extra_widgets import ButtonLineEdit
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
|
||||
|
||||
class GameListHeadBar(QWidget):
|
||||
|
@ -34,7 +33,7 @@ class GameListHeadBar(QWidget):
|
|||
LibraryFilter.ALL: self.tr("All games"),
|
||||
LibraryFilter.INSTALLED: self.tr("Installed"),
|
||||
LibraryFilter.OFFLINE: self.tr("Offline"),
|
||||
# int(LibraryFilter.HIDDEN): self.tr("Hidden"),
|
||||
# LibraryFilter.HIDDEN: self.tr("Hidden"),
|
||||
}
|
||||
for data, text in filters.items():
|
||||
self.filter.addItem(text, data)
|
||||
|
@ -83,15 +82,15 @@ class GameListHeadBar(QWidget):
|
|||
|
||||
integrations_menu = QMenu(parent=self)
|
||||
import_action = QAction(
|
||||
icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu
|
||||
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
|
||||
qta_icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"), integrations_menu
|
||||
)
|
||||
eos_ubisoft_action.triggered.connect(self.goto_eos_ubisoft)
|
||||
|
||||
|
@ -99,19 +98,18 @@ class GameListHeadBar(QWidget):
|
|||
integrations_menu.addAction(egl_sync_action)
|
||||
integrations_menu.addAction(eos_ubisoft_action)
|
||||
|
||||
integrations = QToolButton(parent=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(250)
|
||||
|
||||
installed_tooltip = self.tr("Installed games")
|
||||
self.installed_icon = QLabel(parent=self)
|
||||
self.installed_icon.setPixmap(icon("ph.floppy-disk-back-fill").pixmap(QSize(16, 16)))
|
||||
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()
|
||||
|
@ -120,29 +118,27 @@ class GameListHeadBar(QWidget):
|
|||
self.installed_label.setToolTip(installed_tooltip)
|
||||
available_tooltip = self.tr("Available games")
|
||||
self.available_icon = QLabel(parent=self)
|
||||
self.available_icon.setPixmap(icon("ph.floppy-disk-back-light").pixmap(QSize(16, 16)))
|
||||
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.refresh_list = QPushButton(parent=self)
|
||||
self.refresh_list.setIcon(icon("fa.refresh")) # Reload icon
|
||||
self.refresh_list.setIcon(qta_icon("fa.refresh")) # Reload icon
|
||||
self.refresh_list.clicked.connect(self.__refresh_clicked)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 5, 0, 5)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.filter)
|
||||
layout.addWidget(self.order)
|
||||
layout.addStretch(0)
|
||||
layout.addWidget(integrations)
|
||||
layout.addStretch(3)
|
||||
layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed))
|
||||
layout.addWidget(self.search_bar)
|
||||
layout.addStretch(4)
|
||||
layout.addWidget(self.installed_icon)
|
||||
layout.addWidget(self.installed_label)
|
||||
layout.addWidget(self.available_icon)
|
||||
layout.addWidget(self.available_label)
|
||||
layout.addStretch(4)
|
||||
layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed))
|
||||
layout.addWidget(integrations)
|
||||
layout.addWidget(self.refresh_list)
|
||||
|
||||
def set_games_count(self, inst: int, avail: int) -> None:
|
||||
|
|
|
@ -46,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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -22,12 +22,14 @@ class SettingsTab(SideTabWidget):
|
|||
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)
|
||||
|
|
|
@ -9,10 +9,9 @@ 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
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
from .widgets.proton import ProtonSettings
|
||||
from .widgets.overlay import MangoHudSettings
|
||||
|
||||
logger = getLogger("GameSettings")
|
||||
|
||||
|
@ -24,18 +23,19 @@ class LaunchSettings(LaunchSettingsBase):
|
|||
|
||||
class GameSettings(GameSettingsBase):
|
||||
def __init__(self, parent=None):
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
super(GameSettings, self).__init__(
|
||||
LaunchSettings, DxvkSettings, EnvVars,
|
||||
WineSettings, ProtonSettings, MangoHudSettings,
|
||||
parent=parent
|
||||
)
|
||||
elif pf.system() != "Windows":
|
||||
super(GameSettings, self).__init__(
|
||||
LaunchSettings, DxvkSettings, EnvVars,
|
||||
WineSettings,
|
||||
parent=parent
|
||||
)
|
||||
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,
|
||||
|
|
|
@ -8,7 +8,7 @@ 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":
|
||||
from rare.utils.compat.wine import get_wine_environment
|
||||
|
@ -139,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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
from typing import Tuple, Type, TypeVar
|
||||
|
||||
|
@ -73,7 +74,11 @@ class LaunchSettingsBase(QGroupBox):
|
|||
def __prelaunch_edit_callback(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]):
|
||||
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
|
||||
|
|
|
@ -4,8 +4,17 @@ import shutil
|
|||
from logging import getLogger
|
||||
from typing import Optional, Tuple, Iterable
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QSize, Qt, QMimeData, pyqtSlot
|
||||
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent, QShowEvent
|
||||
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,
|
||||
|
@ -15,13 +24,16 @@ from PyQt5.QtWidgets import (
|
|||
QWidget,
|
||||
QScrollArea,
|
||||
QAction,
|
||||
QToolButton,
|
||||
QMenu, QStackedWidget, QPushButton, QLineEdit, QVBoxLayout, QComboBox,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QLineEdit,
|
||||
QVBoxLayout,
|
||||
QComboBox,
|
||||
)
|
||||
|
||||
from rare.models.wrapper import Wrapper
|
||||
from rare.shared import RareCore
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.dialogs import ButtonDialog, game_title
|
||||
|
||||
if pf.system() in {"Linux", "FreeBSD"}:
|
||||
|
@ -45,7 +57,7 @@ class WrapperEditDialog(ButtonDialog):
|
|||
self.setCentralLayout(self.widget_layout)
|
||||
|
||||
self.accept_button.setText(self.tr("Save"))
|
||||
self.accept_button.setIcon(icon("fa.edit"))
|
||||
self.accept_button.setIcon(qta_icon("fa.edit"))
|
||||
self.accept_button.setEnabled(False)
|
||||
|
||||
self.result: Tuple = ()
|
||||
|
@ -108,7 +120,7 @@ class WrapperWidget(QFrame):
|
|||
text_lbl.setEnabled(wrapper.is_editable)
|
||||
|
||||
image_lbl = QLabel(parent=self)
|
||||
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
|
||||
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)
|
||||
|
@ -118,10 +130,9 @@ class WrapperWidget(QFrame):
|
|||
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 = QPushButton(parent=self)
|
||||
manage_button.setIcon(qta_icon("mdi.menu", fallback="fa.align-justify"))
|
||||
manage_button.setMenu(manage_menu)
|
||||
manage_button.setPopupMode(QToolButton.InstantPopup)
|
||||
manage_button.setEnabled(wrapper.is_editable)
|
||||
if not wrapper.is_editable:
|
||||
manage_button.setToolTip(self.tr("Manage through settings"))
|
||||
|
@ -174,46 +185,75 @@ class WrapperWidget(QFrame):
|
|||
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.widget_stack = QStackedWidget(self)
|
||||
|
||||
self.wrapper_scroll = QScrollArea(self.widget_stack)
|
||||
self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
|
||||
self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.wrapper_scroll.setWidgetResizable(True)
|
||||
self.wrapper_scroll.setProperty("no_kinetic_scroll", True)
|
||||
self.wrapper_container = WrapperContainer(parent=self.wrapper_scroll)
|
||||
self.wrapper_container.orderChanged.connect(self.__on_order_changed)
|
||||
self.wrapper_scroll.setWidget(self.wrapper_container)
|
||||
|
||||
self.no_wrapper_label = QLabel(self.tr("No wrappers defined"), self.widget_stack)
|
||||
|
||||
self.widget_stack.addWidget(self.wrapper_scroll)
|
||||
self.widget_stack.addWidget(self.no_wrapper_label)
|
||||
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.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
|
||||
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.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")
|
||||
self.wrapper_label.setObjectName(f"{self.objectName()}Label")
|
||||
|
||||
main_layout = QHBoxLayout(self)
|
||||
main_layout.addWidget(self.widget_stack)
|
||||
main_layout.addWidget(self.add_button, alignment=Qt.AlignTop)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setAlignment(Qt.AlignTop)
|
||||
main_layout.addWidget(self.wrapper_scroll, alignment=Qt.AlignTop)
|
||||
main_layout.addWidget(self.add_button, alignment=Qt.AlignTop)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
|
||||
self.app_name: str = "default"
|
||||
self.core = RareCore.instance().core()
|
||||
|
@ -225,27 +265,6 @@ class WrapperSettings(QWidget):
|
|||
self.update_state()
|
||||
return super().showEvent(a0)
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def adjust_scrollarea(self, minh: int, maxh: int):
|
||||
wrapper_widget = self.wrapper_container.findChild(WrapperWidget)
|
||||
if not wrapper_widget:
|
||||
return
|
||||
# lk: when the scrollbar is not visible, min and max are 0
|
||||
if maxh > minh:
|
||||
self.wrapper_scroll.setMaximumHeight(
|
||||
wrapper_widget.sizeHint().height()
|
||||
+ self.wrapper_scroll.rect().height() // 2
|
||||
- self.wrapper_scroll.contentsRect().height() // 2
|
||||
+ self.wrapper_container.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()
|
||||
)
|
||||
|
||||
@pyqtSlot(QWidget, int)
|
||||
def __on_order_changed(self, widget: WrapperWidget, new_index: int):
|
||||
wrapper = widget.data()
|
||||
|
@ -268,16 +287,12 @@ class WrapperSettings(QWidget):
|
|||
self.add_user_wrapper(wrapper)
|
||||
|
||||
def __add_wrapper(self, wrapper: Wrapper, position: int = -1):
|
||||
self.widget_stack.setCurrentWidget(self.wrapper_scroll)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
@ -306,7 +321,9 @@ class WrapperSettings(QWidget):
|
|||
|
||||
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)
|
||||
self,
|
||||
self.tr("Warning"),
|
||||
self.tr("Wrapper <b>{0}</b> is already in the list").format(wrapper.as_str),
|
||||
)
|
||||
return
|
||||
|
||||
|
@ -316,7 +333,7 @@ class WrapperSettings(QWidget):
|
|||
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
|
||||
QMessageBox.No,
|
||||
)
|
||||
if ans == QMessageBox.No:
|
||||
return
|
||||
|
@ -329,8 +346,7 @@ class WrapperSettings(QWidget):
|
|||
wrappers.remove(wrapper)
|
||||
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
|
||||
if not wrappers:
|
||||
self.wrapper_scroll.setMaximumHeight(self.no_wrapper_label.sizeHint().height())
|
||||
self.widget_stack.setCurrentWidget(self.no_wrapper_label)
|
||||
self.wrapper_label.setVisible(True)
|
||||
|
||||
@pyqtSlot(object, object)
|
||||
def __update_wrapper(self, old: Wrapper, new: Wrapper):
|
||||
|
@ -347,10 +363,7 @@ class WrapperSettings(QWidget):
|
|||
w.deleteLater()
|
||||
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
|
||||
if not wrappers:
|
||||
self.wrapper_scroll.setMaximumHeight(self.no_wrapper_label.sizeHint().height())
|
||||
self.widget_stack.setCurrentWidget(self.no_wrapper_label)
|
||||
else:
|
||||
self.widget_stack.setCurrentWidget(self.wrapper_scroll)
|
||||
self.wrapper_label.setVisible(True)
|
||||
for wrapper in wrappers:
|
||||
self.__add_wrapper(wrapper)
|
||||
|
||||
|
@ -359,15 +372,19 @@ class WrapperContainer(QWidget):
|
|||
# QWidget: moving widget, int: new index
|
||||
orderChanged: pyqtSignal = pyqtSignal(QWidget, int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, label: QLabel, parent=None):
|
||||
super(WrapperContainer, self).__init__(parent=parent)
|
||||
self.setAcceptDrops(True)
|
||||
self.__layout = QHBoxLayout(self)
|
||||
self.__layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.__layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
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__)
|
||||
|
||||
|
|
|
@ -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
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
0
rare/components/tabs/store/api/__init__.py
Normal file
568
rare/components/tabs/store/api/constants/queries.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
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
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
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
0
rare/components/tabs/store/api/models/__init__.py
Normal file
164
rare/components/tabs/store/api/models/diesel.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
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
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
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
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
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
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)
|
|
@ -1,233 +0,0 @@
|
|||
import urllib.parse
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
|
||||
from rare.components.tabs.store.constants import (
|
||||
wishlist_query,
|
||||
search_query,
|
||||
add_to_wishlist_query,
|
||||
remove_from_wishlist_query,
|
||||
)
|
||||
from rare.components.tabs.store.shop_models import BrowseModel
|
||||
from rare.utils.qt_requests import QtRequests
|
||||
|
||||
logger = getLogger("ShopAPICore")
|
||||
graphql_url = "https://www.epicgames.com/graphql"
|
||||
|
||||
|
||||
class ShopApiCore(QObject):
|
||||
update_wishlist = pyqtSignal()
|
||||
|
||||
def __init__(self, auth_token, lc: str, cc: str):
|
||||
super(ShopApiCore, self).__init__()
|
||||
self.token = auth_token
|
||||
self.language_code: str = lc
|
||||
self.country_code: str = cc
|
||||
self.locale = f"{self.language_code}-{self.country_code}"
|
||||
self.manager = QtRequests(parent=self)
|
||||
self.auth_manager = QtRequests(token=auth_token, parent=self)
|
||||
|
||||
self.browse_active = False
|
||||
self.next_browse_request = tuple(())
|
||||
|
||||
def get_free_games(self, handle_func: callable):
|
||||
url = f"https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale={self.language_code}&country={self.country_code}&allowCountries={self.country_code}"
|
||||
|
||||
self.manager.get(url, lambda data: self._handle_free_games(data, handle_func))
|
||||
|
||||
def _handle_free_games(self, data, handle_func):
|
||||
try:
|
||||
results: dict = data["data"]["Catalog"]["searchStore"]["elements"]
|
||||
except KeyError:
|
||||
logger.error("Free games Api request failed")
|
||||
handle_func(["error", "Key error"])
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Free games Api request failed: {e}")
|
||||
handle_func(["error", e])
|
||||
return
|
||||
handle_func(results)
|
||||
|
||||
def get_wishlist(self, handle_func):
|
||||
self.auth_manager.post(
|
||||
graphql_url,
|
||||
lambda data: self._handle_wishlist(data, handle_func),
|
||||
{
|
||||
"query": wishlist_query,
|
||||
"variables": {
|
||||
"country": self.country_code,
|
||||
"locale": f"{self.language_code}-{self.country_code}",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def _handle_wishlist(self, data, handle_func):
|
||||
try:
|
||||
results: list = data["data"]["Wishlist"]["wishlistItems"]["elements"]
|
||||
except KeyError:
|
||||
logger.error("Free games Api request failed")
|
||||
handle_func(["error", "Key error"])
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Free games Api request failed: {e}")
|
||||
handle_func(["error", e])
|
||||
return
|
||||
|
||||
handle_func(results)
|
||||
|
||||
def search_game(self, name, handle_func):
|
||||
payload = {
|
||||
"query": search_query,
|
||||
"variables": {
|
||||
"category": "games/edition/base|bundles/games|editors|software/edition/base",
|
||||
"count": 10,
|
||||
"country": self.country_code,
|
||||
"keywords": name,
|
||||
"locale": self.locale,
|
||||
"sortDir": "DESC",
|
||||
"allowCountries": self.country_code,
|
||||
"start": 0,
|
||||
"tag": "",
|
||||
"withMapping": False,
|
||||
"withPrice": True,
|
||||
},
|
||||
}
|
||||
|
||||
self.manager.post(
|
||||
graphql_url, lambda data: self._handle_search(data, handle_func), payload,
|
||||
)
|
||||
|
||||
def _handle_search(self, data, handle_func):
|
||||
try:
|
||||
handle_func(data["data"]["Catalog"]["searchStore"]["elements"])
|
||||
except KeyError as e:
|
||||
logger.error(str(e))
|
||||
handle_func([])
|
||||
except Exception as e:
|
||||
logger.error(f"Search Api request failed: {e}")
|
||||
handle_func([])
|
||||
return
|
||||
|
||||
def browse_games(self, browse_model: BrowseModel, handle_func):
|
||||
if self.browse_active:
|
||||
self.next_browse_request = (browse_model, handle_func)
|
||||
return
|
||||
self.browse_active = True
|
||||
url = "https://www.epicgames.com/graphql?operationName=searchStoreQuery&variables={}&extensions={}"
|
||||
variables = urllib.parse.quote_plus(str(
|
||||
dict(browse_model.__dict__))
|
||||
)
|
||||
extensions = urllib.parse.quote_plus(str(
|
||||
dict(
|
||||
persistedQuery=dict(
|
||||
version=1,
|
||||
sha256Hash="6e7c4dd0177150eb9a47d624be221929582df8648e7ec271c821838ff4ee148e"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for old, new in [
|
||||
("%26", "&"),
|
||||
("%27", "%22"),
|
||||
("+", ""),
|
||||
("%3A", ":"),
|
||||
("%2C", ","),
|
||||
("%5B", "["),
|
||||
("%5D", "]"),
|
||||
("True", "true"),
|
||||
]:
|
||||
variables = variables.replace(old, new)
|
||||
extensions = extensions.replace(old, new)
|
||||
|
||||
url = url.format(variables, extensions)
|
||||
self.auth_manager.get(
|
||||
url, lambda data: self._handle_browse_games(data, handle_func)
|
||||
)
|
||||
|
||||
def _handle_browse_games(self, data, handle_func):
|
||||
self.browse_active = False
|
||||
if data is None:
|
||||
data = {}
|
||||
if not self.next_browse_request:
|
||||
|
||||
try:
|
||||
handle_func(data["data"]["Catalog"]["searchStore"]["elements"])
|
||||
except KeyError as e:
|
||||
logger.error(str(e))
|
||||
handle_func([])
|
||||
except Exception as e:
|
||||
logger.error(f"Browse games Api request failed: {e}")
|
||||
handle_func([])
|
||||
return
|
||||
else:
|
||||
self.browse_games(*self.next_browse_request) # pylint: disable=E1120
|
||||
self.next_browse_request = tuple(())
|
||||
|
||||
def get_game(self, slug: str, is_bundle: bool, handle_func):
|
||||
url = f"https://store-content.ak.epicgames.com/api/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}"
|
||||
self.manager.get(url, lambda data: self._handle_get_game(data, handle_func))
|
||||
|
||||
def _handle_get_game(self, data, handle_func):
|
||||
try:
|
||||
handle_func(data)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
handle_func({})
|
||||
|
||||
# needs a captcha
|
||||
def add_to_wishlist(self, namespace, offer_id, handle_func: callable):
|
||||
payload = {
|
||||
"variables": {
|
||||
"offerId": offer_id,
|
||||
"namespace": namespace,
|
||||
"country": self.country_code,
|
||||
"locale": self.locale,
|
||||
},
|
||||
"query": add_to_wishlist_query,
|
||||
}
|
||||
self.auth_manager.post(
|
||||
graphql_url,
|
||||
lambda data: self._handle_add_to_wishlist(data, handle_func),
|
||||
payload,
|
||||
)
|
||||
|
||||
def _handle_add_to_wishlist(self, data, handle_func):
|
||||
try:
|
||||
data = data["data"]["Wishlist"]["addToWishlist"]
|
||||
if data["success"]:
|
||||
handle_func(True)
|
||||
else:
|
||||
handle_func(False)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
handle_func(False)
|
||||
self.update_wishlist.emit()
|
||||
|
||||
def remove_from_wishlist(self, namespace, offer_id, handle_func: callable):
|
||||
payload = {
|
||||
"variables": {
|
||||
"offerId": offer_id,
|
||||
"namespace": namespace,
|
||||
"operation": "REMOVE",
|
||||
},
|
||||
"query": remove_from_wishlist_query,
|
||||
}
|
||||
self.auth_manager.post(
|
||||
graphql_url,
|
||||
lambda data: self._handle_remove_from_wishlist(data, handle_func),
|
||||
payload,
|
||||
)
|
||||
|
||||
def _handle_remove_from_wishlist(self, data, handle_func):
|
||||
try:
|
||||
data = data["data"]["Wishlist"]["removeFromWishlist"]
|
||||
if data["success"]:
|
||||
handle_func(True)
|
||||
else:
|
||||
handle_func(False)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
handle_func(False)
|
||||
self.update_wishlist.emit()
|
|
@ -1,184 +0,0 @@
|
|||
import datetime
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class ImageUrlModel:
|
||||
def __init__(
|
||||
self,
|
||||
front_tall: str = "",
|
||||
offer_image_tall: str = "",
|
||||
thumbnail: str = "",
|
||||
front_wide: str = "",
|
||||
offer_image_wide: str = "",
|
||||
product_logo: str = "",
|
||||
):
|
||||
self.front_tall = front_tall
|
||||
self.offer_image_tall = offer_image_tall
|
||||
self.thumbnail = thumbnail
|
||||
self.front_wide = front_wide
|
||||
self.offer_image_wide = offer_image_wide
|
||||
self.product_logo = product_logo
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data: list):
|
||||
tmp = cls()
|
||||
for item in json_data:
|
||||
if item["type"] == "Thumbnail":
|
||||
tmp.thumbnail = item["url"]
|
||||
elif item["type"] == "DieselStoreFrontTall":
|
||||
tmp.front_tall = item["url"]
|
||||
elif item["type"] == "DieselStoreFrontWide":
|
||||
tmp.front_wide = item["url"]
|
||||
elif item["type"] == "OfferImageTall":
|
||||
tmp.offer_image_tall = item["url"]
|
||||
elif item["type"] == "OfferImageWide":
|
||||
tmp.offer_image_wide = item["url"]
|
||||
elif item["type"] == "ProductLogo":
|
||||
tmp.product_logo = item["url"]
|
||||
return tmp
|
||||
|
||||
|
||||
class ShopGame:
|
||||
# TODO: Copyrights etc
|
||||
def __init__(
|
||||
self,
|
||||
title: str = "",
|
||||
image_urls: ImageUrlModel = None,
|
||||
social_links: dict = None,
|
||||
langs: list = None,
|
||||
reqs: dict = None,
|
||||
publisher: str = "",
|
||||
developer: str = "",
|
||||
original_price: str = "",
|
||||
discount_price: str = "",
|
||||
tags: list = None,
|
||||
namespace: str = "",
|
||||
offer_id: str = "",
|
||||
):
|
||||
self.title = title
|
||||
self.image_urls = image_urls
|
||||
self.links = []
|
||||
if social_links:
|
||||
for item in social_links:
|
||||
if item.startswith("link"):
|
||||
self.links.append(
|
||||
tuple((item.replace("link", ""), social_links[item]))
|
||||
)
|
||||
else:
|
||||
self.links = []
|
||||
self.languages = langs
|
||||
self.reqs = reqs
|
||||
self.publisher = publisher
|
||||
self.developer = developer
|
||||
self.price = original_price
|
||||
self.discount_price = discount_price
|
||||
self.tags = tags
|
||||
self.namespace = namespace
|
||||
self.offer_id = offer_id
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, api_data: dict, search_data: dict):
|
||||
if isinstance(api_data, list):
|
||||
for product in api_data:
|
||||
if product["_title"] == "home":
|
||||
api_data = product
|
||||
break
|
||||
if "pages" in api_data.keys():
|
||||
for page in api_data["pages"]:
|
||||
if page["_slug"] == "home":
|
||||
api_data = page
|
||||
break
|
||||
tmp = cls()
|
||||
tmp.title = search_data.get("title", "Fail")
|
||||
tmp.image_urls = ImageUrlModel.from_json(search_data["keyImages"])
|
||||
links = api_data["data"]["socialLinks"]
|
||||
tmp.links = []
|
||||
for item in links:
|
||||
if item.startswith("link"):
|
||||
tmp.links.append(tuple((item.replace("link", ""), links[item])))
|
||||
tmp.available_voice_langs = api_data["data"]["requirements"].get(
|
||||
"languages", "Failed"
|
||||
)
|
||||
tmp.reqs = {}
|
||||
for i, system in enumerate(api_data["data"]["requirements"].get("systems", [])):
|
||||
try:
|
||||
tmp.reqs[system["systemType"]] = {}
|
||||
except KeyError:
|
||||
continue
|
||||
for req in system["details"]:
|
||||
try:
|
||||
tmp.reqs[system["systemType"]][req["title"]] = (
|
||||
req["minimum"],
|
||||
req["recommended"],
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
tmp.publisher = api_data["data"]["meta"].get("publisher", "")
|
||||
tmp.developer = api_data["data"]["meta"].get("developer", "")
|
||||
if not tmp.developer:
|
||||
for i in search_data["customAttributes"]:
|
||||
if i["key"] == "developerName":
|
||||
tmp.developer = i["value"]
|
||||
tmp.price = search_data["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
|
||||
tmp.discount_price = search_data["price"]["totalPrice"]["fmtPrice"][
|
||||
"discountPrice"
|
||||
]
|
||||
tmp.tags = [
|
||||
i.replace("_", " ").capitalize()
|
||||
for i in api_data["data"]["meta"].get("tags", [])
|
||||
]
|
||||
tmp.namespace = search_data["namespace"]
|
||||
tmp.offer_id = search_data["id"]
|
||||
|
||||
return tmp
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrowseModel:
|
||||
category: str = "games/edition/base|bundles/games|editors|software/edition/base"
|
||||
count: int = 30
|
||||
language_code: str = "en"
|
||||
country_code: str = "US"
|
||||
keywords: str = ""
|
||||
sortDir: str = "DESC"
|
||||
start: int = 0
|
||||
tag: str = ""
|
||||
withMapping: bool = True
|
||||
withPrice: bool = True
|
||||
date: str = (
|
||||
f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%H:%M:%S')}.420Z]"
|
||||
)
|
||||
price: str = ""
|
||||
onSale: bool = False
|
||||
|
||||
@property
|
||||
def __dict__(self):
|
||||
payload = {
|
||||
"allowCountries": self.country_code,
|
||||
"category": self.category,
|
||||
"count": self.count,
|
||||
"country": self.country_code,
|
||||
"keywords": self.keywords,
|
||||
"locale": self.language_code,
|
||||
"priceRange": self.price,
|
||||
"releaseDate": self.date,
|
||||
"sortBy": "releaseDate",
|
||||
"sortDir": self.sortDir,
|
||||
"start": self.start,
|
||||
"tag": self.tag,
|
||||
"withPrice": self.withPrice,
|
||||
}
|
||||
if self.price == "free":
|
||||
payload["freeGame"] = True
|
||||
payload.pop("priceRange")
|
||||
elif self.price.startswith("<price>"):
|
||||
payload["priceRange"] = self.price.replace("<price>", "")
|
||||
if self.onSale:
|
||||
payload["onSale"] = True
|
||||
|
||||
if self.price:
|
||||
payload["effectiveDate"] = self.date
|
||||
else:
|
||||
payload.pop("priceRange")
|
||||
|
||||
return payload
|
|
@ -1,367 +0,0 @@
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import (
|
||||
QGroupBox,
|
||||
QScrollArea,
|
||||
QCheckBox,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QHBoxLayout,
|
||||
)
|
||||
|
||||
from legendary.core import LegendaryCore
|
||||
from rare.ui.components.tabs.store.store import Ui_ShopWidget
|
||||
from rare.utils.extra_widgets import WaitingSpinner, ButtonLineEdit
|
||||
from rare.widgets.flow_layout import FlowLayout
|
||||
from .constants import Constants
|
||||
from .game_widgets import GameWidget
|
||||
from .shop_api_core import ShopApiCore
|
||||
from .shop_models import BrowseModel
|
||||
|
||||
logger = logging.getLogger("Shop")
|
||||
|
||||
|
||||
# noinspection PyAttributeOutsideInit,PyBroadException
|
||||
class ShopWidget(QScrollArea, Ui_ShopWidget):
|
||||
show_info = pyqtSignal(str)
|
||||
show_game = pyqtSignal(dict)
|
||||
free_game_widgets = []
|
||||
active_search_request = False
|
||||
next_search = ""
|
||||
wishlist: list = []
|
||||
|
||||
def __init__(self, path, core: LegendaryCore, shop_api: ShopApiCore):
|
||||
super(ShopWidget, self).__init__()
|
||||
self.setWidgetResizable(True)
|
||||
self.setupUi(self)
|
||||
self.path = path
|
||||
self.core = core
|
||||
self.api_core = shop_api
|
||||
self.price = ""
|
||||
self.tags = []
|
||||
self.types = []
|
||||
self.update_games_allowed = True
|
||||
self.free_widget.setLayout(FlowLayout())
|
||||
|
||||
self.free_stack.addWidget(WaitingSpinner())
|
||||
self.free_stack.setCurrentIndex(1)
|
||||
|
||||
self.discount_widget.setLayout(FlowLayout())
|
||||
self.discount_stack.addWidget(WaitingSpinner())
|
||||
self.discount_stack.setCurrentIndex(1)
|
||||
|
||||
self.game_widget.setLayout(FlowLayout())
|
||||
self.game_stack.addWidget(WaitingSpinner())
|
||||
self.game_stack.setCurrentIndex(1)
|
||||
|
||||
self.search_bar = ButtonLineEdit(
|
||||
"fa.search", placeholder_text=self.tr("Search Games")
|
||||
)
|
||||
self.layout().insertWidget(0, self.search_bar)
|
||||
|
||||
# self.search_bar.textChanged.connect(self.search_games)
|
||||
|
||||
self.search_bar.returnPressed.connect(self.show_search_results)
|
||||
self.search_bar.buttonClicked.connect(self.show_search_results)
|
||||
|
||||
self.init_filter()
|
||||
|
||||
self.search_bar.setHidden(True)
|
||||
self.filter_gb.setHidden(True)
|
||||
self.filter_game_gb.setHidden(True)
|
||||
|
||||
# self.search_bar.textChanged.connect(self.load_completer)
|
||||
|
||||
def load(self):
|
||||
# load free games
|
||||
self.api_core.get_free_games(self.add_free_games)
|
||||
# load wishlist
|
||||
self.api_core.get_wishlist(self.add_wishlist_items)
|
||||
# load browse games
|
||||
self.prepare_request()
|
||||
|
||||
def update_wishlist(self):
|
||||
self.api_core.get_wishlist(self.add_wishlist_items)
|
||||
|
||||
def add_wishlist_items(self, wishlist):
|
||||
for i in range(self.discount_widget.layout().count()):
|
||||
item = self.discount_widget.layout().itemAt(i)
|
||||
if item:
|
||||
item.widget().deleteLater()
|
||||
|
||||
if wishlist and wishlist[0] == "error":
|
||||
self.discount_widget.layout().addWidget(
|
||||
QLabel(self.tr("Failed to get wishlist: {}").format(wishlist[1]))
|
||||
)
|
||||
btn = QPushButton(self.tr("Reload"))
|
||||
self.discount_widget.layout().addWidget(btn)
|
||||
btn.clicked.connect(
|
||||
lambda: self.api_core.get_wishlist(self.add_wishlist_items)
|
||||
)
|
||||
self.discount_stack.setCurrentIndex(0)
|
||||
return
|
||||
|
||||
discounts = 0
|
||||
for game in wishlist:
|
||||
if not game:
|
||||
continue
|
||||
try:
|
||||
if game["offer"]["price"]["totalPrice"]["discount"] > 0:
|
||||
w = GameWidget(self.path, game["offer"])
|
||||
w.show_info.connect(self.show_game.emit)
|
||||
self.discount_widget.layout().addWidget(w)
|
||||
discounts += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"{game} {e}")
|
||||
continue
|
||||
self.discounts_gb.setVisible(discounts > 0)
|
||||
self.discount_stack.setCurrentIndex(0)
|
||||
# fix widget overlay
|
||||
self.discount_widget.layout().update()
|
||||
|
||||
def add_free_games(self, free_games: list):
|
||||
for i in range(self.free_widget.layout().count()):
|
||||
item = self.free_widget.layout().itemAt(i)
|
||||
if item:
|
||||
item.widget().deleteLater()
|
||||
|
||||
if free_games and free_games[0] == "error":
|
||||
self.free_widget.layout().addWidget(
|
||||
QLabel(self.tr("Failed to fetch free games: {}").format(free_games[1]))
|
||||
)
|
||||
btn = QPushButton(self.tr("Reload"))
|
||||
self.free_widget.layout().addWidget(btn)
|
||||
btn.clicked.connect(
|
||||
lambda: self.api_core.get_free_games(self.add_free_games)
|
||||
)
|
||||
self.free_stack.setCurrentIndex(0)
|
||||
return
|
||||
|
||||
self.free_games_now = QGroupBox(self.tr("Now Free"))
|
||||
self.free_games_now.setLayout(QHBoxLayout())
|
||||
self.free_widget.layout().addWidget(self.free_games_now)
|
||||
|
||||
self.coming_free_games = QGroupBox(self.tr("Free Games next week"))
|
||||
self.coming_free_games.setLayout(QHBoxLayout())
|
||||
self.free_widget.layout().addWidget(self.coming_free_games)
|
||||
|
||||
date = datetime.datetime.now()
|
||||
free_games_now = []
|
||||
coming_free_games = []
|
||||
for game in free_games:
|
||||
try:
|
||||
if (
|
||||
game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] == "0"
|
||||
and game["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
|
||||
!= game["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
|
||||
):
|
||||
free_games_now.append(game)
|
||||
continue
|
||||
|
||||
if game["title"] == "Mystery Game":
|
||||
coming_free_games.append(game)
|
||||
continue
|
||||
except KeyError as e:
|
||||
logger.warning(str(e))
|
||||
|
||||
try:
|
||||
# parse datetime to check if game is next week or now
|
||||
try:
|
||||
start_date = datetime.datetime.strptime(
|
||||
game["promotions"]["upcomingPromotionalOffers"][0][
|
||||
"promotionalOffers"
|
||||
][0]["startDate"],
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ",
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
start_date = datetime.datetime.strptime(
|
||||
game["promotions"]["promotionalOffers"][0][
|
||||
"promotionalOffers"
|
||||
][0]["startDate"],
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ",
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
continue
|
||||
|
||||
except TypeError:
|
||||
print("type error")
|
||||
continue
|
||||
|
||||
if start_date > date:
|
||||
coming_free_games.append(game)
|
||||
# free games now
|
||||
now_free = 0
|
||||
for free_game in free_games_now:
|
||||
w = GameWidget(self.path, free_game)
|
||||
w.show_info.connect(self.show_game.emit)
|
||||
self.free_games_now.layout().addWidget(w)
|
||||
self.free_game_widgets.append(w)
|
||||
now_free += 1
|
||||
if now_free == 0:
|
||||
self.free_games_now.layout().addWidget(
|
||||
QLabel(self.tr("Could not find current free game"))
|
||||
)
|
||||
|
||||
# free games next week
|
||||
for free_game in coming_free_games:
|
||||
w = GameWidget(self.path, free_game)
|
||||
if free_game["title"] != "Mystery Game":
|
||||
w.show_info.connect(self.show_game.emit)
|
||||
self.coming_free_games.layout().addWidget(w)
|
||||
# self.coming_free_games.setFixedWidth(int(40 + len(coming_free_games) * 300))
|
||||
self.free_stack.setCurrentIndex(0)
|
||||
|
||||
def show_search_results(self):
|
||||
if self.search_bar.text():
|
||||
self.show_info.emit(self.search_bar.text())
|
||||
|
||||
def init_filter(self):
|
||||
self.none_price.toggled.connect(
|
||||
lambda: self.prepare_request("") if self.none_price.isChecked() else None
|
||||
)
|
||||
self.free_button.toggled.connect(
|
||||
lambda: self.prepare_request("free")
|
||||
if self.free_button.isChecked()
|
||||
else None
|
||||
)
|
||||
self.under10.toggled.connect(
|
||||
lambda: self.prepare_request("<price>[0, 1000)")
|
||||
if self.under10.isChecked()
|
||||
else None
|
||||
)
|
||||
self.under20.toggled.connect(
|
||||
lambda: self.prepare_request("<price>[0, 2000)")
|
||||
if self.under20.isChecked()
|
||||
else None
|
||||
)
|
||||
self.under30.toggled.connect(
|
||||
lambda: self.prepare_request("<price>[0, 3000)")
|
||||
if self.under30.isChecked()
|
||||
else None
|
||||
)
|
||||
self.above.toggled.connect(
|
||||
lambda: self.prepare_request("<price>[1499,]")
|
||||
if self.above.isChecked()
|
||||
else None
|
||||
)
|
||||
# self.on_discount.toggled.connect(lambda: self.prepare_request("sale") if self.on_discount.isChecked() else None)
|
||||
self.on_discount.toggled.connect(lambda: self.prepare_request())
|
||||
constants = Constants()
|
||||
|
||||
self.checkboxes = []
|
||||
|
||||
for groupbox, variables in [
|
||||
(self.genre_gb, constants.categories),
|
||||
(self.platform_gb, constants.platforms),
|
||||
(self.others_gb, constants.others),
|
||||
(self.type_gb, 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.reset_button.clicked.connect(self.reset_filters)
|
||||
|
||||
def reset_filters(self):
|
||||
self.update_games_allowed = False
|
||||
for cb in self.checkboxes:
|
||||
cb.setChecked(False)
|
||||
self.none_price.setChecked(True)
|
||||
|
||||
self.tags = []
|
||||
self.types = []
|
||||
self.update_games_allowed = True
|
||||
self.prepare_request("")
|
||||
|
||||
self.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.on_discount.isChecked():
|
||||
self.free_game_group_box.setVisible(False)
|
||||
self.discounts_gb.setVisible(False)
|
||||
else:
|
||||
self.free_game_group_box.setVisible(True)
|
||||
if len(self.discounts_gb.layout().children()) > 0:
|
||||
self.discounts_gb.setVisible(True)
|
||||
|
||||
self.game_stack.setCurrentIndex(1)
|
||||
|
||||
browse_model = BrowseModel(
|
||||
language_code=self.core.language_code,
|
||||
country_code=self.core.country_code,
|
||||
count=20,
|
||||
price=self.price,
|
||||
onSale=self.on_discount.isChecked(),
|
||||
)
|
||||
browse_model.tag = "|".join(self.tags)
|
||||
|
||||
if self.types:
|
||||
browse_model.category = "|".join(self.types)
|
||||
self.api_core.browse_games(browse_model, self.show_games)
|
||||
|
||||
def show_games(self, data):
|
||||
for item in (
|
||||
self.game_widget.layout().itemAt(i)
|
||||
for i in range(self.game_widget.layout().count())
|
||||
):
|
||||
item.widget().deleteLater()
|
||||
if data:
|
||||
for game in data:
|
||||
w = GameWidget(self.path, game, 275)
|
||||
self.game_widget.layout().addWidget(w)
|
||||
w.show_info.connect(self.show_game.emit)
|
||||
|
||||
else:
|
||||
self.game_widget.layout().addWidget(
|
||||
QLabel(self.tr("Could not get games matching the filter"))
|
||||
)
|
||||
self.game_stack.setCurrentIndex(0)
|
||||
|
||||
self.game_widget.layout().update()
|
||||
|
||||
|
||||
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)
|
258
rare/components/tabs/store/store_api.py
Normal file
258
rare/components/tabs/store/store_api.py
Normal file
|
@ -0,0 +1,258 @@
|
|||
from logging import getLogger
|
||||
from typing import List, Callable
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from rare.components.tabs.store.constants import (
|
||||
wishlist_query,
|
||||
search_query,
|
||||
wishlist_add_query,
|
||||
wishlist_remove_query,
|
||||
)
|
||||
from rare.utils.paths import cache_dir
|
||||
from rare.utils.qt_requests import QtRequests
|
||||
from .api.models.query import SearchStoreQuery
|
||||
from .api.models.diesel import DieselProduct
|
||||
from .api.models.response import (
|
||||
ResponseModel,
|
||||
CatalogOfferModel,
|
||||
)
|
||||
|
||||
logger = getLogger("StoreAPI")
|
||||
graphql_url = "https://graphql.epicgames.com/graphql"
|
||||
|
||||
|
||||
DEBUG: Callable[[], bool] = lambda: "--debug" in QApplication.arguments()
|
||||
|
||||
|
||||
class StoreAPI(QObject):
|
||||
update_wishlist = pyqtSignal()
|
||||
|
||||
def __init__(self, token, language: str, country: str, installed):
|
||||
super(StoreAPI, self).__init__()
|
||||
self.token = token
|
||||
self.language_code: str = language
|
||||
self.country_code: str = country
|
||||
self.locale = f"{self.language_code}-{self.country_code}"
|
||||
self.locale = "en-US"
|
||||
self.manager = QtRequests(parent=self)
|
||||
self.authed_manager = QtRequests(token=token, parent=self)
|
||||
self.cached_manager = QtRequests(cache=str(cache_dir().joinpath("store")), parent=self)
|
||||
|
||||
self.installed = installed
|
||||
|
||||
self.browse_active = False
|
||||
self.next_browse_request = tuple(())
|
||||
|
||||
def get_free(self, handle_func: callable):
|
||||
url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions"
|
||||
params = {
|
||||
"locale": self.locale,
|
||||
"country": self.country_code,
|
||||
"allowCountries": self.country_code,
|
||||
}
|
||||
self.manager.get(url, lambda data: self.__handle_free_games(data, handle_func), params=params)
|
||||
|
||||
@staticmethod
|
||||
def __handle_free_games(data, handle_func):
|
||||
try:
|
||||
response = ResponseModel.from_dict(data)
|
||||
results: List[CatalogOfferModel] = response.data.catalog.searchStore.elements
|
||||
handle_func(results)
|
||||
except KeyError as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.error("Free games Api request failed")
|
||||
handle_func(["error", "Key error"])
|
||||
return
|
||||
except Exception as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.error(f"Free games Api request failed: {e}")
|
||||
handle_func(["error", e])
|
||||
return
|
||||
|
||||
def get_wishlist(self, handle_func):
|
||||
self.authed_manager.post(
|
||||
graphql_url,
|
||||
lambda data: self.__handle_wishlist(data, handle_func),
|
||||
{
|
||||
"query": wishlist_query,
|
||||
"variables": {
|
||||
"country": self.country_code,
|
||||
"locale": self.locale,
|
||||
"withPrice": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __handle_wishlist(data, handle_func):
|
||||
try:
|
||||
response = ResponseModel.from_dict(data)
|
||||
if response.errors:
|
||||
logger.error(response.errors)
|
||||
handle_func(response.data.wishlist.wishlistItems.elements)
|
||||
except KeyError as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.exception("Free games API request failed")
|
||||
handle_func(["error", "Key error"])
|
||||
return
|
||||
except Exception as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.exception(f"Free games API request failed")
|
||||
handle_func(["error", e])
|
||||
return
|
||||
|
||||
def search_game(self, name, handler):
|
||||
payload = {
|
||||
"query": search_query,
|
||||
"variables": {
|
||||
"category": "games/edition/base|bundles/games|editors|software/edition/base",
|
||||
"count": 20,
|
||||
"country": self.country_code,
|
||||
"keywords": name,
|
||||
"locale": self.locale,
|
||||
"sortDir": "DESC",
|
||||
"allowCountries": self.country_code,
|
||||
"start": 0,
|
||||
"tag": "",
|
||||
"withMapping": False,
|
||||
"withPrice": True,
|
||||
},
|
||||
}
|
||||
|
||||
self.manager.post(graphql_url, lambda data: self.__handle_search(data, handler), payload)
|
||||
|
||||
@staticmethod
|
||||
def __handle_search(data, handler):
|
||||
try:
|
||||
response = ResponseModel.from_dict(data)
|
||||
handler(response.data.catalog.searchStore.elements)
|
||||
except KeyError as e:
|
||||
logger.error(str(e))
|
||||
if DEBUG():
|
||||
raise e
|
||||
handler([])
|
||||
except Exception as e:
|
||||
logger.error(f"Search Api request failed: {e}")
|
||||
if DEBUG():
|
||||
raise e
|
||||
handler([])
|
||||
return
|
||||
|
||||
def browse_games(self, browse_model: SearchStoreQuery, handle_func):
|
||||
if self.browse_active:
|
||||
self.next_browse_request = (browse_model, handle_func)
|
||||
return
|
||||
self.browse_active = True
|
||||
payload = {
|
||||
"query": search_query,
|
||||
"variables": browse_model.to_dict()
|
||||
}
|
||||
self.manager.post(graphql_url, lambda data: self.__handle_browse_games(data, handle_func), payload)
|
||||
|
||||
def __handle_browse_games(self, data, handle_func):
|
||||
self.browse_active = False
|
||||
if data is None:
|
||||
data = {}
|
||||
if not self.next_browse_request:
|
||||
try:
|
||||
response = ResponseModel.from_dict(data)
|
||||
handle_func(response.data.catalog.searchStore.elements)
|
||||
except KeyError as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.error(str(e))
|
||||
handle_func([])
|
||||
except Exception as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.error(f"Browse games Api request failed: {e}")
|
||||
handle_func([])
|
||||
return
|
||||
else:
|
||||
self.browse_games(*self.next_browse_request) # pylint: disable=E1120
|
||||
self.next_browse_request = tuple(())
|
||||
|
||||
# def get_game_config_graphql(self, namespace: str, handle_func):
|
||||
# payload = {
|
||||
# "query": config_query,
|
||||
# "variables": {
|
||||
# "namespace": namespace
|
||||
# }
|
||||
# }
|
||||
|
||||
def __make_graphql_query(self):
|
||||
pass
|
||||
|
||||
def __make_api_query(self):
|
||||
pass
|
||||
|
||||
def get_game_config_cms(self, slug: str, is_bundle: bool, handle_func):
|
||||
url = "https://store-content.ak.epicgames.com/api"
|
||||
url += f"/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}"
|
||||
self.manager.get(url, lambda data: self.__handle_get_game(data, handle_func))
|
||||
|
||||
@staticmethod
|
||||
def __handle_get_game(data, handle_func):
|
||||
try:
|
||||
product = DieselProduct.from_dict(data)
|
||||
handle_func(product)
|
||||
except Exception as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.error(str(e))
|
||||
# handle_func({})
|
||||
|
||||
# needs a captcha
|
||||
def add_to_wishlist(self, namespace, offer_id, handle_func: callable):
|
||||
payload = {
|
||||
"query": wishlist_add_query,
|
||||
"variables": {
|
||||
"offerId": offer_id,
|
||||
"namespace": namespace,
|
||||
"country": self.country_code,
|
||||
"locale": self.locale,
|
||||
},
|
||||
}
|
||||
self.authed_manager.post(graphql_url, lambda data: self._handle_add_to_wishlist(data, handle_func), payload)
|
||||
|
||||
def _handle_add_to_wishlist(self, data, handle_func):
|
||||
try:
|
||||
response = ResponseModel.from_dict(data)
|
||||
data = response.data.wishlist.addToWishlist
|
||||
handle_func(data.success)
|
||||
except Exception as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.error(str(e))
|
||||
handle_func(False)
|
||||
self.update_wishlist.emit()
|
||||
|
||||
def remove_from_wishlist(self, namespace, offer_id, handle_func: callable):
|
||||
payload = {
|
||||
"query": wishlist_remove_query,
|
||||
"variables": {
|
||||
"offerId": offer_id,
|
||||
"namespace": namespace,
|
||||
"operation": "REMOVE",
|
||||
},
|
||||
}
|
||||
self.authed_manager.post(graphql_url, lambda data: self._handle_remove_from_wishlist(data, handle_func),
|
||||
payload)
|
||||
|
||||
def _handle_remove_from_wishlist(self, data, handle_func):
|
||||
try:
|
||||
response = ResponseModel.from_dict(data)
|
||||
data = response.data.wishlist.removeFromWishlist
|
||||
handle_func(data.success)
|
||||
except Exception as e:
|
||||
if DEBUG():
|
||||
raise e
|
||||
logger.error(str(e))
|
||||
handle_func(False)
|
||||
self.update_wishlist.emit()
|
0
rare/components/tabs/store/widgets/__init__.py
Normal file
0
rare/components/tabs/store/widgets/__init__.py
Normal file
269
rare/components/tabs/store/widgets/details.py
Normal file
269
rare/components/tabs/store/widgets/details.py
Normal file
|
@ -0,0 +1,269 @@
|
|||
import logging
|
||||
from typing import List, Dict
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl, pyqtSignal
|
||||
from PyQt5.QtGui import QFont, QDesktopServices, QKeyEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QGridLayout,
|
||||
QSizePolicy,
|
||||
)
|
||||
|
||||
from rare.components.tabs.store.api.models.diesel import DieselProduct, DieselProductDetail, DieselSystemDetail
|
||||
from rare.components.tabs.store.api.models.response import CatalogOfferModel
|
||||
from rare.components.tabs.store.store_api import StoreAPI
|
||||
from rare.models.image import ImageSize
|
||||
from rare.ui.components.tabs.store.details import Ui_StoreDetailsWidget
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
from rare.widgets.side_tab import SideTabWidget, SideTabContents
|
||||
from .image import LoadingImageWidget
|
||||
|
||||
logger = logging.getLogger("StoreDetails")
|
||||
|
||||
|
||||
class StoreDetailsWidget(QWidget, SideTabContents):
|
||||
back_clicked: pyqtSignal = pyqtSignal()
|
||||
|
||||
# TODO Design
|
||||
def __init__(self, installed: List, store_api: StoreAPI, parent=None):
|
||||
super(StoreDetailsWidget, self).__init__(parent=parent)
|
||||
self.implements_scrollarea = True
|
||||
|
||||
self.ui = Ui_StoreDetailsWidget()
|
||||
self.ui.setupUi(self)
|
||||
self.ui.main_layout.setContentsMargins(0, 0, 3, 0)
|
||||
|
||||
self.store_api = store_api
|
||||
self.installed = installed
|
||||
self.catalog_offer: CatalogOfferModel = None
|
||||
|
||||
self.image = LoadingImageWidget(store_api.cached_manager, self)
|
||||
self.image.setFixedSize(ImageSize.Display)
|
||||
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop)
|
||||
self.ui.left_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.ui.wishlist_button.clicked.connect(self.add_to_wishlist)
|
||||
self.ui.store_button.clicked.connect(self.button_clicked)
|
||||
self.ui.wishlist_button.setVisible(True)
|
||||
self.in_wishlist = False
|
||||
self.wishlist = []
|
||||
|
||||
self.requirements_tabs = SideTabWidget(parent=self.ui.requirements_frame)
|
||||
self.requirements_tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.ui.requirements_layout.addWidget(self.requirements_tabs)
|
||||
|
||||
self.ui.back_button.setIcon(qta_icon("fa.chevron-left"))
|
||||
self.ui.back_button.clicked.connect(self.back_clicked)
|
||||
|
||||
self.setDisabled(False)
|
||||
|
||||
def handle_wishlist_update(self, wishlist: List[CatalogOfferModel]):
|
||||
if wishlist and wishlist[0] == "error":
|
||||
return
|
||||
self.wishlist = [game.id for game in wishlist]
|
||||
if self.id_str in self.wishlist:
|
||||
self.in_wishlist = True
|
||||
self.ui.wishlist_button.setText(self.tr("Remove from Wishlist"))
|
||||
else:
|
||||
self.in_wishlist = False
|
||||
|
||||
def update_game(self, offer: CatalogOfferModel):
|
||||
self.ui.title.setText(offer.title)
|
||||
self.title_str = offer.title
|
||||
self.id_str = offer.id
|
||||
self.store_api.get_wishlist(self.handle_wishlist_update)
|
||||
|
||||
# lk: delete tabs in reverse order because indices are updated on deletion
|
||||
while self.requirements_tabs.count():
|
||||
self.requirements_tabs.widget(0).deleteLater()
|
||||
self.requirements_tabs.removeTab(0)
|
||||
self.requirements_tabs.clear()
|
||||
|
||||
slug = offer.productSlug
|
||||
if not slug:
|
||||
for mapping in offer.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 offer.namespace in self.installed:
|
||||
self.ui.store_button.setText(self.tr("Show Game on Epic Page"))
|
||||
self.ui.status.setVisible(True)
|
||||
else:
|
||||
self.ui.store_button.setText(self.tr("Buy Game in Epic Games Store"))
|
||||
self.ui.status.setVisible(False)
|
||||
|
||||
self.ui.original_price.setText(self.tr("Loading"))
|
||||
# self.title.setText(self.tr("Loading"))
|
||||
# self.image.setPixmap(QPixmap())
|
||||
is_bundle = False
|
||||
for i in offer.categories:
|
||||
if "bundles" in i.get("path", ""):
|
||||
is_bundle = True
|
||||
|
||||
# init API request
|
||||
if slug:
|
||||
self.store_api.get_game_config_cms(offer.productSlug, is_bundle, self.data_received)
|
||||
# else:
|
||||
# self.data_received({})
|
||||
self.catalog_offer = offer
|
||||
|
||||
def add_to_wishlist(self):
|
||||
if not self.in_wishlist:
|
||||
self.store_api.add_to_wishlist(
|
||||
self.catalog_offer.namespace,
|
||||
self.catalog_offer.id,
|
||||
lambda success: self.ui.wishlist_button.setText(self.tr("Remove from wishlist"))
|
||||
if success
|
||||
else self.ui.wishlist_button.setText("Something went wrong")
|
||||
)
|
||||
else:
|
||||
self.store_api.remove_from_wishlist(
|
||||
self.catalog_offer.namespace,
|
||||
self.catalog_offer.id,
|
||||
lambda success: self.ui.wishlist_button.setText(self.tr("Add to wishlist"))
|
||||
if success
|
||||
else self.ui.wishlist_button.setText("Something went wrong"),
|
||||
)
|
||||
|
||||
def data_received(self, product: DieselProduct):
|
||||
try:
|
||||
if product.pages:
|
||||
product_data: DieselProductDetail = product.pages[0].data
|
||||
else:
|
||||
product_data: DieselProductDetail = product.data
|
||||
except Exception as e:
|
||||
raise e
|
||||
logger.error(str(e))
|
||||
|
||||
self.ui.original_price.setFont(self.font())
|
||||
price = self.catalog_offer.price.totalPrice.fmtPrice["originalPrice"]
|
||||
discount_price = self.catalog_offer.price.totalPrice.fmtPrice["discountPrice"]
|
||||
if price == "0" or price == 0:
|
||||
self.ui.original_price.setText(self.tr("Free"))
|
||||
else:
|
||||
self.ui.original_price.setText(price)
|
||||
if price != discount_price:
|
||||
font = self.font()
|
||||
font.setStrikeOut(True)
|
||||
self.ui.original_price.setFont(font)
|
||||
self.ui.discount_price.setText(
|
||||
discount_price
|
||||
if discount_price != "0"
|
||||
else self.tr("Free")
|
||||
)
|
||||
self.ui.discount_price.setVisible(True)
|
||||
else:
|
||||
self.ui.discount_price.setVisible(False)
|
||||
|
||||
requirements = product_data.requirements
|
||||
if requirements and requirements.systems:
|
||||
for system in requirements.systems:
|
||||
req_widget = RequirementsWidget(system, self.requirements_tabs)
|
||||
self.requirements_tabs.addTab(req_widget, system.systemType)
|
||||
self.ui.requirements_frame.setVisible(True)
|
||||
else:
|
||||
self.ui.requirements_frame.setVisible(False)
|
||||
|
||||
key_images = self.catalog_offer.keyImages
|
||||
img_url = key_images.for_dimensions(self.image.size().width(), self.image.size().height())
|
||||
self.image.fetchPixmap(img_url.url)
|
||||
|
||||
# self.image_stack.setCurrentIndex(0)
|
||||
about = product_data.about
|
||||
self.ui.description_label.setMarkdown(about.desciption)
|
||||
self.ui.developer.setText(about.developerAttribution)
|
||||
# try:
|
||||
# if isinstance(aboudeveloper, list):
|
||||
# self.ui.dev.setText(", ".join(self.game.developer))
|
||||
# else:
|
||||
# self.ui.dev.setText(self.game.developer)
|
||||
# except KeyError:
|
||||
# pass
|
||||
tags = product_data.unmapped["meta"].get("tags", [])
|
||||
self.ui.tags.setText(", ".join(tags))
|
||||
|
||||
# clear Layout
|
||||
for b in self.ui.social_links.findChildren(SocialButton, options=Qt.FindDirectChildrenOnly):
|
||||
self.ui.social_links_layout.removeWidget(b)
|
||||
b.deleteLater()
|
||||
|
||||
links = product_data.socialLinks
|
||||
link_count = 0
|
||||
for name, url in links.items():
|
||||
if name == "_type":
|
||||
continue
|
||||
name = name.replace("link", "").lower()
|
||||
if name == "homepage":
|
||||
icn = qta_icon("mdi.web", "fa.search", scale_factor=1.5)
|
||||
else:
|
||||
try:
|
||||
icn = qta_icon(f"mdi.{name}", f"fa.{name}", scale_factor=1.5)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
continue
|
||||
|
||||
button = SocialButton(icn, url, parent=self.ui.social_links)
|
||||
self.ui.social_links_layout.addWidget(button)
|
||||
link_count += 1
|
||||
|
||||
self.ui.social_links.setEnabled(bool(link_count))
|
||||
|
||||
self.setEnabled(True)
|
||||
|
||||
# def add_wishlist_items(self, wishlist: List[CatalogGameModel]):
|
||||
# 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.store_api.language_code}/p/{self.slug}"))
|
||||
|
||||
def keyPressEvent(self, a0: QKeyEvent):
|
||||
if a0.key() == Qt.Key_Escape:
|
||||
self.back_clicked.emit()
|
||||
|
||||
|
||||
class SocialButton(QPushButton):
|
||||
def __init__(self, icn, url, parent=None):
|
||||
super(SocialButton, self).__init__(icn, "", parent=parent)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.url = url
|
||||
self.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url)))
|
||||
self.setToolTip(url)
|
||||
|
||||
|
||||
class RequirementsWidget(QWidget, SideTabContents):
|
||||
def __init__(self, system: DieselSystemDetail, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.implements_scrollarea = True
|
||||
|
||||
bold_font = self.font()
|
||||
bold_font.setBold(True)
|
||||
|
||||
req_layout = QGridLayout(self)
|
||||
min_label = QLabel(self.tr("Minimum"), parent=self)
|
||||
min_label.setFont(bold_font)
|
||||
rec_label = QLabel(self.tr("Recommend"), parent=self)
|
||||
rec_label.setFont(bold_font)
|
||||
req_layout.addWidget(min_label, 0, 1)
|
||||
req_layout.addWidget(rec_label, 0, 2)
|
||||
req_layout.setColumnStretch(1, 2)
|
||||
req_layout.setColumnStretch(2, 2)
|
||||
for i, detail in enumerate(system.details):
|
||||
req_layout.addWidget(QLabel(detail.title, parent=self), i + 1, 0)
|
||||
min_label = ElideLabel(detail.minimum, parent=self)
|
||||
req_layout.addWidget(min_label, i + 1, 1)
|
||||
rec_label = ElideLabel(detail.recommended, parent=self)
|
||||
req_layout.addWidget(rec_label, i + 1, 2)
|
||||
req_layout.setAlignment(Qt.AlignTop)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
18
rare/components/tabs/store/widgets/groups.py
Normal file
18
rare/components/tabs/store/widgets/groups.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from PyQt5.QtWidgets import QGroupBox, QLayout
|
||||
|
||||
from rare.widgets.loading_widget import LoadingWidget
|
||||
|
||||
|
||||
class StoreGroup(QGroupBox):
|
||||
def __init__(self, title: str, layout: type[QLayout], parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setTitle(title)
|
||||
self.main_layout = layout(self)
|
||||
self.loading_widget = LoadingWidget(autostart=True, parent=self)
|
||||
|
||||
def loading(self, state: bool) -> None:
|
||||
if state:
|
||||
self.loading_widget.start()
|
||||
else:
|
||||
self.loading_widget.stop()
|
||||
|
112
rare/components/tabs/store/widgets/image.py
Normal file
112
rare/components/tabs/store/widgets/image.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import (
|
||||
QPixmap,
|
||||
QImage,
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QSpacerItem,
|
||||
QSizePolicy,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
)
|
||||
|
||||
from rare.utils.qt_requests import QtRequests
|
||||
from rare.widgets.image_widget import ImageWidget
|
||||
from rare.widgets.loading_widget import LoadingWidget
|
||||
|
||||
|
||||
class IconWidget(object):
|
||||
def __init__(self):
|
||||
self.mini_widget: QWidget = None
|
||||
self.title_label: QLabel = None
|
||||
self.developer_label: QLabel = None
|
||||
self.price_label: QLabel = None
|
||||
self.discount_label: QLabel = None
|
||||
|
||||
def setupUi(self, widget: QWidget):
|
||||
# on-hover popup
|
||||
self.mini_widget = QWidget(parent=widget)
|
||||
self.mini_widget.setObjectName(f"{type(self).__name__}MiniWidget")
|
||||
self.mini_widget.setFixedHeight(int(widget.height() // 3))
|
||||
|
||||
# game title
|
||||
self.title_label = QLabel(parent=self.mini_widget)
|
||||
self.title_label.setObjectName(f"{type(self).__name__}TitleLabel")
|
||||
self.title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.title_label.setAlignment(Qt.AlignTop)
|
||||
self.title_label.setAutoFillBackground(False)
|
||||
self.title_label.setWordWrap(True)
|
||||
|
||||
# information below title
|
||||
self.developer_label = QLabel(parent=self.mini_widget)
|
||||
self.developer_label.setObjectName(f"{type(self).__name__}TooltipLabel")
|
||||
self.developer_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
self.developer_label.setAutoFillBackground(False)
|
||||
|
||||
self.price_label = QLabel(parent=self.mini_widget)
|
||||
self.price_label.setObjectName(f"{type(self).__name__}TooltipLabel")
|
||||
self.price_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.price_label.setAutoFillBackground(False)
|
||||
|
||||
self.discount_label = QLabel(parent=self.mini_widget)
|
||||
self.discount_label.setObjectName(f"{type(self).__name__}TooltipLabel")
|
||||
self.discount_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.discount_label.setAutoFillBackground(False)
|
||||
|
||||
# Create layouts
|
||||
# layout on top of the image, holds the status label, a spacer item and the mini widget
|
||||
image_layout = QVBoxLayout()
|
||||
image_layout.setContentsMargins(2, 2, 2, 2)
|
||||
|
||||
# layout for the mini widget, holds the top row and the info label
|
||||
mini_layout = QVBoxLayout()
|
||||
mini_layout.setSpacing(0)
|
||||
|
||||
# layout for the top row, holds the title and the launch button
|
||||
row_layout = QHBoxLayout()
|
||||
row_layout.setSpacing(6)
|
||||
row_layout.setAlignment(Qt.AlignBottom)
|
||||
|
||||
# Layout the widgets
|
||||
# (from inner to outer)
|
||||
row_layout.addWidget(self.developer_label, stretch=2)
|
||||
row_layout.addWidget(self.price_label)
|
||||
row_layout.addWidget(self.discount_label)
|
||||
mini_layout.addWidget(self.title_label)
|
||||
mini_layout.addLayout(row_layout)
|
||||
self.mini_widget.setLayout(mini_layout)
|
||||
|
||||
image_layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
|
||||
image_layout.addWidget(self.mini_widget)
|
||||
widget.setLayout(image_layout)
|
||||
|
||||
|
||||
class LoadingImageWidget(ImageWidget):
|
||||
def __init__(self, manager: QtRequests, parent=None):
|
||||
super(LoadingImageWidget, self).__init__(parent=parent)
|
||||
self.ui = IconWidget()
|
||||
self.spinner = LoadingWidget(parent=self)
|
||||
self.spinner.setVisible(False)
|
||||
self.manager = manager
|
||||
|
||||
def fetchPixmap(self, url):
|
||||
self.setPixmap(QPixmap())
|
||||
self.spinner.setFixedSize(self._image_size.size)
|
||||
self.spinner.start()
|
||||
self.manager.get(url, self.__on_image_ready, params={
|
||||
"resize": 1,
|
||||
"w": self._image_size.base.size.width(),
|
||||
"h": self._image_size.base.size.height(),
|
||||
})
|
||||
|
||||
def __on_image_ready(self, data):
|
||||
cover = QImage()
|
||||
cover.loadFromData(data)
|
||||
# cover = cover.scaled(self._image_size.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
cover.setDevicePixelRatio(self._image_size.base.pixel_ratio)
|
||||
cover = cover.convertToFormat(QImage.Format_ARGB32_Premultiplied)
|
||||
cover = QPixmap(cover)
|
||||
self.setPixmap(cover)
|
||||
self.spinner.stop()
|
136
rare/components/tabs/store/widgets/items.py
Normal file
136
rare/components/tabs/store/widgets/items.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
import logging
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWidgets import QPushButton
|
||||
|
||||
from rare.components.tabs.store.api.models.response import CatalogOfferModel
|
||||
from rare.models.image import ImageSize
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.utils.qt_requests import QtRequests
|
||||
from .image import LoadingImageWidget
|
||||
|
||||
logger = logging.getLogger("StoreWidgets")
|
||||
|
||||
|
||||
class ItemWidget(LoadingImageWidget):
|
||||
show_details = pyqtSignal(CatalogOfferModel)
|
||||
|
||||
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, parent=None):
|
||||
super(ItemWidget, self).__init__(manager, parent=parent)
|
||||
self.catalog_game = catalog_game
|
||||
|
||||
def mousePressEvent(self, a0: QMouseEvent) -> None:
|
||||
if a0.button() == Qt.LeftButton:
|
||||
a0.accept()
|
||||
self.show_details.emit(self.catalog_game)
|
||||
if a0.button() == Qt.RightButton:
|
||||
a0.accept()
|
||||
|
||||
|
||||
class StoreItemWidget(ItemWidget):
|
||||
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, parent=None):
|
||||
super(StoreItemWidget, self).__init__(manager, catalog_game, parent=parent)
|
||||
self.setFixedSize(ImageSize.DisplayWide)
|
||||
self.ui.setupUi(self)
|
||||
if catalog_game:
|
||||
self.init_ui(catalog_game)
|
||||
|
||||
def init_ui(self, game: CatalogOfferModel):
|
||||
if not game:
|
||||
self.ui.title_label.setText(self.tr("An error occurred"))
|
||||
return
|
||||
|
||||
self.ui.title_label.setText(game.title)
|
||||
for attr in game.customAttributes:
|
||||
if attr["key"] == "developerName":
|
||||
developer = attr["value"]
|
||||
break
|
||||
else:
|
||||
developer = game.seller["name"]
|
||||
self.ui.developer_label.setText(developer)
|
||||
price = game.price.totalPrice.fmtPrice["originalPrice"]
|
||||
discount_price = game.price.totalPrice.fmtPrice["discountPrice"]
|
||||
self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}')
|
||||
if price != discount_price:
|
||||
font = self.ui.price_label.font()
|
||||
font.setStrikeOut(True)
|
||||
self.ui.price_label.setFont(font)
|
||||
self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}')
|
||||
else:
|
||||
self.ui.discount_label.setVisible(False)
|
||||
|
||||
key_images = game.keyImages
|
||||
self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url)
|
||||
|
||||
# for img in json_info["keyImages"]:
|
||||
# if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo"]:
|
||||
# if img["type"] == "VaultClosed" and json_info["title"] != "Mystery Game":
|
||||
# continue
|
||||
# self.fetchPixmap(img["url"])
|
||||
# break
|
||||
# else:
|
||||
# logger.info(", ".join([img["type"] for img in json_info["keyImages"]]))
|
||||
|
||||
|
||||
class ResultsItemWidget(ItemWidget):
|
||||
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None):
|
||||
super(ResultsItemWidget, self).__init__(manager, catalog_game, parent=parent)
|
||||
self.setFixedSize(ImageSize.Display)
|
||||
self.ui.setupUi(self)
|
||||
|
||||
key_images = catalog_game.keyImages
|
||||
self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url)
|
||||
|
||||
self.ui.title_label.setText(catalog_game.title)
|
||||
|
||||
price = catalog_game.price.totalPrice.fmtPrice["originalPrice"]
|
||||
discount_price = catalog_game.price.totalPrice.fmtPrice["discountPrice"]
|
||||
self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}')
|
||||
if price != discount_price:
|
||||
font = self.ui.price_label.font()
|
||||
font.setStrikeOut(True)
|
||||
self.ui.price_label.setFont(font)
|
||||
self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}')
|
||||
else:
|
||||
self.ui.discount_label.setVisible(False)
|
||||
|
||||
|
||||
class WishlistItemWidget(ItemWidget):
|
||||
delete_from_wishlist = pyqtSignal(CatalogOfferModel)
|
||||
|
||||
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None):
|
||||
super(WishlistItemWidget, self).__init__(manager, catalog_game, parent=parent)
|
||||
self.setFixedSize(ImageSize.DisplayWide)
|
||||
self.ui.setupUi(self)
|
||||
for attr in catalog_game.customAttributes:
|
||||
if attr["key"] == "developerName":
|
||||
developer = attr["value"]
|
||||
break
|
||||
else:
|
||||
developer = catalog_game.seller["name"]
|
||||
original_price = catalog_game.price.totalPrice.fmtPrice["originalPrice"]
|
||||
discount_price = catalog_game.price.totalPrice.fmtPrice["discountPrice"]
|
||||
|
||||
self.ui.title_label.setText(catalog_game.title)
|
||||
self.ui.developer_label.setText(developer)
|
||||
self.ui.price_label.setText(f'{original_price if original_price != "0" else self.tr("Free")}')
|
||||
if original_price != discount_price:
|
||||
font = self.ui.price_label.font()
|
||||
font.setStrikeOut(True)
|
||||
self.ui.price_label.setFont(font)
|
||||
self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}')
|
||||
else:
|
||||
self.ui.discount_label.setVisible(False)
|
||||
key_images = catalog_game.keyImages
|
||||
self.fetchPixmap(
|
||||
key_images.for_dimensions(self.width(), self.height()).url
|
||||
)
|
||||
|
||||
self.delete_button = QPushButton(self)
|
||||
self.delete_button.setIcon(qta_icon("mdi.delete", color="white"))
|
||||
self.delete_button.clicked.connect(
|
||||
lambda: self.delete_from_wishlist.emit(self.catalog_game)
|
||||
)
|
||||
self.layout().insertWidget(0, self.delete_button, alignment=Qt.AlignRight)
|
||||
|
|
@ -1,45 +1,116 @@
|
|||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QStackedWidget, QMessageBox
|
||||
from enum import IntEnum
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QShowEvent
|
||||
from PyQt5.QtWidgets import QMessageBox, QWidget, QSizePolicy
|
||||
|
||||
from rare.components.tabs.store import ShopApiCore
|
||||
from rare.components.tabs.store.game_widgets import WishlistWidget
|
||||
from rare.ui.components.tabs.store.wishlist import Ui_Wishlist
|
||||
from rare.utils.extra_widgets import WaitingSpinner
|
||||
from rare.utils.misc import icon
|
||||
from rare.utils.misc import qta_icon
|
||||
from rare.widgets.flow_layout import FlowLayout
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from rare.widgets.sliding_stack import SlidingStackedWidget
|
||||
from .api.models.response import WishlistItemModel, CatalogOfferModel
|
||||
from .store_api import StoreAPI
|
||||
from .widgets.details import StoreDetailsWidget
|
||||
from .widgets.items import WishlistItemWidget
|
||||
|
||||
|
||||
class Wishlist(QStackedWidget, Ui_Wishlist):
|
||||
show_game_info = pyqtSignal(dict)
|
||||
class WishlistPage(SlidingStackedWidget, SideTabContents):
|
||||
def __init__(self, api: StoreAPI, parent=None):
|
||||
super(WishlistPage, self).__init__(parent=parent)
|
||||
self.implements_scrollarea = True
|
||||
|
||||
self.wishlist_widget = WishlistWidget(api, parent=self)
|
||||
self.wishlist_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.wishlist_widget.set_title.connect(self.set_title)
|
||||
self.wishlist_widget.show_details.connect(self.show_details)
|
||||
|
||||
self.details_widget = StoreDetailsWidget([], 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.wishlist_widget)
|
||||
self.addWidget(self.details_widget)
|
||||
|
||||
@pyqtSlot()
|
||||
def show_main(self):
|
||||
self.slideInWidget(self.wishlist_widget)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def show_details(self, game: CatalogOfferModel):
|
||||
self.details_widget.update_game(game)
|
||||
self.slideInWidget(self.details_widget)
|
||||
|
||||
|
||||
class WishlistOrder(IntEnum):
|
||||
NAME = 1
|
||||
PRICE = 2
|
||||
DISCOUNT = 3
|
||||
DEVELOPER = 4
|
||||
|
||||
|
||||
class WishlistFilter(IntEnum):
|
||||
NONE = 0
|
||||
DISCOUNT = 1
|
||||
|
||||
|
||||
class WishlistWidget(QWidget, SideTabContents):
|
||||
show_details = pyqtSignal(CatalogOfferModel)
|
||||
update_wishlist_signal = pyqtSignal()
|
||||
|
||||
def __init__(self, api_core: ShopApiCore):
|
||||
super(Wishlist, self).__init__()
|
||||
self.api_core = api_core
|
||||
self.setupUi(self)
|
||||
self.addWidget(WaitingSpinner())
|
||||
self.setCurrentIndex(1)
|
||||
self.wishlist = []
|
||||
self.widgets = []
|
||||
def __init__(self, api: StoreAPI, parent=None):
|
||||
super(WishlistWidget, self).__init__(parent=parent)
|
||||
self.implements_scrollarea = True
|
||||
self.api = api
|
||||
self.ui = Ui_Wishlist()
|
||||
self.ui.setupUi(self)
|
||||
self.ui.main_layout.setContentsMargins(0, 0, 3, 0)
|
||||
|
||||
self.sort_cb.currentIndexChanged.connect(
|
||||
lambda i: self.set_wishlist(self.wishlist, i)
|
||||
)
|
||||
self.filter_cb.currentIndexChanged.connect(self.set_filter)
|
||||
self.reload_button.clicked.connect(self.update_wishlist)
|
||||
self.reload_button.setIcon(icon("fa.refresh", color="white"))
|
||||
self.wishlist_layout = FlowLayout()
|
||||
self.ui.container_layout.addLayout(self.wishlist_layout, stretch=1)
|
||||
|
||||
self.reverse.stateChanged.connect(
|
||||
lambda: self.set_wishlist(sort=self.sort_cb.currentIndex())
|
||||
filters = {
|
||||
WishlistFilter.NONE: self.tr("All items"),
|
||||
WishlistFilter.DISCOUNT: self.tr("Discount"),
|
||||
}
|
||||
for data, text in filters.items():
|
||||
self.ui.filter_combo.addItem(text, data)
|
||||
self.ui.filter_combo.currentIndexChanged.connect(self.filter_wishlist)
|
||||
|
||||
sortings = {
|
||||
WishlistOrder.NAME: self.tr("Name"),
|
||||
WishlistOrder.PRICE: self.tr("Price"),
|
||||
WishlistOrder.DISCOUNT: self.tr("Discount"),
|
||||
WishlistOrder.DEVELOPER: self.tr("Developer"),
|
||||
}
|
||||
for data, text in sortings.items():
|
||||
self.ui.order_combo.addItem(text, data)
|
||||
self.ui.order_combo.currentIndexChanged.connect(self.order_wishlist)
|
||||
|
||||
self.ui.reload_button.setIcon(qta_icon("fa.refresh", color="white"))
|
||||
self.ui.reload_button.clicked.connect(self.update_wishlist)
|
||||
|
||||
self.ui.reverse_check.stateChanged.connect(
|
||||
lambda: self.order_wishlist(self.ui.order_combo.currentIndex())
|
||||
)
|
||||
|
||||
self.setEnabled(False)
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
self.update_wishlist()
|
||||
return super().showEvent(a0)
|
||||
|
||||
def update_wishlist(self):
|
||||
self.setCurrentIndex(1)
|
||||
self.api_core.get_wishlist(self.set_wishlist)
|
||||
self.setEnabled(False)
|
||||
self.api.get_wishlist(self.set_wishlist)
|
||||
|
||||
def delete_from_wishlist(self, game):
|
||||
self.api_core.remove_from_wishlist(
|
||||
game["namespace"],
|
||||
game["id"],
|
||||
def delete_from_wishlist(self, game: CatalogOfferModel):
|
||||
self.api.remove_from_wishlist(
|
||||
game.namespace,
|
||||
game.id,
|
||||
lambda success: self.update_wishlist()
|
||||
if success
|
||||
else QMessageBox.warning(
|
||||
|
@ -48,72 +119,68 @@ class Wishlist(QStackedWidget, Ui_Wishlist):
|
|||
)
|
||||
self.update_wishlist_signal.emit()
|
||||
|
||||
def set_filter(self, i):
|
||||
count = 0
|
||||
for w in self.widgets:
|
||||
if i == 1 and not w.discount:
|
||||
w.setVisible(False)
|
||||
@pyqtSlot(int)
|
||||
def filter_wishlist(self, index: int = int(WishlistFilter.NONE)):
|
||||
list_filter = self.ui.filter_combo.itemData(index, Qt.UserRole)
|
||||
widgets = self.ui.container.findChildren(WishlistItemWidget, options=Qt.FindDirectChildrenOnly)
|
||||
for w in widgets:
|
||||
if list_filter == WishlistFilter.NONE:
|
||||
w.setVisible(True)
|
||||
elif list_filter == WishlistFilter.DISCOUNT:
|
||||
w.setVisible(bool(w.catalog_game.price.totalPrice.discount))
|
||||
else:
|
||||
w.setVisible(True)
|
||||
count += 1
|
||||
have_visible = any(map(lambda x: x.isVisible(), widgets))
|
||||
self.ui.no_games_label.setVisible(not have_visible)
|
||||
|
||||
if i == 0:
|
||||
w.setVisible(True)
|
||||
@pyqtSlot(int)
|
||||
def order_wishlist(self, index: int = int(WishlistOrder.NAME)):
|
||||
list_order = self.ui.order_combo.itemData(index, Qt.UserRole)
|
||||
widgets = self.ui.container.findChildren(WishlistItemWidget, options=Qt.FindDirectChildrenOnly)
|
||||
for w in widgets:
|
||||
self.wishlist_layout.removeWidget(w)
|
||||
|
||||
if count == 0:
|
||||
self.no_games_label.setVisible(True)
|
||||
if list_order == WishlistOrder.NAME:
|
||||
def func(x: WishlistItemWidget):
|
||||
return x.catalog_game.title
|
||||
elif list_order == WishlistOrder.PRICE:
|
||||
def func(x: WishlistItemWidget):
|
||||
return x.catalog_game.price.totalPrice.discountPrice
|
||||
elif list_order == WishlistOrder.DEVELOPER:
|
||||
def func(x: WishlistItemWidget):
|
||||
return x.catalog_game.seller["name"]
|
||||
elif list_order == WishlistOrder.DISCOUNT:
|
||||
def func(x: WishlistItemWidget):
|
||||
discount = x.catalog_game.price.totalPrice.discountPrice
|
||||
original = x.catalog_game.price.totalPrice.originalPrice
|
||||
return 1 - (discount / original)
|
||||
else:
|
||||
self.no_games_label.setVisible(False)
|
||||
def func(x: WishlistItemWidget):
|
||||
return x.catalog_game.title
|
||||
|
||||
def set_wishlist(self, wishlist=None, sort=0):
|
||||
reverse = self.ui.reverse_check.isChecked()
|
||||
widgets = sorted(widgets, key=func, reverse=reverse)
|
||||
for w in widgets:
|
||||
self.wishlist_layout.addWidget(w)
|
||||
|
||||
def set_wishlist(self, wishlist: List[WishlistItemModel] = None):
|
||||
if wishlist and wishlist[0] == "error":
|
||||
return
|
||||
|
||||
if wishlist is not None:
|
||||
self.wishlist = wishlist
|
||||
widgets = self.ui.container.findChildren(WishlistItemWidget, options=Qt.FindDirectChildrenOnly)
|
||||
for w in widgets:
|
||||
self.wishlist_layout.removeWidget(w)
|
||||
w.deleteLater()
|
||||
|
||||
for i in self.widgets:
|
||||
i.deleteLater()
|
||||
self.ui.no_games_label.setVisible(bool(wishlist))
|
||||
|
||||
if sort == 0:
|
||||
sorted_list = sorted(self.wishlist, key=lambda x: x["offer"]["title"])
|
||||
elif sort == 1:
|
||||
sorted_list = sorted(
|
||||
self.wishlist,
|
||||
key=lambda x: x["offer"]["price"]["totalPrice"]["fmtPrice"][
|
||||
"discountPrice"
|
||||
],
|
||||
)
|
||||
elif sort == 2:
|
||||
sorted_list = sorted(
|
||||
self.wishlist, key=lambda x: x["offer"]["seller"]["name"]
|
||||
)
|
||||
elif sort == 3:
|
||||
sorted_list = sorted(
|
||||
self.wishlist,
|
||||
reverse=True,
|
||||
key=lambda x: 1
|
||||
- (
|
||||
x["offer"]["price"]["totalPrice"]["discountPrice"]
|
||||
/ x["offer"]["price"]["totalPrice"]["originalPrice"]
|
||||
),
|
||||
)
|
||||
else:
|
||||
sorted_list = self.wishlist
|
||||
self.widgets.clear()
|
||||
|
||||
if len(sorted_list) == 0:
|
||||
self.no_games_label.setVisible(True)
|
||||
else:
|
||||
self.no_games_label.setVisible(False)
|
||||
|
||||
if self.reverse.isChecked():
|
||||
sorted_list.reverse()
|
||||
|
||||
for game in sorted_list:
|
||||
w = WishlistWidget(game["offer"])
|
||||
self.widgets.append(w)
|
||||
self.list_layout.addWidget(w)
|
||||
w.open_game.connect(self.show_game_info.emit)
|
||||
for game in wishlist:
|
||||
w = WishlistItemWidget(self.api.cached_manager, game.offer, self.ui.container)
|
||||
w.show_details.connect(self.show_details)
|
||||
w.delete_from_wishlist.connect(self.delete_from_wishlist)
|
||||
self.setCurrentIndex(0)
|
||||
self.wishlist_layout.addWidget(w)
|
||||
|
||||
self.order_wishlist(self.ui.order_combo.currentIndex())
|
||||
self.filter_wishlist(self.ui.filter_combo.currentIndex())
|
||||
|
||||
self.setEnabled(True)
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import QTabBar, QToolButton
|
||||
|
||||
from rare.utils.misc import icon
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QTabBar, QSizePolicy, QPushButton
|
||||
|
||||
|
||||
class MainTabBar(QTabBar):
|
||||
def __init__(self, parent=None):
|
||||
super(MainTabBar, self).__init__(parent=parent)
|
||||
self.setObjectName("MainTabBar")
|
||||
self.setObjectName(type(self).__name__)
|
||||
font = self.font()
|
||||
font.setPointSize(font.pointSize() + 2)
|
||||
font.setPointSize(font.pointSize() + 1)
|
||||
font.setBold(True)
|
||||
self.setFont(font)
|
||||
self.expanded = -1
|
||||
|
@ -24,11 +22,9 @@ class MainTabBar(QTabBar):
|
|||
return size
|
||||
|
||||
|
||||
class TabButtonWidget(QToolButton):
|
||||
def __init__(self, button_icon: str, tool_tip: str, fallback_icon=None, parent=None):
|
||||
class TabButtonWidget(QPushButton):
|
||||
def __init__(self, icon: QIcon, tooltip: str = "", parent=None):
|
||||
super(TabButtonWidget, self).__init__(parent=parent)
|
||||
self.setText("Icon")
|
||||
self.setPopupMode(QToolButton.InstantPopup)
|
||||
self.setIcon(icon(button_icon, fallback_icon, scale_factor=1.25))
|
||||
self.setToolTip(tool_tip)
|
||||
self.setIconSize(QSize(25, 25))
|
||||
self.setObjectName(type(self).__name__)
|
||||
self.setIcon(icon)
|
||||
self.setToolTip(tooltip)
|
||||
|
|
|
@ -41,11 +41,11 @@ class RareGame(RareGameSlim):
|
|||
return cls(
|
||||
queued=data.get("queued", False),
|
||||
queue_pos=data.get("queue_pos", None),
|
||||
last_played=datetime.fromisoformat(x) if (x := data.get("last_played", None)) else datetime.min,
|
||||
grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", None)) else datetime.min,
|
||||
last_played=datetime.fromisoformat(x) if (x := data.get("last_played", "")) else datetime.min,
|
||||
grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", "")) else datetime.min,
|
||||
steam_appid=data.get("steam_appid", None),
|
||||
steam_grade=data.get("steam_grade", None),
|
||||
steam_date=datetime.fromisoformat(x) if (x := data.get("steam_date", None)) else datetime.min,
|
||||
steam_date=datetime.fromisoformat(x) if (x := data.get("steam_date", "")) else datetime.min,
|
||||
tags=data.get("tags", []),
|
||||
)
|
||||
|
||||
|
@ -432,10 +432,13 @@ class RareGame(RareGameSlim):
|
|||
if self.metadata.steam_grade != "pending":
|
||||
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
|
||||
|
||||
if elapsed_time.days > 3 and (self.metadata.steam_grade is None or self.metadata.steam_appid is None):
|
||||
if elapsed_time.days > 3:
|
||||
logger.info("Refreshing ProtonDB grade for %s", self.app_title)
|
||||
|
||||
def _set_steam_grade():
|
||||
appid, rating = get_rating(self.core, self.app_name)
|
||||
self.set_steam_grade(appid, rating)
|
||||
|
||||
worker = QRunnable.create(_set_steam_grade)
|
||||
QThreadPool.globalInstance().start(worker)
|
||||
self.metadata.steam_grade = "pending"
|
||||
|
@ -445,12 +448,15 @@ class RareGame(RareGameSlim):
|
|||
def steam_appid(self) -> Optional[int]:
|
||||
return self.metadata.steam_appid
|
||||
|
||||
def set_steam_appid(self, appid: int):
|
||||
set_envvar(self.app_name, "SteamAppId", str(appid))
|
||||
set_envvar(self.app_name, "SteamGameId", str(appid))
|
||||
set_envvar(self.app_name, "STEAM_COMPAT_APP_ID", str(appid))
|
||||
self.metadata.steam_appid = appid
|
||||
|
||||
def set_steam_grade(self, appid: int, grade: str) -> None:
|
||||
if appid and self.steam_appid is None:
|
||||
set_envvar(self.app_name, "SteamAppId", str(appid))
|
||||
set_envvar(self.app_name, "SteamGameId", str(appid))
|
||||
set_envvar(self.app_name, "STEAM_COMPAT_APP_ID", str(appid))
|
||||
self.metadata.steam_appid = appid
|
||||
self.set_steam_appid(appid)
|
||||
self.metadata.steam_grade = grade
|
||||
self.metadata.steam_date = datetime.utcnow()
|
||||
self.__save_metadata()
|
||||
|
@ -565,8 +571,8 @@ class RareGame(RareGameSlim):
|
|||
if wine_pfx:
|
||||
args.extend(["--wine-prefix", wine_pfx])
|
||||
|
||||
logger.info(f"Starting game process: ({executable} {' '.join(args)})")
|
||||
QProcess.startDetached(executable, args)
|
||||
logger.info(f"Start new Process: ({executable} {' '.join(args)})")
|
||||
self.game_process.connect_to_server(on_startup=False)
|
||||
return True
|
||||
|
||||
|
|
|
@ -2,22 +2,22 @@
|
|||
Active\AlternateBase=#fff7f7f7
|
||||
Active\Base=#ff333344
|
||||
Active\BrightText=#ffffffff
|
||||
Active\Button=#ff3c3f41
|
||||
Active\Button=#ff272a2e
|
||||
Active\ButtonText=#ffeeeeee
|
||||
Active\Dark=#ff9f0910
|
||||
Active\Highlight=#ff2f4f4f
|
||||
Active\Dark=#ff232529
|
||||
Active\Highlight=#ff385e5e
|
||||
Active\HighlightedText=#ffeeeeee
|
||||
Active\Light=#ffffffff
|
||||
Active\Link=#ff0000ff
|
||||
Active\Light=#ff2c2f33
|
||||
Active\Link=#ff0000dd
|
||||
Active\LinkVisited=#ffff00ff
|
||||
Active\Mid=#ffb80e35
|
||||
Active\Midlight=#ffca0651
|
||||
Active\Mid=#ff25272b
|
||||
Active\Midlight=#ff292c30
|
||||
Active\PlaceholderText=#80eeeeee
|
||||
Active\Shadow=#ff767676
|
||||
Active\Shadow=#ff0b0c0d
|
||||
Active\Text=#ffeeeeee
|
||||
Active\ToolTipBase=#ffffffdc
|
||||
Active\ToolTipText=#ffeeeeee
|
||||
Active\Window=#ff202225
|
||||
Active\Window=#ff212226
|
||||
Active\WindowText=#ffeeeeee
|
||||
Disabled\ButtonText=#ff808080
|
||||
Disabled\HighlightedText=#ff808080
|
||||
|
|
BIN
rare/resources/languages/rare_bg_BG.qm
Normal file
BIN
rare/resources/languages/rare_bg_BG.qm
Normal file
Binary file not shown.
3230
rare/resources/languages/rare_bg_BG.ts
Normal file
3230
rare/resources/languages/rare_bg_BG.ts
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -118,22 +118,22 @@
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/dialogs/login/browser_login.py" line="70"/>
|
||||
<location filename="../../components/dialogs/login/browser_login.py" line="72"/>
|
||||
<source>Logging in...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/dialogs/login/browser_login.py" line="77"/>
|
||||
<location filename="../../components/dialogs/login/browser_login.py" line="79"/>
|
||||
<source>Login failed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/dialogs/login/browser_login.py" line="49"/>
|
||||
<location filename="../../components/dialogs/login/browser_login.py" line="51"/>
|
||||
<source>Copied to clipboard</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/dialogs/login/browser_login.py" line="31"/>
|
||||
<location filename="../../components/dialogs/login/browser_login.py" line="32"/>
|
||||
<source>Insert authorizationCode here</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -174,17 +174,17 @@
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/game_info/cloud_saves.py" line="151"/>
|
||||
<location filename="../../components/tabs/games/game_info/cloud_saves.py" line="152"/>
|
||||
<source>Error - {}</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/game_info/cloud_saves.py" line="192"/>
|
||||
<location filename="../../components/tabs/games/game_info/cloud_saves.py" line="193"/>
|
||||
<source>Newer</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/game_info/cloud_saves.py" line="151"/>
|
||||
<location filename="../../components/tabs/games/game_info/cloud_saves.py" line="152"/>
|
||||
<source>Error while calculating path for <b>{}</b>. Insufficient permissions to create <b>{}</b></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -210,17 +210,17 @@
|
|||
<context>
|
||||
<name>CloudSyncDialog</name>
|
||||
<message>
|
||||
<location filename="../../launcher/cloud_sync_dialog.py" line="30"/>
|
||||
<location filename="../../commands/launcher/cloud_sync_dialog.py" line="30"/>
|
||||
<source>Cloud saves for</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/cloud_sync_dialog.py" line="43"/>
|
||||
<location filename="../../commands/launcher/cloud_sync_dialog.py" line="43"/>
|
||||
<source>Skip</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/cloud_sync_dialog.py" line="50"/>
|
||||
<location filename="../../commands/launcher/cloud_sync_dialog.py" line="50"/>
|
||||
<source>Newer</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -251,7 +251,7 @@
|
|||
<context>
|
||||
<name>Code</name>
|
||||
<message>
|
||||
<location filename="../../shared/game_process.py" line="105"/>
|
||||
<location filename="../../shared/game_process.py" line="106"/>
|
||||
<source>Error in game {}:
|
||||
{}</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
@ -271,43 +271,43 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<context>
|
||||
<name>ConsoleDialog</name>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="39"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="39"/>
|
||||
<source>Show environment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="43"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="43"/>
|
||||
<source>Save output to file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="47"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="47"/>
|
||||
<source>Clear console</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="53"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="53"/>
|
||||
<source>Terminate</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="58"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="58"/>
|
||||
<source>Kill</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="107"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="107"/>
|
||||
<source>Saved</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="123"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="123"/>
|
||||
<source>Application "{}" finished with "{}"
|
||||
</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="28"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="28"/>
|
||||
<source>Console</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -325,7 +325,7 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../launcher/console_dialog.py" line="150"/>
|
||||
<location filename="../../commands/launcher/console_dialog.py" line="150"/>
|
||||
<source>Environment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -517,52 +517,52 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<context>
|
||||
<name>DxvkSettings</name>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="270"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="235"/>
|
||||
<source>FPS</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="271"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="236"/>
|
||||
<source>Frametime</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="272"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="237"/>
|
||||
<source>Memory usage</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="273"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="238"/>
|
||||
<source>GPU usage</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="276"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="241"/>
|
||||
<source>D3D feature level</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="279"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="244"/>
|
||||
<source>Scale</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="277"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="242"/>
|
||||
<source>Compiler activity</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="268"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="233"/>
|
||||
<source>DXVK settings</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="274"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="239"/>
|
||||
<source>Device info</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="275"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="240"/>
|
||||
<source>DXVK version</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -570,22 +570,22 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<context>
|
||||
<name>EGLSyncExportGroup</name>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="316"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="312"/>
|
||||
<source>Exportable games</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="317"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="313"/>
|
||||
<source>No games to export to EGL</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="318"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="314"/>
|
||||
<source>Export</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="334"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="330"/>
|
||||
<source>The following errors occurred while exporting.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -623,12 +623,12 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="102"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="98"/>
|
||||
<source>Default Wine prefix is unset, or path does not exist. Create it or configure it in Settings -> Linux.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="109"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="105"/>
|
||||
<source>Default Wine prefix is set but EGL manifests path does not exist. Your configured default Wine prefix might not be where EGL is installed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -641,22 +641,22 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<context>
|
||||
<name>EGLSyncImportGroup</name>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="354"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="350"/>
|
||||
<source>Importable games</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="355"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="351"/>
|
||||
<source>No games to import from EGL</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="356"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="352"/>
|
||||
<source>Import</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="373"/>
|
||||
<location filename="../../components/tabs/games/integrations/egl_sync_group.py" line="369"/>
|
||||
<source>The following errors occurred while importing.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -978,7 +978,7 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/games/game_info/game_info.py" line="342"/>
|
||||
<location filename="../../components/tabs/games/game_info/game_info.py" line="393"/>
|
||||
<source>Install</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -1127,17 +1127,17 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="81"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="85"/>
|
||||
<source>Import Game</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="103"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="107"/>
|
||||
<source>Search Game</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="86"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="90"/>
|
||||
<source>Sync with EGL</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -1147,22 +1147,22 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="110"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="112"/>
|
||||
<source>Installed games</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="119"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="121"/>
|
||||
<source>Available games</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="89"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="93"/>
|
||||
<source>Epic Overlay and Ubisoft</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="99"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="103"/>
|
||||
<source>Integrations</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -1192,22 +1192,22 @@ This is usually do it the game or Rare's game launcher already running</sou
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="62"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="64"/>
|
||||
<source>Title</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="63"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="65"/>
|
||||
<source>Recently played</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="64"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="66"/>
|
||||
<source>Newest</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="65"/>
|
||||
<location filename="../../components/tabs/games/head_bar.py" line="67"/>
|
||||
<source>Oldest</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -1652,7 +1652,7 @@ Successfully imported {} games, failed to import {} games and {} errors occurred
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/dialogs/install_dialog.py" line="106"/>
|
||||
<location filename="../../components/dialogs/install_dialog.py" line="229"/>
|
||||
<source>Warning</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -1931,17 +1931,17 @@ Successfully imported {} games, failed to import {} games and {} errors occurred
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/legendary.py" line="237"/>
|
||||
<location filename="../../components/tabs/settings/legendary.py" line="238"/>
|
||||
<source>Cleanup complete! Successfully removed {}</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/legendary.py" line="49"/>
|
||||
<location filename="../../components/tabs/settings/legendary.py" line="50"/>
|
||||
<source>Default installation folder for macOS games</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/legendary.py" line="58"/>
|
||||
<location filename="../../components/tabs/settings/legendary.py" line="59"/>
|
||||
<source>Default installation folder for Windows games</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -2023,12 +2023,12 @@ Disabling this greatly improves start-up time, but some library filters may not
|
|||
<context>
|
||||
<name>ListGameWidget</name>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/game_widgets/list_game_widget.py" line="39"/>
|
||||
<location filename="../../components/tabs/games/game_widgets/list_game_widget.py" line="37"/>
|
||||
<source>Launch</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/game_widgets/list_game_widget.py" line="39"/>
|
||||
<location filename="../../components/tabs/games/game_widgets/list_game_widget.py" line="37"/>
|
||||
<source>Link/Play</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -2139,97 +2139,97 @@ Are you sure you want to quit?</source>
|
|||
<context>
|
||||
<name>MangoHudSettings</name>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="301"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="266"/>
|
||||
<source>MangoHud settings</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="303"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="268"/>
|
||||
<source>Read config</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="304"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="269"/>
|
||||
<source>FPS</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="305"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="270"/>
|
||||
<source>Frame time</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="306"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="271"/>
|
||||
<source>CPU load</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="307"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="272"/>
|
||||
<source>GPU load</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="308"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="273"/>
|
||||
<source>CPU temperature</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="309"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="274"/>
|
||||
<source>GPU temperature</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="310"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="275"/>
|
||||
<source>Memory usage</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="311"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="276"/>
|
||||
<source>VRAM usage</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="312"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="277"/>
|
||||
<source>Local time</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="313"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="278"/>
|
||||
<source>MangoHud version</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="314"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="279"/>
|
||||
<source>System architecture</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="315"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="280"/>
|
||||
<source>FPS graph</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="316"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="281"/>
|
||||
<source>GPU name</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="317"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="282"/>
|
||||
<source>CPU power consumption</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="318"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="283"/>
|
||||
<source>GPU power consumption</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="321"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="286"/>
|
||||
<source>Font size</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="322"/>
|
||||
<location filename="../../components/tabs/settings/widgets/overlay.py" line="287"/>
|
||||
<source>Position</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -2336,27 +2336,27 @@ Are you sure you want to quit?</source>
|
|||
<context>
|
||||
<name>ProtonSettings</name>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="32"/>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="33"/>
|
||||
<source>Please select path for proton prefix</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="27"/>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="28"/>
|
||||
<source>Proton settings</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="41"/>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="42"/>
|
||||
<source>Proton tool</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="42"/>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="43"/>
|
||||
<source>Compat data</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="58"/>
|
||||
<location filename="../../components/tabs/settings/widgets/proton.py" line="59"/>
|
||||
<source>Don't use a compatibility tool</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -2463,7 +2463,7 @@ Are you sure you want to quit?</source>
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/rpc.py" line="39"/>
|
||||
<location filename="../../components/tabs/settings/widgets/rpc.py" line="40"/>
|
||||
<source>Pypresence is not installed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -2492,150 +2492,165 @@ Are you sure you want to quit?</source>
|
|||
<context>
|
||||
<name>RareSettings</name>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="149"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="139"/>
|
||||
<source>Interface</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="150"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="140"/>
|
||||
<source>Language</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="154"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="57"/>
|
||||
<source>None</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="156"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="145"/>
|
||||
<source>Behavior</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="157"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="146"/>
|
||||
<source>Restore window size on application startup</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="164"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="153"/>
|
||||
<source>Logs</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="167"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="156"/>
|
||||
<source>Shortcuts</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="155"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="171"/>
|
||||
<source>Create start menu link</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="112"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="126"/>
|
||||
<source>Remove desktop link</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="152"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="168"/>
|
||||
<source>Remove start menu link</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="169"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="186"/>
|
||||
<source>Remove Desktop link</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="172"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="189"/>
|
||||
<source>Create desktop link</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="106"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="120"/>
|
||||
<source>Not supported</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="175"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="192"/>
|
||||
<source>Error</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="175"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="192"/>
|
||||
<source>Permission error, cannot remove {}</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="151"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="141"/>
|
||||
<source>Color scheme</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="153"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="142"/>
|
||||
<source>Style sheet</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="155"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="143"/>
|
||||
<source>Restart Rare to apply changes.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="158"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="147"/>
|
||||
<source>Show notifications when downloads complete</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="159"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="148"/>
|
||||
<source>Show console windows when launching games</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="160"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="149"/>
|
||||
<source>Exit to system tray</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="161"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="150"/>
|
||||
<source>Queue game updates on application startup</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="162"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="151"/>
|
||||
<source>Confirm before launching games</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="163"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="152"/>
|
||||
<source>Automatically upload/download cloud saves</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="165"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="154"/>
|
||||
<source>Open log folder</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="166"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="155"/>
|
||||
<source>Clean log folder</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="168"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="157"/>
|
||||
<source>Create on desktop</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="169"/>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="158"/>
|
||||
<source>Create in menu</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="37"/>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="35"/>
|
||||
<source>System default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../ui/components/tabs/settings/rare.py" line="144"/>
|
||||
<source>Library view</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="69"/>
|
||||
<source>Game covers</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/rare.py" line="70"/>
|
||||
<source>Vertical list</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SearchResults</name>
|
||||
|
@ -2868,7 +2883,7 @@ Are you sure you want to quit?</source>
|
|||
<context>
|
||||
<name>SideTabWidget</name>
|
||||
<message>
|
||||
<location filename="../../widgets/side_tab.py" line="130"/>
|
||||
<location filename="../../widgets/side_tab.py" line="132"/>
|
||||
<source>Back</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -2986,12 +3001,12 @@ Are you sure you want to quit?</source>
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/wine.py" line="47"/>
|
||||
<location filename="../../components/tabs/settings/widgets/wine.py" line="48"/>
|
||||
<source>Prefix</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../components/tabs/settings/widgets/wine.py" line="48"/>
|
||||
<location filename="../../components/tabs/settings/widgets/wine.py" line="47"/>
|
||||
<source>Executable</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -3201,7 +3216,7 @@ Are you sure you want to quit?</source>
|
|||
<context>
|
||||
<name>widget</name>
|
||||
<message>
|
||||
<location filename="../../components/tabs/games/game_widgets/list_widget.py" line="111"/>
|
||||
<location filename="../../components/tabs/games/game_widgets/list_widget.py" line="113"/>
|
||||
<source>Install</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -5,7 +5,7 @@ from typing import Union, Type
|
|||
import qstylizer.style
|
||||
from PyQt5.QtCore import QDir, QObject
|
||||
from PyQt5.QtGui import QColor
|
||||
from PyQt5.pyrcc import RCCResourceLibrary, CONSTANT_COMPRESSLEVEL_DEFAULT, CONSTANT_COMPRESSTHRESHOLD_DEFAULT
|
||||
from PyQt5.pyrcc import RCCResourceLibrary, CONSTANT_COMPRESSTHRESHOLD_DEFAULT
|
||||
from PyQt5.sip import wrappertype
|
||||
|
||||
from rare.utils.misc import widget_object_name
|
||||
|
@ -64,6 +64,21 @@ def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""):
|
|||
css = qstylizer.style.StyleSheet()
|
||||
|
||||
|
||||
# Generic flat button
|
||||
css['QPushButton[flat="true"]'].setValues(
|
||||
border="0px",
|
||||
borderRadius="5px",
|
||||
backgroundColor="rgba(255, 255, 255, 5%)",
|
||||
)
|
||||
|
||||
|
||||
# InfoLabel
|
||||
css.QLabel["#InfoLabel"].setValues(
|
||||
color="#999",
|
||||
fontStyle="italic",
|
||||
fontWeight="normal",
|
||||
)
|
||||
|
||||
# [Un]InstallButton
|
||||
css.QPushButton["#InstallButton"].setValues(
|
||||
borderColor=QColor(0, 180, 0).name(),
|
||||
|
@ -197,6 +212,15 @@ css.QPushButton[css_name(SelectViewWidget, "Button")].setValues(
|
|||
)
|
||||
|
||||
|
||||
# ButtonLineEdit
|
||||
from rare.utils.extra_widgets import ButtonLineEdit
|
||||
css.QPushButton[css_name(ButtonLineEdit, "Button")].setValues(
|
||||
backgroundColor="transparent",
|
||||
border="0px",
|
||||
padding="0px",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open("stylesheet.qss", "w") as qss:
|
||||
qss.write(f'\n/* This file is auto-generated from "{os.path.basename(__file__)}". DO NOT EDIT!!! */\n\n')
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
|
||||
/* This file is auto-generated from "stylesheet.py". DO NOT EDIT!!! */
|
||||
|
||||
QPushButton[flat="true"] {
|
||||
border: 0px;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(255, 255, 255, 5%);
|
||||
}
|
||||
QLabel#InfoLabel {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
QPushButton#InstallButton {
|
||||
border-color: #00b400;
|
||||
background-color: #007800;
|
||||
|
@ -106,3 +116,8 @@ QPushButton#SelectViewWidgetButton {
|
|||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
QPushButton#ButtonLineEditButton {
|
||||
background-color: transparent;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -20,7 +20,6 @@ disabled: #43474d #767778 -- border for disabled widgets
|
|||
alternate: #3c3f41 #A3DAAA -- border color for gradient backgrounds on widgets like Tabs and Popups
|
||||
|
||||
[Special]
|
||||
info-normal: #bbb #4D5059 -- infoLabel
|
||||
install-normal: #070 #F9A7FF -- install
|
||||
install-hover: #050 #BB7DBF -- install
|
||||
install-disabled: #020 #7D5380 -- install
|
||||
|
@ -48,12 +47,6 @@ QLabel:disabled {
|
|||
padding: 0px;
|
||||
selection-background-color: #71DA7E;
|
||||
}
|
||||
QLabel[infoLabel="1"] {
|
||||
color: #4D5059;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
||||
QMenu,
|
||||
QListView,
|
||||
|
@ -84,6 +77,7 @@ QScrollBar {
|
|||
border-radius: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
QHeaderView::section,
|
||||
QTableView QTableCornerButton::section,
|
||||
QLineEdit,
|
||||
|
@ -101,6 +95,7 @@ QScrollBar {
|
|||
background-color: #DADDDE;
|
||||
selection-background-color: #71DA7E;
|
||||
}
|
||||
|
||||
QLineEdit,
|
||||
QTextEdit,
|
||||
QTimeEdit,
|
||||
|
@ -110,12 +105,18 @@ QComboBox,
|
|||
QSpinBox,
|
||||
QDoubleSpinBox,
|
||||
QProgressBar,
|
||||
QScrollArea,
|
||||
QPushButton {
|
||||
height: 1.30em;
|
||||
min-height: 3.00ex;
|
||||
/* min-height: 1.30em; */
|
||||
/* min-height: 18px; */
|
||||
}
|
||||
QToolButton {
|
||||
height: 1.10em;
|
||||
min-height: 3.00ex;
|
||||
/* min-height: 1.10em; */
|
||||
/* min-height: 15px; */
|
||||
}
|
||||
|
||||
QFrame[frameShape="0"] {
|
||||
border-width: 0px;
|
||||
}
|
||||
|
@ -183,14 +184,19 @@ QComboBox QAbstractItemView {
|
|||
border-top-width: 1;
|
||||
image: url(":/stylesheets/ChildOfMetropolis/sort-down.svg");
|
||||
}
|
||||
|
||||
QProgressBar {
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
padding: 0px;
|
||||
border-width: 0px;
|
||||
width: 2%;
|
||||
margin: 0%;
|
||||
margin: 0px;
|
||||
background-color: #71DA7E;
|
||||
}
|
||||
|
||||
QScrollBar {
|
||||
border-radius: 4px;
|
||||
padding: 1px;
|
||||
|
@ -266,6 +272,12 @@ QTableView {
|
|||
alternate-background-color: #BCBEBF;
|
||||
}
|
||||
|
||||
QListView QLineEdit,
|
||||
QTreeView QLineEdit,
|
||||
QTableView QLineEdit {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
QListView::item,
|
||||
QTreeView::item {
|
||||
margin-right: 1px;
|
||||
|
@ -314,12 +326,15 @@ QToolButton {
|
|||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
QPushButton::menu-indicator {
|
||||
/*
|
||||
QPushButton::menu-indicator,
|
||||
QToolButton::menu-indicator {
|
||||
subcontrol-position: right center;
|
||||
subcontrol-origin: padding;
|
||||
left: -2px;
|
||||
border-style: none;
|
||||
}
|
||||
*/
|
||||
|
||||
QGroupBox,
|
||||
QCheckBox,
|
||||
|
@ -451,12 +466,6 @@ QSizeGrip {
|
|||
height: 4px;
|
||||
}
|
||||
|
||||
QLineEdit#SearchBar {
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
background-color: #DADDDE;
|
||||
}
|
||||
|
||||
QTabWidget::pane {
|
||||
}
|
||||
QTabWidget::tab-bar {
|
||||
|
@ -610,6 +619,39 @@ QTabBar::tab:right:selected:disabled {
|
|||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
QStatusBar {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-top-color: #5CD3FF;
|
||||
border-bottom-color: #A3DAAA;
|
||||
background: qlineargradient(
|
||||
x1: 0, y1: 3,
|
||||
x2: 0, y2: 0,
|
||||
stop: 0 #A3DAAA,
|
||||
stop: 1 #C2C4C5);
|
||||
}
|
||||
|
||||
QToolTip {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #5CD3FF;
|
||||
border-radius: 4px;
|
||||
padding: 1px;
|
||||
opacity: 200;
|
||||
}
|
||||
|
||||
QBalloonTip {
|
||||
color: #36393F;
|
||||
background-color: #C2C4C5;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #5CD3FF;
|
||||
border-radius: 4px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
/* Main tab bar styling */
|
||||
QTabBar#MainTabBar {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
|
@ -632,7 +674,7 @@ QTabBar#MainTabBar::tab {
|
|||
margin-right: 3px;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #5CD3FF;
|
||||
padding: 5px;
|
||||
padding: 3px 5px;
|
||||
}/*
|
||||
QTabBar#MainTabBar::tab:top:first,
|
||||
QTabBar#MainTabBar::tab:bottom:first {
|
||||
|
@ -651,6 +693,17 @@ QTabBar#MainTabBar::tab:top:selected {
|
|||
border-color: #5CD3FF;
|
||||
border-bottom-color: #C2C4C5;
|
||||
}
|
||||
QPushButton#TabButtonWidget,
|
||||
QToolButton#TabButtonWidget {
|
||||
padding: 0px;
|
||||
border-color: rgb( 51, 54, 59);
|
||||
}
|
||||
QPushButton#TabButtonWidget:disabled,
|
||||
QToolButton#TabButtonWidget:disabled {
|
||||
border-color: rgb( 41, 43, 47);
|
||||
}
|
||||
|
||||
/* Side tab bar styling */
|
||||
QTabBar#SideTabBar {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
|
@ -688,41 +741,16 @@ QTabBar#SideTabBar::tab:disabled {
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
QStatusBar {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-top-color: #5CD3FF;
|
||||
border-bottom-color: #A3DAAA;
|
||||
background: qlineargradient(
|
||||
x1: 0, y1: 3,
|
||||
x2: 0, y2: 0,
|
||||
stop: 0 #A3DAAA,
|
||||
stop: 1 #C2C4C5);
|
||||
}
|
||||
|
||||
QToolTip {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #5CD3FF;
|
||||
border-radius: 4px;
|
||||
padding: 1px;
|
||||
opacity: 200;
|
||||
}
|
||||
|
||||
QBalloonTip {
|
||||
color: #36393F;
|
||||
background-color: #C2C4C5;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #5CD3FF;
|
||||
border-radius: 4px;
|
||||
padding: 1px;
|
||||
/* Search bar styling */
|
||||
QLineEdit#SearchBar {
|
||||
border-radius: 5px;
|
||||
background-color: #DADDDE;
|
||||
}
|
||||
|
||||
/* Wrapper settings styling */
|
||||
QPushButton#WrapperWidgetButton,
|
||||
QToolButton#WrapperWidgetButton {
|
||||
padding: 0px;
|
||||
border-color: #DADDDE;
|
||||
}
|
||||
QPushButton#WrapperWidgetButton:disabled,
|
||||
|
@ -736,6 +764,7 @@ QScrollArea#WrapperSettingsScroll {
|
|||
QScrollBar#WrapperSettingsScrollBar {
|
||||
background-color: #BCBEBF;
|
||||
}
|
||||
/*
|
||||
QLabel#WrapperSettingsLabel {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
|
@ -745,3 +774,8 @@ QLabel#WrapperSettingsLabel {
|
|||
border-color: #71DA7E;
|
||||
background-color: #BCBEBF;
|
||||
}
|
||||
QLabel#WrapperSettingsLabel:disabled {
|
||||
border-color: rgb( 67, 71, 77);
|
||||
background-color: rgb( 32, 34, 37);
|
||||
}
|
||||
*/
|
||||
|
|
Binary file not shown.
|
@ -1,4 +1,4 @@
|
|||
<svg height="640" viewBox="0 0 320 640" width="320" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m41 236.475h238c21.4 0 32.1 25.9 17 41l-119 119c-9.4 9.4-24.6 9.4-33.9 0l-119.1-119c-15.1-15.1-4.4-41 17-41z"
|
||||
fill="#eee"/>
|
||||
fill="#ccc"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 241 B After Width: | Height: | Size: 241 B |
|
@ -1,4 +1,4 @@
|
|||
<svg height="320" viewBox="0 0 320 320" width="320" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m41 76.475h238c21.4 0 32.1 25.9 17 41l-119 119c-9.4 9.4-24.6 9.4-33.9 0l-119.1-119c-15.1-15.1-4.4-41 17-41z"
|
||||
fill="#eee"/>
|
||||
fill="#ccc"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue