1
0
Fork 0
mirror of synced 2024-06-03 03:04:42 +12:00

Compare commits

...

70 commits

Author SHA1 Message Date
Stelios Tsampas 8fc92a3a30
Merge pull request #408 from loathingKernel/develop
Steam shortcuts, Discord RPC, MangoHud
2024-05-31 16:16:17 +03:00
loathingKernel a33821e8e3 SteamShortcuts: Ignore pylint error 2024-05-31 15:59:19 +03:00
loathingKernel 4bff7a9fad RareLauncher: Add more logging 2024-05-31 15:53:46 +03:00
loathingKernel 88aecd0741 SteamShortcuts: Add action in the right-click drop-down menu
to export and delete game shortcuts from Steam.
2024-05-31 15:53:08 +03:00
loathingKernel da93dc2e5e ImageManager: Add the ability to request different types of cover art images 2024-05-31 00:21:58 +03:00
loathingKernel 4587c73671 ImageUrlModel: Add more types for lookup.
The issue happened with Farming Simulator 22 freebie, because it doesn't
least a tall image. This should be checked again in the future.
2024-05-28 22:58:44 +03:00
loathingKernel 9419ff402b Icon/ListViewContainer: Make sorting work more accurately to advertised 2024-05-28 22:52:58 +03:00
loathingKernel 844f932222 RareGame: Initialized datetimes as UTC too 2024-05-28 22:51:29 +03:00
Stelios Tsampas 7203126df2
Merge pull request #409 from cherouvim/patch-1
chore: Use actual target repo URLs.
2024-05-28 10:04:16 +03:00
Ioannis Cherouvim 7012f4ba93
chore: Use actual target repo URLs.
I assume that this repo was previously owned by `Dummerle`.
2024-05-28 06:17:38 +03:00
loathingKernel ddb619c355 ImageSize: Increase wide image base size
Rename `Icon` preset to `Smallest` to allow the `Icon` name to be reused
2024-05-27 15:37:21 +03:00
loathingKernel 99bcdbaaf0 ImageManager: Download and prepare wide images too 2024-05-27 15:33:44 +03:00
loathingKernel 5909145377 RareGame: Use tz aware datetime in metadata 2024-05-27 15:11:12 +03:00
loathingKernel a53293f279 Rare: Replace utcnow() due to deprecation notice 2024-05-27 14:21:43 +03:00
loathingKernel ad5ecc3d58 OverlaySettings: Add fps_limit,vsync,gl_vsync MangoHud options 2024-05-27 14:12:05 +03:00
loathingKernel ef1dd2e0fe WineResolver: Quote game titles for visibility 2024-05-25 09:21:18 +03:00
loathingKernel cc7a2185a3 CompatUtils: Remove extraneous space 2024-05-25 09:05:43 +03:00
loathingKernel 52c9b1bbdf CompatSteam: Search in config for library folders manifest 2024-05-25 08:51:19 +03:00
loathingKernel 3c52fe90ad CompatUtils: Stricter platform checks 2024-05-25 08:50:36 +03:00
loathingKernel 42a799c7bc LibraryHeadBar: Fix LibraryFilter always reverting to default at start. 2024-05-25 08:50:05 +03:00
loathingKernel 015eeb2710 SteamShortcuts: Implement some basic handling to export games to Steam
as non-steam games.
2024-05-24 13:53:05 +03:00
loathingKernel e1afbd879b ImageManager: Use 'tall' instead of 'card' for the names 2024-05-24 13:51:22 +03:00
loathingKernel f020b0ae39 SelectViewWidget: Move it next to GameListHeadBar
Finally delete extra_widgets, finally!
2024-05-20 17:15:42 +03:00
loathingKernel c216689aaf ButtonLineEdit: Move into its own file under rare/widgets 2024-05-20 17:02:50 +03:00
loathingKernel 90b8d6a541 Utils: Remove deprecated widgets
WaitingSpinner replaced by LoadingWidget
ImageLabel replaced by ImageWidget
2024-05-20 16:48:18 +03:00
loathingKernel e0a0c7ee5d DiscordRPC: Fix Rare not updating message when running a game 2024-05-20 16:03:47 +03:00
loathingKernel 125709b53b Rare: Refactor Discord RPC file, object and variable names 2024-05-20 14:36:43 +03:00
loathingKernel 826d116dd6 QJsonModel: Reformat 2024-05-20 13:44:45 +03:00
loathingKernel 5051ead0ca ImageManager: Add wide image filenames 2024-05-20 12:52:20 +03:00
loathingKernel 21dd30c40a ImageManager: Possibly fix segmentation fault when updating images 2024-05-20 10:04:23 +03:00
loathingKernel 3f67ce515f RareLauncher: Print debug when running pre-launch command 2024-05-20 08:07:29 +03:00
loathingKernel cded2e561c RareLauncher: Always show console when running detached games 2024-05-20 08:07:29 +03:00
loathingKernel a5c7c88716 RareLauncher: Start the pre launch command as detached if we don't wait 2024-05-20 08:07:29 +03:00
loathingKernel f44413d17c RareLauncher: Use better defaults for LaunchArgs 2024-05-20 08:07:29 +03:00
loathingKernel 23fac230df RareLauncher: Check if dry running before anything else 2024-05-20 08:07:29 +03:00
loathingKernel 5e9abc46be RareSettings: Update string 2024-05-20 08:07:29 +03:00
Stelios Tsampas 94c2f765f3
Merge pull request #407 from loathingKernel/develop
RareLauncher: Fix console window wrapping and skip DLC check for launchable addons
2024-05-19 15:29:36 +03:00
loathingKernel 71348dbf23 RareLauncher: Refactor signal name 2024-05-19 15:19:38 +03:00
loathingKernel 2396f0fb83 RareLauncher: Allow running launchable addons 2024-05-19 15:15:13 +03:00
loathingKernel 6dd5d2f546 GameDetails: Fix form growth policy 2024-05-19 14:52:34 +03:00
loathingKernel a4db64082f RareLauncher: Fix word wrapping in console window 2024-05-19 13:11:56 +03:00
Stelios Tsampas 50ff2ae4fd
Merge pull request #405 from loathingKernel/develop
Support paths with spaces in pre launch command field
2024-05-18 18:22:20 +03:00
loathingKernel 0ac4cf5a7c LaunchSettingsBase: Use shlex.split validate the pre-launch command 2024-05-18 18:16:51 +03:00
loathingKernel 259bc98eb9 PreLaunch: Quote the selected file path if it contains spaces. 2024-05-18 18:15:25 +03:00
loathingKernel 6594d65be1 RareGame: Improve launch message 2024-05-17 23:50:02 +03:00
Stelios Tsampas 42341d5c81
Merge pull request #404 from RareDevs/loathingKernel-patch-1
Create codeql.yml
2024-05-17 23:42:43 +03:00
Stelios Tsampas 4dea963154
Merge pull request #403 from loathingKernel/develop
RareLauncher: Fix wrong arguments in pre-launch command `QProcess.start()`
2024-05-17 23:40:24 +03:00
Stelios Tsampas 87b0ae0540
Create codeql.yml 2024-05-17 23:36:22 +03:00
loathingKernel 9b476afe8c RareLauncher: Fix wrong arguments in pre-launch command QProcess.start() 2024-05-17 23:29:35 +03:00
loathingKernel 95e760791b DetailsWidget: Rename to StoreDetailsWidget
In continuation of the previous change, prepend `Store` to the name to
specify the difference.

The goal is to add `StoreDetailsWidget` as a second view in `GameDetails`
with information sourced from the Epic Games Store.
2024-05-17 13:34:24 +03:00
loathingKernel fec4e3c0e1 GameDetails: Refactor file structure
`GameInfo` has been renamed to `GameDetails` to align it with
the similar `StoreDetails` page while making the difference clearer.

Remove the `game_` prefix from the file names to reduce noise. The path
should be enough to provide scope.
2024-05-17 13:31:48 +03:00
Stelios Tsampas fb736fa47b
Merge pull request #401 from loathingKernel/develop
Address pylint errors
2024-05-16 19:32:44 +03:00
loathingKernel 3b721fdd13 WinePathResolver: Disable pylint check (possibly-used-before-assignment) 2024-05-16 19:29:14 +03:00
loathingKernel f9cc1b48f1 GameSettings: Fix pylint errors (possibly-used-before-assignment) 2024-05-16 19:27:59 +03:00
loathingKernel 9017826b16 StoreAPI: Log exception 2024-05-16 13:58:59 +03:00
loathingKernel 4bb1fb10ee SteamGrades: Print an error message instead of the exception 2024-05-16 13:58:10 +03:00
loathingKernel beb4f6c310 Workflows: Update Windows packages to python 3.12
Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com>
2024-05-16 13:57:30 +03:00
loathingKernel 337d753d0c Workflows: Disable fail-fast for pylint checks 2024-05-16 13:38:16 +03:00
loathingKernel 4497a1c712 Project: Use vdf package on all platforms
Useful on Windows too, to export games to Steam as "Non-Steam Game"
2024-05-16 13:32:00 +03:00
loathingKernel 07fa7890cf RareGame: Update protondb grade every 3 days even if it already exists. 2024-05-16 13:18:05 +03:00
loathingKernel 1d8ae15ea9 SteamGrades: Fix exception when trying to get file timestamp
if the file does not exist.

At the same time fix some pylint warnings.
2024-05-16 13:15:48 +03:00
loathingKernel b9d034eef7 Project: Add pylint configuration file 2024-05-16 11:42:32 +03:00
Stelios Tsampas 9f67f930a9
Merge pull request #400 from loathingKernel/develop
LoadingWidget: Start playing movie once the widget is visible if `autostart` is enabled
2024-05-15 22:50:02 +03:00
loathingKernel 8ebcc3a700 LoadingWidget: Start playing movie once the widget is visible if autostart is enabled
Fixes a subtle bug that would cause increased CPU usage due to spawning
multiple singleshot times with very short timeouts (15ms) until the Store
tab was loaded.
2024-05-15 22:48:52 +03:00
Stelios Tsampas 0ddd5a0674
Merge pull request #399 from loathingKernel/develop
WineResolver: Do not unset 'DISPLAY' when running silently
2024-05-15 15:18:14 +03:00
loathingKernel 0c848c41b0 WineResolver: Do not unset 'DISPLAY' when running silently
When unsetting DISPLAY, Wine hangs after executing a command, and doesn't
allow for other instances of wine to start, i.e. games do not launch, with
a cryptic error. This also fixes the previously observed issues with
`winepath.exe` and `reg.exe` never exiting.

This is likely due to the newly introduced Wayland driver. This behavior
can be observed when Wayland support is compiled into Wine and the wayland
driver is enabled in the prefix's registry. In this case, if running under
X11 and DISPLAY is not set, Wine will hang, and the process never returns.
2024-05-15 15:13:51 +03:00
Stelios Tsampas 8f018cb162
Merge pull request #396 from ARez2/main
Fix typo in README
2024-04-13 06:40:38 +03:00
ARez 34abca2a4e
Fix typo in README 2024-04-13 01:34:09 +02:00
Stelios Tsampas cb919ed69f
Merge pull request #392 from loathingKernel/next
commands: add `__init__.py` for module discovery
2024-03-06 17:59:27 +02:00
loathingKernel 1c886535a5
commands: add __init__.py for module discovery 2024-03-06 17:57:48 +02:00
80 changed files with 2430 additions and 1123 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -7,9 +7,9 @@
Rare is a graphical interface for Legendary, a command line alternative to Epic Games launcher, based on PyQt5
<div align="center">
<img src="https://github.com/Dummerle/Rare/blob/main/rare/resources/images/Rare_nonsquared.png?raw=true" alt="Logo" width="200"/>
<img src="https://github.com/RareDevs/Rare/blob/main/rare/resources/images/Rare_nonsquared.png?raw=true" alt="Logo" width="200"/>
<p><i>Logo by <a href="https://github.com/MultisampledNight">@MultisampledNight</a> available
<a href="https://github.com/Dummerle/Rare/blob/main/rare/resources/images/">here</a>,
<a href="https://github.com/RareDevs/Rare/blob/main/rare/resources/images/">here</a>,
licensed under CC BY-SA 4.0</i></p>
</div>
@ -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
@ -76,7 +76,7 @@ There are some AUR packages available:
#### Debian based
- DUR package: [rare](https://mpr.hunterwittenborn.com/packages/rare)
- `.deb` file in [releases page](https://github.com/Dummerle/Rare/releases)
- `.deb` file in [releases page](https://github.com/RareDevs/Rare/releases)
**Note**:
- pypresence is an optional package. You can install it from [DUR](https://mpr.hunterwittenborn.com/packages/python3-pypresence) or with pip.
@ -85,7 +85,7 @@ There are some AUR packages available:
### macOS
There is a `.dmg` file available in [releases page](https://github.com/Dummerle/Rare/releases).
There is a `.dmg` file available in [releases page](https://github.com/RareDevs/Rare/releases).
**Note**: When you launch it, you will see an error, that the package is from an unknown source. You have to enable it manually in `Settings -> Security and Privacy`. Otherwise, Gatekeeper will block Rare from running.
@ -94,9 +94,9 @@ You can also use `pip`.
### Windows
There is an `.msi` installer available in [releases page](https://github.com/Dummerle/Rare/releases).
There is an `.msi` installer available in [releases page](https://github.com/RareDevs/Rare/releases).
There is also a semi-portable `.zip` archive in [releases page](https://github.com/Dummerle/Rare/releases) that lets you run Rare without installing it.
There is also a semi-portable `.zip` archive in [releases page](https://github.com/RareDevs/Rare/releases) that lets you run Rare without installing it.
**Important**: On recent version of Windows you should have MSVC 2015 installed, you can get it from [here](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022)
@ -116,12 +116,12 @@ You can install Rare with the following one-liner:
### Packages
In [releases page](https://github.com/Dummerle/Rare/releases), AppImages are available for Linux, a .msi file for windows and a .dmg
In [releases page](https://github.com/RareDevs/Rare/releases), AppImages are available for Linux, a .msi file for windows and a .dmg
file for macOS.
### Latest development version
In the [actions](https://github.com/Dummerle/Rare/actions) tab you can find packages for the latest commits.
In the [actions](https://github.com/RareDevs/Rare/actions) tab you can find packages for the latest commits.
**Note**: They might be unstable and likely broken.

643
pylintrc Normal file
View 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

View file

View file

@ -1,5 +1,6 @@
import json
import platform
import shlex
import subprocess
import time
import traceback
@ -8,7 +9,8 @@ from logging import getLogger
from signal import signal, SIGINT, SIGTERM, strsignal
from typing import Optional
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt, pyqtSlot
from PyQt5 import sip
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt, pyqtSlot, QTimer
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
from PyQt5.QtWidgets import QApplication
@ -34,15 +36,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,10 +77,15 @@ 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("Running pre-launch command %s, %s", pre_launch_command[0], pre_launch_command[1:])
if launch_args.pre_launch_wait:
proc.start(pre_launch_command[0], pre_launch_command[1:])
self.logger.debug("Waiting for pre-launch command to finish")
proc.waitForFinished(-1)
else:
proc.startDetached(pre_launch_command[0], pre_launch_command[1:])
return launch_args
@ -146,7 +153,10 @@ class RareLauncher(RareApp):
language = self.settings.value(*options.language)
self.load_translator(language)
if QSettings(self).value(*options.log_games):
if (
QSettings(self).value(*options.log_games)
or (game.app_name in DETACHED_APP_NAMES and platform.system() == "Windows")
):
self.console = ConsoleDialog(game.app_title)
self.console.show()
@ -169,15 +179,19 @@ class RareLauncher(RareApp):
self.success = True
self.start_time = time.time()
# This launches the application after it has been instantiated.
# The timer's signal will be serviced once we call `exec()` on the application
QTimer.singleShot(0, self.start)
@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")
)
@ -278,9 +292,31 @@ class RareLauncher(RareApp):
self.console.set_env(args.environment)
self.start_time = time.time()
if self.args.dry_run:
self.logger.info("Dry run %s (%s)", self.rgame.app_title, self.rgame.app_name)
self.logger.info("%s %s", args.executable, " ".join(args.arguments))
if self.console:
self.console.log(f"Dry run {self.rgame.app_title} ({self.rgame.app_name})")
self.console.log(f"{shlex.join([args.executable] + args.arguments)}")
self.console.accept_close = True
print(shlex.join([args.executable] + args.arguments))
self.stop()
return
if args.is_origin_game:
QDesktopServices.openUrl(QUrl(args.executable))
self.stop() # stop because it is no subprocess
self.stop() # stop because it is not a subprocess
return
self.logger.debug("Launch command %s, %s", args.executable, " ".join(args.arguments))
self.logger.debug("Working directory %s", args.working_directory)
if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
if self.console:
self.console.log(f"Launching as a detached process")
subprocess.Popen([args.executable] + args.arguments, cwd=args.working_directory,
env={i: args.environment.value(i) for i in args.environment.keys()})
self.stop() # stop because we do not attach to the output
return
if args.working_directory:
@ -293,23 +329,7 @@ class RareLauncher(RareApp):
new_state=StateChangedModel.States.started
)
))
if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
self.game_process.deleteLater()
if self.console:
self.console.log("Launching game as a detached process")
subprocess.Popen([args.executable] + args.arguments, cwd=args.working_directory,
env={i: args.environment.value(i) for i in args.environment.keys()})
self.stop()
return
if self.args.dry_run:
self.logger.info("Dry run activated")
if self.console:
self.console.log(f"{args.executable} {' '.join(args.arguments)}")
self.console.log(f"Do not start {self.rgame.app_name}")
self.console.accept_close = True
print(args.executable, " ".join(args.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 +343,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)
@ -355,23 +375,22 @@ class RareLauncher(RareApp):
self.console.log("Uploading saves...")
self.start_prepare(action)
def start(self, args: InitArgs):
if not args.offline:
def start(self):
if not self.args.offline:
try:
if not self.core.login():
raise ValueError("You are not logged in")
except ValueError:
# automatically launch offline if available
self.logger.error("Not logged in. Trying to launch the game in offline mode")
args.offline = True
self.args.offline = True
if not args.offline and self.rgame.auto_sync_saves:
if not self.args.offline and self.rgame.auto_sync_saves:
self.logger.info("Start sync worker")
worker = SyncCheckWorker(self.core, self.rgame)
worker.signals.error_occurred.connect(self.error_occurred)
worker.signals.sync_state_ready.connect(self.sync_ready)
QThreadPool.globalInstance().start(worker)
return
else:
self.start_prepare()
@ -384,20 +403,26 @@ class RareLauncher(RareApp):
self.game_process.finished.disconnect()
if self.game_process.receivers(self.game_process.errorOccurred):
self.game_process.errorOccurred.disconnect()
except TypeError as e:
self.logger.error(f"Failed to disconnect process signals: {e}")
except (TypeError, RuntimeError) as e:
self.logger.error("Failed to disconnect process signals: %s", e)
self.logger.info("Stopping server")
if self.game_process.state() != QProcess.NotRunning:
self.game_process.kill()
exit_code = self.game_process.exitCode()
self.game_process.deleteLater()
self.logger.info("Stopping server %s", self.server.socketDescriptor())
try:
self.server.close()
self.server.deleteLater()
except RuntimeError:
pass
except RuntimeError as e:
self.logger.error("Error occured while stopping server: %s", e)
self.processEvents()
if not self.console:
self.exit()
self.exit(exit_code)
else:
self.console.on_process_exit(self.rgame.app_name, 0)
self.console.on_process_exit(self.rgame.app_title, exit_code)
def launch(args: Namespace) -> int:
@ -412,7 +437,7 @@ def launch(args: Namespace) -> int:
# This prevents ghost QLocalSockets, which block the name, which makes it unable to start
# No handling for SIGKILL
def sighandler(s, frame):
app.logger.info(f"{strsignal(s)} received. Stopping")
app.logger.info("%s received. Stopping", strsignal(s))
app.stop()
app.exit(1)
signal(SIGINT, sighandler)
@ -422,7 +447,13 @@ def launch(args: Namespace) -> int:
app.stop()
app.exit(1)
return 1
app.start(args)
# app.exit_app.connect(lambda: app.exit(0))
return app.exec_()
try:
exit_code = app.exec()
except Exception as e:
app.logger.error("Unhandled error %s", e)
exit_code = 1
finally:
if not sip.isdeleted(app.server):
app.server.close()
return exit_code

View file

@ -1,3 +1,4 @@
import os
from typing import Union
from PyQt5.QtCore import QProcessEnvironment, pyqtSignal, QSize, Qt
@ -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")

View file

@ -2,7 +2,7 @@ import os
import platform
import shutil
from argparse import Namespace
from dataclasses import dataclass
from dataclasses import dataclass, field
from logging import getLogger
from typing import List
@ -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):
@ -43,8 +43,8 @@ class InitArgs(Namespace):
@dataclass
class LaunchArgs:
executable: str = ""
arguments: List[str] = None
working_directory: str = None
arguments: List[str] = field(default_factory=list)
working_directory: str = ""
environment: QProcessEnvironment = None
pre_launch_command: str = ""
pre_launch_wait: bool = False
@ -129,9 +129,7 @@ def get_game_params(rgame: RareGameSlim, args: InitArgs, launch_args: LaunchArgs
launch_args.environment.insert(name, value)
full_params.extend(params.launch_command)
full_params.append(
os.path.join(params.game_directory, params.game_executable)
)
full_params.append(os.path.join(params.game_directory, params.game_executable))
full_params.extend(params.game_parameters)
full_params.extend(params.egl_parameters)
full_params.extend(params.user_parameters)
@ -156,7 +154,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")

View file

@ -1,7 +1,7 @@
import os
import shutil
from argparse import Namespace
from datetime import datetime, timezone
from datetime import datetime, timezone, UTC
from typing import Optional
import requests.exceptions
@ -60,7 +60,7 @@ class Rare(RareApp):
def poke_timer(self):
dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]).replace(tzinfo=timezone.utc)
dt_now = datetime.utcnow().replace(tzinfo=timezone.utc)
dt_now = datetime.now(UTC)
td = abs(dt_exp - dt_now)
self.relogin_timer.start(int(td.total_seconds() - 60) * 1000)
self.logger.info(f"Renewed session expires at {self.core.lgd.userdata['expires_at']}")

View file

@ -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,
@ -101,9 +101,9 @@ class MainWindow(QMainWindow):
if not self.args.offline:
try:
from rare.utils.rpc import DiscordRPC
from rare.utils.discord_rpc import DiscordRPC
self.rpc = DiscordRPC()
self.discord_rpc = DiscordRPC()
except ModuleNotFoundError:
logger.warning("Discord RPC module not found")

View file

@ -5,7 +5,7 @@ from rare.shared import RareCore, LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.utils.misc import qta_icon, ExitCodes
from .account import AccountWidget
from .downloads import DownloadsTab
from .games import GamesTab
from .games import GamesLibrary
from .settings import SettingsTab
from .settings.debug import DebugSettings
from .store import StoreTab
@ -28,7 +28,7 @@ class MainTabWidget(QTabWidget):
self.setTabBar(self.tab_bar)
# Generate Tabs
self.games_tab = GamesTab(self)
self.games_tab = GamesLibrary(self)
self.games_index = self.addTab(self.games_tab, self.tr("Games"))
# Downloads Tab after Games Tab to use populated RareCore games list

View file

@ -14,6 +14,7 @@ from rare.components.dialogs.install_dialog import InstallDialog
from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame
from rare.models.image import ImageSize
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
from rare.models.options import options
from rare.shared import RareCore
@ -209,9 +210,9 @@ class DownloadsTab(QWidget):
self.__thread = dl_thread
self.download_widget.ui.kill_button.setDisabled(False)
self.download_widget.ui.dl_name.setText(item.download.game.app_title)
self.download_widget.setPixmap(
RareCore.instance().image_manager().get_pixmap(rgame.app_name, True)
)
self.download_widget.setPixmap(self.rcore.image_manager().get_pixmap(
rgame.app_name, ImageSize.Wide, True
))
self.signals.application.notify.emit(
self.tr("Downloads"),

View file

@ -29,7 +29,7 @@ class DownloadWidget(ImageWidget):
def prepare_pixmap(self, pixmap: QPixmap) -> QPixmap:
device: QImage = QImage(
pixmap.size().width() * 3,
pixmap.size().width() * 1,
int(self.sizeHint().height() * pixmap.devicePixelRatioF()) + 1,
QImage.Format_ARGB32_Premultiplied,
)
@ -38,9 +38,9 @@ class DownloadWidget(ImageWidget):
painter.fillRect(device.rect(), brush)
# the gradient could be cached and reused as it is expensive
gradient = QLinearGradient(0, 0, device.width(), 0)
gradient.setColorAt(0.15, Qt.transparent)
gradient.setColorAt(0.02, Qt.transparent)
gradient.setColorAt(0.5, Qt.black)
gradient.setColorAt(0.85, Qt.transparent)
gradient.setColorAt(0.98, Qt.transparent)
painter.setCompositionMode(QPainter.CompositionMode_DestinationIn)
painter.fillRect(device.rect(), gradient)
painter.end()

View file

@ -34,7 +34,7 @@ class QueueInfoWidget(QWidget):
self.image_manager = ImageManagerSingleton()
self.image = ImageWidget(self)
self.image.setFixedSize(ImageSize.Icon)
self.image.setFixedSize(ImageSize.LibraryIcon)
self.ui.image_layout.addWidget(self.image)
self.ui.queue_info_layout.setAlignment(Qt.AlignTop)
@ -50,7 +50,7 @@ class QueueInfoWidget(QWidget):
if old_igame:
self.ui.title.setText(old_igame.title)
self.image.setPixmap(self.image_manager.get_pixmap(old_igame.app_name, color=True))
self.image.setPixmap(self.image_manager.get_pixmap(old_igame.app_name, ImageSize.LibraryIcon))
def update_information(self, game, igame, analysis, old_igame):
self.ui.title.setText(game.app_title)
@ -60,7 +60,7 @@ class QueueInfoWidget(QWidget):
self.ui.local_version.setText(elide_text(self.ui.local_version, igame.version))
self.ui.dl_size.setText(format_size(analysis.dl_size) if analysis else "")
self.ui.install_size.setText(format_size(analysis.install_size) if analysis else "")
self.image.setPixmap(self.image_manager.get_pixmap(game.app_name, color=True))
self.image.setPixmap(self.image_manager.get_pixmap(game.app_name, ImageSize.LibraryIcon))
class UpdateWidget(QFrame):

View file

@ -17,15 +17,15 @@ from .game_info import GameInfoTabs
from .game_widgets import LibraryWidgetController, LibraryFilter, LibraryOrder, LibraryView
from .game_widgets.icon_game_widget import IconGameWidget
from .game_widgets.list_game_widget import ListGameWidget
from .head_bar import GameListHeadBar
from .head_bar import LibraryHeadBar
from .integrations import IntegrationsTabs
logger = getLogger("GamesTab")
logger = getLogger("GamesLibrary")
class GamesTab(QStackedWidget):
class GamesLibrary(QStackedWidget):
def __init__(self, parent=None):
super(GamesTab, self).__init__(parent=parent)
super(GamesLibrary, self).__init__(parent=parent)
self.rcore = RareCore.instance()
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
@ -37,7 +37,7 @@ class GamesTab(QStackedWidget):
games_page_layout = QVBoxLayout(self.games_page)
self.addWidget(self.games_page)
self.head_bar = GameListHeadBar(parent=self.games_page)
self.head_bar = LibraryHeadBar(parent=self.games_page)
self.head_bar.goto_import.connect(self.show_import)
self.head_bar.goto_egl_sync.connect(self.show_egl_sync)
self.head_bar.goto_eos_ubisoft.connect(self.show_eos_ubisoft)
@ -123,6 +123,7 @@ class GamesTab(QStackedWidget):
logger.warning("Excluding %s from the game list", rgame.app_title)
continue
self.filter_games(self.head_bar.current_filter())
self.order_games(self.head_bar.current_order())
self.update_count_games_label()
def add_library_widget(self, rgame: RareGame):

View file

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

View file

@ -19,7 +19,7 @@ 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.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
@ -28,13 +28,13 @@ 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")
@ -58,7 +58,7 @@ class GameInfo(QWidget, SideTabContents):
self.rgame: Optional[RareGame] = None
self.image = ImageWidget(self)
self.image.setFixedSize(ImageSize.Display)
self.image.setFixedSize(ImageSize.DisplayTall)
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop)
self.ui.install_button.clicked.connect(self.__on_install)
@ -270,7 +270,7 @@ class GameInfo(QWidget, SideTabContents):
@pyqtSlot()
def __update_widget(self):
""" React to state updates from RareGame """
self.image.setPixmap(self.rgame.get_pixmap(True))
self.image.setPixmap(self.rgame.get_pixmap(ImageSize.DisplayTall, True))
self.ui.lbl_version.setDisabled(self.rgame.is_non_asset)
self.ui.version.setDisabled(self.rgame.is_non_asset)

View file

@ -6,8 +6,8 @@ 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, qta_icon
@ -23,28 +23,28 @@ class GameDlcWidget(QFrame):
self.rdlc = rdlc
self.image = ImageWidget(self)
self.image.setFixedSize(ImageSize.Icon)
self.image.setFixedSize(ImageSize.LibraryIcon)
self.ui.dlc_layout.insertWidget(0, self.image)
self.ui.dlc_name.setText(rdlc.app_title)
self.ui.version.setText(rdlc.version)
self.ui.app_name.setText(rdlc.app_name)
self.image.setPixmap(rdlc.pixmap)
# self.image.setPixmap(rdlc.get_pixmap_icon(rdlc.is_installed))
self.__update()
rdlc.signals.widget.update.connect(self.__update)
@pyqtSlot()
def __update(self):
self.ui.action_button.setEnabled(self.rdlc.is_idle)
self.image.setPixmap(self.rdlc.pixmap)
self.image.setPixmap(self.rdlc.get_pixmap(ImageSize.LibraryIcon, self.rdlc.is_installed))
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
if self.rdlc.pixmap.isNull():
self.rdlc.load_pixmap()
if not self.rdlc.has_pixmap:
self.rdlc.load_pixmaps()
super().showEvent(a0)
@ -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()

View file

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

View file

@ -122,7 +122,7 @@ class IconViewContainer(ViewContainer):
elif order_by == LibraryOrder.RECENT:
# Sort by recently played
self.layout().sort(
key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.metadata.last_played),
key=lambda x: (x.widget().rgame.is_installed, not x.widget().rgame.is_non_asset, x.widget().rgame.metadata.last_played),
reverse=True,
)
else:
@ -164,7 +164,7 @@ class ListViewContainer(ViewContainer):
)
elif order_by == LibraryOrder.RECENT:
list_widgets.sort(
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.metadata.last_played),
key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.metadata.last_played),
reverse=True,
)
else:

View file

@ -2,18 +2,22 @@ import platform
import random
from logging import getLogger
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent, QTimer
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent
from PyQt5.QtGui import QMouseEvent, QShowEvent, QPaintEvent
from PyQt5.QtWidgets import QMessageBox, QAction
from rare.models.game import RareGame
from rare.shared import (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
ArgumentsSingleton,
ImageManagerSingleton,
)
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton, ImageManagerSingleton
from rare.utils.paths import desktop_links_supported, desktop_link_path, create_desktop_link
from rare.utils.steam_shortcuts import (
steam_shortcuts_supported,
steam_shortcut_exists,
remove_steam_shortcut,
remove_steam_coverart,
add_steam_shortcut,
add_steam_coverart,
save_steam_shortcuts,
)
from .library_widget import LibraryWidget
logger = getLogger("GameWidget")
@ -40,13 +44,14 @@ class GameWidget(LibraryWidget):
self.install_action.triggered.connect(self._install)
self.desktop_link_action = QAction(self)
self.desktop_link_action.triggered.connect(
lambda: self._create_link(self.rgame.folder_name, "desktop")
)
self.desktop_link_action.triggered.connect(lambda: self._create_link(self.rgame.folder_name, "desktop"))
self.menu_link_action = QAction(self)
self.menu_link_action.triggered.connect(
lambda: self._create_link(self.rgame.folder_name, "start_menu")
self.menu_link_action.triggered.connect(lambda: self._create_link(self.rgame.folder_name, "start_menu"))
self.steam_shortcut_action = QAction(self)
self.steam_shortcut_action.triggered.connect(
lambda: self._create_steam_shortcut(self.rgame.app_name, self.rgame.app_title)
)
self.reload_action = QAction(self.tr("Reload Image"), self)
@ -58,24 +63,15 @@ class GameWidget(LibraryWidget):
self.update_actions()
# signals
self.rgame.signals.widget.update.connect(lambda: self.setPixmap(self.rgame.pixmap))
self.rgame.signals.widget.update.connect(self.update_pixmap)
self.rgame.signals.widget.update.connect(self.update_buttons)
self.rgame.signals.widget.update.connect(self.update_state)
self.rgame.signals.game.installed.connect(self.update_actions)
self.rgame.signals.game.uninstalled.connect(self.update_actions)
self.rgame.signals.progress.start.connect(
lambda: self.showProgress(
self.image_manager.get_pixmap(self.rgame.app_name, True),
self.image_manager.get_pixmap(self.rgame.app_name, False)
)
)
self.rgame.signals.progress.update.connect(
lambda p: self.updateProgress(p)
)
self.rgame.signals.progress.finish.connect(
lambda e: self.hideProgress(e)
)
self.rgame.signals.progress.start.connect(self.start_progress)
self.rgame.signals.progress.update.connect(lambda p: self.updateProgress(p))
self.rgame.signals.progress.finish.connect(lambda e: self.hideProgress(e))
self.state_strings = {
RareGame.State.IDLE: "",
@ -88,7 +84,7 @@ class GameWidget(LibraryWidget):
"has_update": self.tr("Update available"),
"needs_verification": self.tr("Needs verification"),
"not_can_launch": self.tr("Can't launch"),
"save_not_up_to_date": self.tr("Save is not up-to-date")
"save_not_up_to_date": self.tr("Save is not up-to-date"),
}
self.hover_strings = {
@ -104,19 +100,18 @@ class GameWidget(LibraryWidget):
# lk: abstract class for typing, the `self.ui` attribute should be used
# lk: by the Ui class in the children. It must contain at least the same
# lk: attributes as `GameWidgetUi` class
__slots__ = "ui"
__slots__ = "ui", "update_pixmap", "start_progress"
def paintEvent(self, a0: QPaintEvent) -> None:
if not self.visibleRegion().isNull() and self.rgame.pixmap.isNull():
if not self.visibleRegion().isNull() and not self.rgame.has_pixmap:
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):
self.killTimer(a0.timerId())
self.rgame.load_pixmap()
self.rgame.load_pixmaps()
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
@ -132,9 +127,11 @@ class GameWidget(LibraryWidget):
self.ui.status_label.setText(self.state_strings["needs_verification"])
elif not self.rgame.can_launch and self.rgame.is_installed:
self.ui.status_label.setText(self.state_strings["not_can_launch"])
elif self.rgame.igame and (
self.rgame.game.supports_cloud_saves or self.rgame.game.supports_mac_cloud_saves
) and not self.rgame.is_save_up_to_date:
elif (
self.rgame.igame
and (self.rgame.game.supports_cloud_saves or self.rgame.game.supports_mac_cloud_saves)
and not self.rgame.is_save_up_to_date
):
self.ui.status_label.setText(self.state_strings["save_not_up_to_date"])
else:
self.ui.status_label.setText(self.state_strings[self.rgame.state])
@ -149,6 +146,8 @@ class GameWidget(LibraryWidget):
self.ui.launch_btn.setVisible(self.rgame.is_installed)
self.ui.launch_btn.setEnabled(self.rgame.can_launch)
self.steam_shortcut_action.setEnabled(self.rgame.has_pixmap)
@pyqtSlot()
def update_actions(self):
for action in self.actions():
@ -171,6 +170,13 @@ class GameWidget(LibraryWidget):
self.menu_link_action.setText(self.tr("Create Start Menu link"))
self.addAction(self.menu_link_action)
if steam_shortcuts_supported() and self.rgame.is_installed:
if steam_shortcut_exists(self.rgame.app_name):
self.steam_shortcut_action.setText(self.tr("Remove from Steam"))
else:
self.steam_shortcut_action.setText(self.tr("Add to Steam"))
self.addAction(self.steam_shortcut_action)
self.addAction(self.reload_action)
if self.rgame.is_installed and not self.rgame.is_origin:
self.addAction(self.uninstall_action)
@ -223,9 +229,7 @@ class GameWidget(LibraryWidget):
offline = True
if self.rgame.has_update:
skip_version_check = True
self.rgame.launch(
offline=offline, skip_update_check=skip_version_check
)
self.rgame.launch(offline=offline, skip_update_check=skip_version_check)
@pyqtSlot()
def _install(self):
@ -235,7 +239,8 @@ class GameWidget(LibraryWidget):
def _uninstall(self):
self.show_info.emit(self.rgame)
def _create_link(self, name, link_type):
@pyqtSlot(str, str)
def _create_link(self, name: str, link_type: str):
if not desktop_links_supported():
QMessageBox.warning(
self,
@ -258,16 +263,18 @@ class GameWidget(LibraryWidget):
except PermissionError:
QMessageBox.warning(self, "Error", "Could not create shortcut.")
return
if link_type == "desktop":
self.desktop_link_action.setText(self.tr("Remove Desktop link"))
elif link_type == "start_menu":
self.menu_link_action.setText(self.tr("Remove Start Menu link"))
else:
if shortcut_path.exists():
shortcut_path.unlink(missing_ok=True)
self.update_actions()
if link_type == "desktop":
self.desktop_link_action.setText(self.tr("Create Desktop link"))
elif link_type == "start_menu":
self.menu_link_action.setText(self.tr("Create Start Menu link"))
@pyqtSlot(str, str)
def _create_steam_shortcut(self, app_name: str, app_title: str):
if steam_shortcut_exists(app_name):
if shortcut := remove_steam_shortcut(app_name):
remove_steam_coverart(shortcut)
else:
if shortcut := add_steam_shortcut(app_name, app_title):
add_steam_coverart(app_name, shortcut)
save_steam_shortcuts()
self.update_actions()

View file

@ -1,7 +1,7 @@
from logging import getLogger
from typing import Optional
from PyQt5.QtCore import QEvent
from PyQt5.QtCore import QEvent, pyqtSlot
from rare.models.game import RareGame
from rare.models.image import ImageSize
@ -15,7 +15,7 @@ class IconGameWidget(GameWidget):
def __init__(self, rgame: RareGame, parent=None):
super().__init__(rgame, parent)
self.setObjectName(f"{rgame.app_name}")
self.setFixedSize(ImageSize.Library)
self.setFixedSize(ImageSize.LibraryTall)
self.ui = IconWidget()
self.ui.setupUi(self)
@ -34,6 +34,17 @@ class IconGameWidget(GameWidget):
self.ui.launch_btn.installEventFilter(self)
self.ui.install_btn.installEventFilter(self)
@pyqtSlot()
def update_pixmap(self):
self.setPixmap(self.rgame.get_pixmap(ImageSize.LibraryTall, self.rgame.is_installed))
@pyqtSlot()
def start_progress(self):
self.showProgress(
self.rgame.get_pixmap(ImageSize.LibraryTall, True),
self.rgame.get_pixmap(ImageSize.LibraryTall, False)
)
def enterEvent(self, a0: Optional[QEvent] = None) -> None:
if a0 is not None:
a0.accept()

View file

@ -1,6 +1,6 @@
from logging import getLogger
from PyQt5.QtCore import Qt, QEvent, QRect
from PyQt5.QtCore import Qt, QEvent, QRect, pyqtSlot
from PyQt5.QtGui import (
QPalette,
QBrush,
@ -12,6 +12,7 @@ from PyQt5.QtGui import (
)
from rare.models.game import RareGame
from rare.models.image import ImageSize
from rare.utils.misc import format_size
from .game_widget import GameWidget
from .list_widget import ListWidget
@ -50,6 +51,17 @@ class ListGameWidget(GameWidget):
self.ui.launch_btn.installEventFilter(self)
self.ui.install_btn.installEventFilter(self)
@pyqtSlot()
def update_pixmap(self):
self.setPixmap(self.rgame.get_pixmap(ImageSize.LibraryWide, self.rgame.is_installed))
@pyqtSlot()
def start_progress(self):
self.showProgress(
self.rgame.get_pixmap(ImageSize.LibraryWide, True),
self.rgame.get_pixmap(ImageSize.LibraryWide, False)
)
def enterEvent(self, a0: QEvent = None) -> None:
if a0 is not None:
a0.accept()
@ -70,7 +82,7 @@ class ListGameWidget(GameWidget):
def prepare_pixmap(self, pixmap: QPixmap) -> QPixmap:
device: QImage = QImage(
pixmap.size().width() * 3,
pixmap.size().width() * 1,
int(self.sizeHint().height() * pixmap.devicePixelRatioF()) + 1,
QImage.Format_ARGB32_Premultiplied
)
@ -79,9 +91,9 @@ class ListGameWidget(GameWidget):
painter.fillRect(device.rect(), brush)
# the gradient could be cached and reused as it is expensive
gradient = QLinearGradient(0, 0, device.width(), 0)
gradient.setColorAt(0.15, Qt.transparent)
gradient.setColorAt(0.02, Qt.transparent)
gradient.setColorAt(0.5, Qt.black)
gradient.setColorAt(0.85, Qt.transparent)
gradient.setColorAt(0.98, Qt.transparent)
painter.setCompositionMode(QPainter.CompositionMode_DestinationIn)
painter.fillRect(device.rect(), gradient)
painter.end()
@ -104,7 +116,7 @@ class ListGameWidget(GameWidget):
brush = QBrush(self._pixmap)
brush.setTransform(self._transform)
width = int(self._pixmap.width() / self._pixmap.devicePixelRatioF())
origin = self.width() - width
origin = self.width() // 2
painter.setBrushOrigin(origin, 0)
fill_rect = QRect(origin, 0, width, self.height())
painter.fillRect(fill_rect, brush)

View file

@ -1,9 +1,14 @@
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, QSize, Qt
import logging
from PyQt5.QtCore import QSettings, pyqtSlot, QSize, Qt
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import (
QHBoxLayout,
QWidget,
QPushButton,
)
from PyQt5.QtWidgets import (
QLabel,
QPushButton,
QWidget,
QHBoxLayout,
QComboBox,
QMenu,
QAction, QSpacerItem, QSizePolicy,
@ -11,11 +16,11 @@ from PyQt5.QtWidgets import (
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 qta_icon
from rare.widgets.button_edit import ButtonLineEdit
class GameListHeadBar(QWidget):
class LibraryHeadBar(QWidget):
filterChanged = pyqtSignal(object)
orderChanged = pyqtSignal(object)
viewChanged = pyqtSignal(object)
@ -24,7 +29,8 @@ class GameListHeadBar(QWidget):
goto_eos_ubisoft = pyqtSignal()
def __init__(self, parent=None):
super(GameListHeadBar, self).__init__(parent=parent)
super(LibraryHeadBar, self).__init__(parent=parent)
self.logger = logging.getLogger(type(self).__name__)
self.rcore = RareCore.instance()
self.settings = QSettings(self)
@ -47,12 +53,13 @@ class GameListHeadBar(QWidget):
self.filter.addItem(self.tr("Include Unreal"), LibraryFilter.INCLUDE_UE)
try:
_filter = self.settings.value(*options.library_filter)
_filter = LibraryFilter(self.settings.value(*options.library_filter))
if (index := self.filter.findData(_filter, Qt.UserRole)) < 0:
raise ValueError
raise ValueError(f"Filter '{_filter}' is not available")
else:
self.filter.setCurrentIndex(index)
except (TypeError, ValueError):
except (TypeError, ValueError) as e:
self.logger.error("Error while loading library: %s", e)
self.settings.setValue(options.library_filter.key, options.library_filter.default)
_filter = LibraryFilter(options.library_filter.default)
self.filter.setCurrentIndex(self.filter.findData(_filter, Qt.UserRole))
@ -71,10 +78,11 @@ class GameListHeadBar(QWidget):
try:
_order = LibraryOrder(self.settings.value(*options.library_order))
if (index := self.order.findData(_order, Qt.UserRole)) < 0:
raise ValueError
raise ValueError(f"Order '{_order}' is not available")
else:
self.order.setCurrentIndex(index)
except (TypeError, ValueError):
except (TypeError, ValueError) as e:
self.logger.error("Error while loading library: %s", e)
self.settings.setValue(options.library_order.key, options.library_order.default)
_order = LibraryOrder(options.library_order.default)
self.order.setCurrentIndex(self.order.findData(_order, Qt.UserRole))
@ -166,3 +174,43 @@ class GameListHeadBar(QWidget):
data = self.order.itemData(index, Qt.UserRole)
self.orderChanged.emit(data)
self.settings.setValue(options.library_order.key, int(data))
class SelectViewWidget(QWidget):
toggled = pyqtSignal(bool)
def __init__(self, icon_view: bool, parent=None):
super(SelectViewWidget, self).__init__(parent=parent)
self.icon_button = QPushButton(self)
self.icon_button.setObjectName(f"{type(self).__name__}Button")
self.list_button = QPushButton(self)
self.list_button.setObjectName(f"{type(self).__name__}Button")
if icon_view:
self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="orange"))
self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="#eee"))
else:
self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="#eee"))
self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="orange"))
self.icon_button.clicked.connect(self.icon)
self.list_button.clicked.connect(self.list)
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.icon_button)
layout.addWidget(self.list_button)
self.setLayout(layout)
def icon(self):
self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="orange"))
self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="#eee"))
self.toggled.emit(True)
def list(self):
self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="#eee"))
self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="orange"))
self.toggled.emit(False)

View file

@ -6,7 +6,7 @@ from PyQt5.QtCore import QSettings, Qt, pyqtSlot, QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QWidget, QMessageBox
from rare.components.tabs.settings.widgets.rpc import RPCSettings
from rare.components.tabs.settings.widgets.discord_rpc import DiscordRPCSettings
from rare.models.options import options, LibraryView
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
@ -75,8 +75,8 @@ class RareSettings(QWidget):
self.ui.view_combo.setCurrentIndex(0)
self.ui.view_combo.currentIndexChanged.connect(self.on_view_combo_changed)
self.rpc = RPCSettings(self)
self.ui.right_layout.insertWidget(1, self.rpc, alignment=Qt.AlignTop)
self.discord_rpc_settings = DiscordRPCSettings(self)
self.ui.right_layout.insertWidget(1, self.discord_rpc_settings, alignment=Qt.AlignTop)
self.ui.sys_tray.setChecked(self.settings.value(*options.sys_tray))
self.ui.sys_tray.stateChanged.connect(

View file

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

View file

@ -3,34 +3,34 @@ from PyQt5.QtWidgets import QGroupBox
from rare.shared import GlobalSignalsSingleton
from rare.models.options import options
from rare.ui.components.tabs.settings.widgets.rpc import Ui_RPCSettings
from rare.ui.components.tabs.settings.widgets.discord_rpc import Ui_DiscordRPCSettings
class RPCSettings(QGroupBox):
class DiscordRPCSettings(QGroupBox):
def __init__(self, parent):
super(RPCSettings, self).__init__(parent=parent)
self.ui = Ui_RPCSettings()
super(DiscordRPCSettings, self).__init__(parent=parent)
self.ui = Ui_DiscordRPCSettings()
self.ui.setupUi(self)
self.signals = GlobalSignalsSingleton()
self.settings = QSettings()
self.ui.enable.setCurrentIndex(self.settings.value(*options.rpc_enable))
self.ui.enable.setCurrentIndex(self.settings.value(*options.discord_rpc_mode))
self.ui.enable.currentIndexChanged.connect(self.__enable_changed)
self.ui.show_game.setChecked((self.settings.value(*options.rpc_name)))
self.ui.show_game.setChecked((self.settings.value(*options.discord_rpc_game)))
self.ui.show_game.stateChanged.connect(
lambda: self.settings.setValue(options.rpc_name.key, self.ui.show_game.isChecked())
lambda: self.settings.setValue(options.discord_rpc_game.key, self.ui.show_game.isChecked())
)
self.ui.show_os.setChecked((self.settings.value(*options.rpc_os)))
self.ui.show_os.setChecked((self.settings.value(*options.discord_rpc_os)))
self.ui.show_os.stateChanged.connect(
lambda: self.settings.setValue(options.rpc_os.key, self.ui.show_os.isChecked())
lambda: self.settings.setValue(options.discord_rpc_os.key, self.ui.show_os.isChecked())
)
self.ui.show_time.setChecked((self.settings.value(*options.rpc_time)))
self.ui.show_time.setChecked((self.settings.value(*options.discord_rpc_time)))
self.ui.show_time.stateChanged.connect(
lambda: self.settings.setValue(options.rpc_time.key, self.ui.show_time.isChecked())
lambda: self.settings.setValue(options.discord_rpc_time.key, self.ui.show_time.isChecked())
)
try:
@ -40,5 +40,5 @@ class RPCSettings(QGroupBox):
self.setToolTip(self.tr("Pypresence is not installed"))
def __enable_changed(self, i):
self.settings.setValue(options.rpc_enable.key, i)
self.signals.discord_rpc.apply_settings.emit()
self.settings.setValue(options.discord_rpc_mode.key, i)
self.signals.discord_rpc.update_settings.emit()

View file

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

View file

@ -45,11 +45,11 @@ class OverlayComboBox(QComboBox):
self.setCurrentIndex(0)
def getValue(self) -> Optional[str]:
return f"{self.option}={self.currentText()}" if self.currentIndex() > 0 else None
return f"{self.option}={self.currentData(Qt.UserRole)}" if self.currentIndex() > 0 else None
def setValue(self, options: Dict[str, str]):
if (value := options.get(self.option, None)) is not None:
self.setCurrentText(value)
self.setCurrentIndex(self.findData(value, Qt.UserRole))
options.pop(self.option)
else:
self.setDefault()
@ -93,10 +93,13 @@ class OverlayNumberInput(OverlayLineEdit):
class OverlaySelectInput(OverlayComboBox):
def __init__(self, option: str, values: List, parent=None):
def __init__(self, option: str, values: Tuple, parent=None):
super().__init__(option, parent=parent)
for item in values:
text, data = item
self.addItem(text, data)
# self.addItems([str(v) for v in values])
self.addItems(map(str, values))
# self.addItems(map(str, values))
class ActivationStates(IntEnum):
@ -241,23 +244,42 @@ class DxvkSettings(OverlaySettings):
OverlayCheckBox("api", self.tr("D3D feature level")),
OverlayCheckBox("compiler", self.tr("Compiler activity")),
]
form = [(OverlayNumberInput("scale", 1.0), self.tr("Scale"))]
form = [
(OverlayNumberInput("scale", 1.0), self.tr("Scale"))
]
self.setupWidget(grid, form, "DXVK_HUD", "0", "1")
def update_settings_override(self, state: ActivationStates):
pass
mangohud_position = [
"default",
"top-left",
"top-right",
"middle-left",
"middle-right",
"bottom-left",
"bottom-right",
"top-center",
]
mangohud_position = (
("default", "default"),
("top-left", "top-left"),
("top-right", "top-right"),
("middle-left", "middle-left"),
("middle-right", "middle-right"),
("bottom-left", "bottom-left"),
("bottom-right", "bottom-right"),
("top-center", "top-center"),
)
mangohud_vsync = (
("config", None),
("adaptive", "0"),
("off", "1"),
("mailbox", "2"),
("on", "3"),
)
mangohud_gl_vsync = (
("config", None),
("off", "0"),
("on", "1"),
("half", "2"),
("third", "3"),
("quarter", "4"),
)
class MangoHudSettings(OverlaySettings):
@ -283,6 +305,9 @@ class MangoHudSettings(OverlaySettings):
OverlayCheckBox("gpu_power", self.tr("GPU power consumption")),
]
form = [
(OverlayNumberInput("fps_limit", 0), self.tr("FPS Limit")),
(OverlaySelectInput("vsync", mangohud_vsync), self.tr("Vulkan VSync")),
(OverlaySelectInput("gl_vsync", mangohud_gl_vsync), self.tr("OpenGL VSync")),
(OverlayNumberInput("font_size", 24), self.tr("Font size")),
(OverlaySelectInput("position", mangohud_position), self.tr("Position")),
]

View file

@ -6,7 +6,6 @@ from .api.models.response import CatalogOfferModel
from .landing import LandingWidget, LandingPage
from .search import SearchPage
from .store_api import StoreAPI
from .widgets.details import DetailsWidget
from .wishlist import WishlistPage

View file

@ -1,12 +1,12 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import datetime, timezone, UTC
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)
end_date: datetime = datetime.now(UTC)
def __str__(self):
def fmt_date(date: datetime) -> str:

View file

@ -45,8 +45,8 @@ class ImageUrlModel:
@dataclass
class KeyImagesModel:
key_images: Optional[List[ImageUrlModel]] = None
tall_types = ("DieselStoreFrontTall", "OfferImageTall", "Thumbnail", "ProductLogo", "DieselGameBoxLogo")
wide_types = ("DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo")
tall_types = ("DieselGameBoxTall", "DieselStoreFrontTall", "OfferImageTall", "DieselGameBoxLogo", "Thumbnail", "ProductLogo")
wide_types = ("DieselGameBoxwide", "DieselStoreFrontWide", "OfferImageWide", "DieselGameBox", "VaultClosed", "ProductLogo")
def __getitem__(self, item):
return self.key_images[item]

View file

@ -1,5 +1,5 @@
import datetime
import logging
from datetime import datetime, UTC
from typing import List
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QObject, QEvent
@ -19,7 +19,7 @@ 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 DetailsWidget
from .widgets.details import StoreDetailsWidget
from .widgets.groups import StoreGroup
from .widgets.items import StoreItemWidget
@ -44,7 +44,7 @@ class LandingPage(SlidingStackedWidget, SideTabContents):
self.landing_scroll.widget().setAutoFillBackground(False)
self.landing_scroll.viewport().setAutoFillBackground(False)
self.details_widget = DetailsWidget([], store_api, parent=self)
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)
@ -184,7 +184,7 @@ class LandingWidget(QWidget, SideTabContents):
self.free_games_next.layout().removeWidget(w)
w.deleteLater()
date = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
date = datetime.now(UTC)
free_now = []
free_next = []
for item in free_games:

View file

@ -12,7 +12,7 @@ from PyQt5.QtWidgets import (
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.button_edit import ButtonLineEdit
from rare.widgets.side_tab import SideTabContents
from rare.widgets.sliding_stack import SlidingStackedWidget
from .api.models.query import SearchStoreQuery
@ -20,7 +20,7 @@ from .api.models.response import CatalogOfferModel
from .constants import Constants
from .results import ResultsWidget
from .store_api import StoreAPI
from .widgets.details import DetailsWidget
from .widgets.details import StoreDetailsWidget
logger = logging.getLogger("Shop")
@ -35,7 +35,7 @@ class SearchPage(SlidingStackedWidget, SideTabContents):
self.search_widget.set_title.connect(self.set_title)
self.search_widget.show_details.connect(self.show_details)
self.details_widget = DetailsWidget([], store_api, parent=self)
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)

View file

@ -97,13 +97,13 @@ class StoreAPI(QObject):
except KeyError as e:
if DEBUG():
raise e
logger.error("Free games Api request failed")
logger.exception("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}")
logger.exception(f"Free games API request failed")
handle_func(["error", e])
return

View file

@ -15,7 +15,7 @@ from rare.components.tabs.store.api.models.diesel import DieselProduct, DieselPr
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_DetailsWidget
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
@ -24,15 +24,15 @@ from .image import LoadingImageWidget
logger = logging.getLogger("StoreDetails")
class DetailsWidget(QWidget, SideTabContents):
class StoreDetailsWidget(QWidget, SideTabContents):
back_clicked: pyqtSignal = pyqtSignal()
# TODO Design
def __init__(self, installed: List, store_api: StoreAPI, parent=None):
super(DetailsWidget, self).__init__(parent=parent)
super(StoreDetailsWidget, self).__init__(parent=parent)
self.implements_scrollarea = True
self.ui = Ui_DetailsWidget()
self.ui = Ui_StoreDetailsWidget()
self.ui.setupUi(self)
self.ui.main_layout.setContentsMargins(0, 0, 3, 0)
@ -41,7 +41,7 @@ class DetailsWidget(QWidget, SideTabContents):
self.catalog_offer: CatalogOfferModel = None
self.image = LoadingImageWidget(store_api.cached_manager, self)
self.image.setFixedSize(ImageSize.Display)
self.image.setFixedSize(ImageSize.DisplayTall)
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop)
self.ui.left_layout.setAlignment(Qt.AlignTop)
@ -176,7 +176,9 @@ class DetailsWidget(QWidget, SideTabContents):
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)
# FIXME: check why there was no tall image
if img_url:
self.image.fetchPixmap(img_url.url)
# self.image_stack.setCurrentIndex(0)
about = product_data.about

View file

@ -76,7 +76,7 @@ class StoreItemWidget(ItemWidget):
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.setFixedSize(ImageSize.DisplayTall)
self.ui.setupUi(self)
key_images = catalog_game.keyImages

View file

@ -12,7 +12,7 @@ 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 DetailsWidget
from .widgets.details import StoreDetailsWidget
from .widgets.items import WishlistItemWidget
@ -26,7 +26,7 @@ class WishlistPage(SlidingStackedWidget, SideTabContents):
self.wishlist_widget.set_title.connect(self.set_title)
self.wishlist_widget.show_details.connect(self.show_details)
self.details_widget = DetailsWidget([], api, parent=self)
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)

View file

@ -8,10 +8,11 @@ from threading import Lock
from typing import List, Optional, Dict, Set
from PyQt5.QtCore import QRunnable, pyqtSlot, QProcess, QThreadPool
from PyQt5.QtGui import QPixmap, QPixmapCache
from PyQt5.QtGui import QPixmap
from legendary.lfs import eos
from legendary.models.game import Game, InstalledGame
from rare.models.image import ImageSize
from rare.lgndr.core import LegendaryCore
from rare.models.base_game import RareGameBase, RareGameSlim
from rare.models.install import InstallOptionsModel, UninstallOptionsModel
@ -29,23 +30,31 @@ class RareGame(RareGameSlim):
class Metadata:
queued: bool = False
queue_pos: Optional[int] = None
last_played: datetime = datetime.min
grant_date: datetime = datetime.min
last_played: datetime = datetime.min.replace(tzinfo=UTC)
grant_date: datetime = datetime.min.replace(tzinfo=UTC)
steam_appid: Optional[int] = None
steam_grade: Optional[str] = None
steam_date: datetime = datetime.min
steam_date: datetime = datetime.min.replace(tzinfo=UTC)
steam_shortcut: Optional[int] = None
tags: List[str] = field(default_factory=list)
# For compatibility with previously created game metadata
@staticmethod
def parse_date(strdate: str):
dt = datetime.fromisoformat(strdate) if strdate else datetime.min
return dt.replace(tzinfo=UTC)
@classmethod
def from_dict(cls, data: Dict):
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=RareGame.Metadata.parse_date(data.get("last_played", "")),
grant_date=RareGame.Metadata.parse_date(data.get("grant_date", "")),
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=RareGame.Metadata.parse_date(data.get("steam_date", "")),
steam_shortcut=data.get("steam_shortcut", None),
tags=data.get("tags", []),
)
@ -54,11 +63,12 @@ class RareGame(RareGameSlim):
return dict(
queued=self.queued,
queue_pos=self.queue_pos,
last_played=self.last_played.isoformat() if self.last_played else datetime.min,
grant_date=self.grant_date.isoformat() if self.grant_date else datetime.min,
last_played=self.last_played.isoformat() if self.last_played else datetime.min.replace(tzinfo=UTC),
grant_date=self.grant_date.isoformat() if self.grant_date else datetime.min.replace(tzinfo=UTC),
steam_appid=self.steam_appid,
steam_grade=self.steam_grade,
steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min,
steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min.replace(tzinfo=UTC),
steam_shortcut=self.steam_shortcut,
tags=self.tags,
)
@ -76,7 +86,7 @@ class RareGame(RareGameSlim):
if self.game.app_title == "Unreal Engine":
self.game.app_title += f" {self.game.app_name.split('_')[-1]}"
self.pixmap: QPixmap = QPixmap()
self.has_pixmap: bool = False
self.metadata: RareGame.Metadata = RareGame.Metadata()
self.__load_metadata()
self.grant_date()
@ -118,7 +128,7 @@ class RareGame(RareGameSlim):
@pyqtSlot(int)
def __game_launched(self, code: int):
self.state = RareGame.State.RUNNING
self.metadata.last_played = datetime.now()
self.metadata.last_played = datetime.now(UTC)
if code == GameProcess.Code.ON_STARTUP:
return
self.__save_metadata()
@ -430,13 +440,16 @@ class RareGame(RareGameSlim):
if platform.system() == "Windows" or self.is_unreal:
return "na"
if self.metadata.steam_grade != "pending":
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
elapsed_time = abs(datetime.now(UTC) - self.metadata.steam_date)
if elapsed_time.days > 3 and (self.metadata.steam_grade is None or self.metadata.steam_appid is None):
def _set_steam_grade():
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)
worker = QRunnable.create(set_steam_grade)
QThreadPool.globalInstance().start(worker)
self.metadata.steam_grade = "pending"
return self.metadata.steam_grade
@ -445,20 +458,23 @@ 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.metadata.steam_date = datetime.now(UTC)
self.__save_metadata()
self.signals.widget.update.emit()
def grant_date(self, force=False) -> datetime:
if (entitlements := self.core.lgd.entitlements) is None:
return self.metadata.grant_date.replace(tzinfo=UTC)
return self.metadata.grant_date
if self.metadata.grant_date == datetime.min.replace(tzinfo=UTC) or force:
logger.debug("Grant date for %s not found in metadata, resolving", self.app_name)
matching = filter(lambda ent: ent["namespace"] == self.game.namespace, entitlements)
@ -468,7 +484,7 @@ class RareGame(RareGameSlim):
) if entitlement else datetime.min.replace(tzinfo=UTC)
self.metadata.grant_date = grant_date
self.__save_metadata()
return self.metadata.grant_date.replace(tzinfo=UTC)
return self.metadata.grant_date
def set_origin_attributes(self, path: str, size: int = 0) -> None:
self.__origin_install_path = path
@ -489,19 +505,19 @@ class RareGame(RareGameSlim):
return bool(not self.is_foreign or self.can_run_offline)
return False
def get_pixmap(self, color=True) -> QPixmap:
QPixmapCache.clear()
return self.image_manager.get_pixmap(self.app_name, color)
def get_pixmap(self, preset: ImageSize.Preset, color=True) -> QPixmap:
return self.image_manager.get_pixmap(self.app_name, preset, color)
@pyqtSlot(object)
def set_pixmap(self):
self.pixmap = self.image_manager.get_pixmap(self.app_name, self.is_installed)
QPixmapCache.clear()
if not self.pixmap.isNull():
# self.pixmap = not self.image_manager.get_pixmap(self.app_name, self.is_installed).isNull()
self.has_pixmap = True
if self.has_pixmap:
self.signals.widget.update.emit()
def load_pixmap(self):
""" Do not call this function, call set_pixmap instead. This is only used for startup image loading """
if self.pixmap.isNull():
def load_pixmaps(self):
""" Do not call this function, call set_pixmap instead. This is only used for initial image loading """
if not self.has_pixmap:
self.image_manager.download_image(self.game, self.set_pixmap, 0, False)
def refresh_pixmap(self):
@ -565,8 +581,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

View file

@ -1,25 +1,32 @@
from enum import Enum
from typing import Tuple
from PyQt5.QtCore import QSize
class Orientation(Enum):
class ImageType(Enum):
Tall = 0
Wide = 1
Icon = 3
Logo = 2
class ImageSize:
class Preset:
def __init__(self, divisor: float, pixel_ratio: float, orientation: Orientation = Orientation.Tall,
def __init__(self, divisor: float, pixel_ratio: float, orientation: ImageType = ImageType.Tall,
base: 'ImageSize.Preset' = None):
self.__divisor = divisor
self.__pixel_ratio = pixel_ratio
if orientation == Orientation.Tall:
if orientation == ImageType.Tall:
self.__img_factor = 67
self.__size = QSize(self.__img_factor * 3, self.__img_factor * 4) * pixel_ratio / divisor
else:
self.__img_factor = 17
if orientation == ImageType.Wide:
self.__img_factor = 34
self.__size = QSize(self.__img_factor * 16, self.__img_factor * 9) * pixel_ratio / divisor
if orientation == ImageType.Icon:
self.__img_factor = 128
self.__size = QSize(self.__img_factor * 1, self.__img_factor * 1) * pixel_ratio / divisor
self.__orientation = orientation
# lk: for prettier images set this to true
# self.__smooth_transform: bool = True
self.__smooth_transform = divisor <= 2
@ -49,38 +56,45 @@ class ImageSize:
def pixel_ratio(self) -> float:
return self.__pixel_ratio
@property
def orientation(self) -> ImageType:
return self.__orientation
@property
def aspect_ratio(self) -> Tuple[int, int]:
if self.__orientation == ImageType.Tall:
return 3, 4
elif self.__orientation == ImageType.Wide:
return 16, 9
else:
return 0, 0
@property
def base(self) -> 'ImageSize.Preset':
return self.__base
Image = Preset(1, 1)
Tall = Preset(1, 1)
"""! @brief Size and pixel ratio of the image on disk"""
ImageWide = Preset(1, 1, Orientation.Wide)
"""! @brief Size and pixel ratio for wide 16/9 image on disk"""
Display = Preset(1, 1, base=Image)
DisplayTall = Preset(1, 1, base=Tall)
"""! @brief Size and pixel ratio for displaying"""
DisplayWide = Preset(1, 1, Orientation.Wide, base=ImageWide)
"""! @brief Size and pixel ratio for wide 16/9 image display"""
LibraryWide = Preset(1.21, 1, Orientation.Wide, base=ImageWide)
Library = Preset(1.21, 1, base=Image)
LibraryTall = Preset(1.21, 1, base=Tall)
"""! @brief Same as Display"""
Small = Preset(3, 1, base=Image)
"""! @brief Small image size for displaying"""
Wide = Preset(1, 1, ImageType.Wide)
"""! @brief Size and pixel ratio for wide 16/9 image on disk"""
SmallWide = Preset(3, 1, Orientation.Wide, base=ImageWide)
"""! @brief Small image size for displaying"""
DisplayWide = Preset(2, 1, ImageType.Wide, base=Wide)
"""! @brief Size and pixel ratio for wide 16/9 image display"""
Smaller = Preset(4, 1, base=Image)
"""! @brief Smaller image size for displaying"""
LibraryWide = Preset(2.41, 1, ImageType.Wide, base=Wide)
SmallerWide = Preset(4, 1, Orientation.Wide, base=ImageWide)
"""! @brief Smaller image size for displaying"""
Icon = Preset(1, 1, ImageType.Icon)
"""! @brief Size and pixel ratio of the icon on disk"""
Icon = Preset(5, 1, base=Image)
"""! @brief Smaller image size for UI icons"""
DisplayIcon = Preset(1, 1, ImageType.Icon, base=Icon)
"""! @brief Size and pixel ratio of the icon on disk"""
LibraryIcon = Preset(2.2, 1, ImageType.Icon, base=Icon)
"""! @brief Size and pixel ratio of the icon on disk"""

View file

@ -45,10 +45,10 @@ class Defaults(Namespace):
)
library_order = Value(key="library_order", default=int(LibraryOrder.TITLE), dtype=int)
rpc_enable = Value(key="rpc_enable", default=0, dtype=int)
rpc_name = Value(key="rpc_game", default=True, dtype=bool)
rpc_time = Value(key="rpc_time", default=True, dtype=bool)
rpc_os = Value(key="rpc_os", default=True, dtype=bool)
discord_rpc_mode = Value(key="discord_rpc_mode", default=0, dtype=int)
discord_rpc_game = Value(key="discord_rpc_game", default=True, dtype=bool)
discord_rpc_time = Value(key="discord_rpc_time", default=True, dtype=bool)
discord_rpc_os = Value(key="discord_rpc_os", default=True, dtype=bool)
options = Defaults()

View file

@ -37,10 +37,12 @@ class GlobalSignals:
dequeue = pyqtSignal(str)
class DiscordRPCSignals(QObject):
# str: app_title
set_title = pyqtSignal(str)
# str: app_name
update_presence = pyqtSignal(str)
# str: app_name
remove_presence = pyqtSignal(str)
# none
apply_settings = pyqtSignal()
update_settings = pyqtSignal()
def __init__(self):
self.application = GlobalSignals.ApplicationSignals()

154
rare/models/steam.py Normal file
View file

@ -0,0 +1,154 @@
import binascii
import shlex
from dataclasses import dataclass, field, asdict
from datetime import datetime, UTC
from typing import Dict, List, Type, Any
class SteamUser:
def __init__(self, long_id: str, user: Dict):
super(SteamUser, self).__init__()
self._long_id: str = long_id
self._user = user.copy()
@property
def long_id(self) -> int:
return int(self._long_id)
@property
def short_id(self) -> int:
return self.long_id & 0xFFFFFFFF
@property
def account_name(self) -> str:
return self._user.get("AccountName", "")
@property
def persona_name(self) -> str:
return self._user.get("PersonaName", "")
@property
def most_recent(self) -> bool:
return bool(int(self._user.get("MostRecent", "0")))
@property
def last_login(self) -> datetime:
return datetime.fromtimestamp(float(self._user.get("Timestamp", "0")), UTC)
@property
def __dict__(self):
return dict(
long_id=self.long_id,
short_id=self.short_id,
account_name=self.account_name,
persona_name=self.persona_name,
most_recent=self.most_recent,
last_login=self.last_login,
)
def __repr__(self):
return repr(vars(self))
@dataclass
class SteamShortcut:
appid: int
AppName: str
Exe: str
StartDir: str
icon: str
ShortcutPath: str
LaunchOptions: str
IsHidden: bool
AllowDesktopConfig: bool
AllowOverlay: bool
OpenVR: bool
Devkit: bool
DevkitGameID: str
DevkitOverrideAppID: int
LastPlayTime: int
FlatpakAppID: str
tags: Dict = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["SteamShortcut"], src: Dict[str, Any]) -> "SteamShortcut":
d = src.copy()
tmp = cls(
appid=d.pop("appid", 0),
AppName=d.pop("AppName", ""),
Exe=d.pop("Exe", ""),
StartDir=d.pop("StartDir", ""),
icon=d.pop("icon", ""),
ShortcutPath=d.pop("ShortcutPath", ""),
LaunchOptions=d.pop("LaunchOptions", ""),
IsHidden=bool(d.pop("IsHidden", 0)),
AllowDesktopConfig=bool(d.pop("AllowDesktopConfig", 1)),
AllowOverlay=bool(d.pop("AllowOverlay", 1)),
OpenVR=bool(d.pop("OpenVR", 0)),
Devkit=bool(d.pop("Devkit", 0)),
DevkitGameID=d.pop("DevkitGameID", ""),
DevkitOverrideAppID=d.pop("DevkitOverrideAppID", 0),
LastPlayTime=d.pop("LastPlayTime", 0),
FlatpakAppID=d.pop("FlatpakAppID", ""),
tags=d.pop("tags", {}),
)
return tmp
@classmethod
def create(
cls: Type["SteamShortcut"],
app_name: str,
app_title: str,
executable: str,
start_dir: str,
icon: str,
launch_options: List[str],
) -> "SteamShortcut":
shortcut = cls.from_dict({})
shortcut.appid = cls.calculate_appid(app_name)
shortcut.AppName = app_title
shortcut.Exe = shlex.quote(executable)
shortcut.StartDir = shlex.quote(start_dir)
shortcut.icon = shlex.quote(icon)
shortcut.LaunchOptions = shlex.join(launch_options)
return shortcut
@staticmethod
def calculate_appid(app_name) -> int:
key = "rare_steam_shortcut_" + app_name
top = binascii.crc32(str.encode(key, "utf-8")) | 0x80000000
return (((top << 32) | 0x02000000) >> 32) - 0x100000000
def shortcut_appid(self) -> int:
return self.appid
def coverart_appid(self) -> int:
return self.shortcut_appid() + 0x100000000
@property
def grid_wide(self) -> str:
return f"{self.coverart_appid()}.png"
@property
def grid_tall(self) -> str:
return f"{self.coverart_appid()}p.png"
@property
def game_hero(self) -> str:
return f"{self.coverart_appid()}_hero.png"
@property
def game_logo(self) -> str:
return f"{self.coverart_appid()}_logo.png"
@property
def last_played(self):
return datetime.fromtimestamp(float(self.LastPlayTime), UTC)
@property
def __dict__(self):
ret = dict(
shortcut_appid=self.shortcut_appid(), grid_appid=self.coverart_appid(), last_played=self.last_played
)
return ret

View file

@ -196,16 +196,8 @@ css.QPushButton[css_name(ListWidget, "Button")].textAlign.setValue("left")
css.QLabel[css_name(ListWidget, "InfoLabel")].color.setValue("#999")
# WaitingSpinner
from rare.utils.extra_widgets import WaitingSpinner
css.QLabel[css_name(WaitingSpinner)].setValues(
marginLeft="auto",
marginRight="auto",
)
# SelectViewWidget
from rare.utils.extra_widgets import SelectViewWidget
from rare.components.tabs.games.head_bar import SelectViewWidget
css.QPushButton[css_name(SelectViewWidget, "Button")].setValues(
border="none",
backgroundColor="transparent",
@ -213,7 +205,7 @@ css.QPushButton[css_name(SelectViewWidget, "Button")].setValues(
# ButtonLineEdit
from rare.utils.extra_widgets import ButtonLineEdit
from rare.widgets.button_edit import ButtonLineEdit
css.QPushButton[css_name(ButtonLineEdit, "Button")].setValues(
backgroundColor="transparent",
border="0px",

View file

@ -108,10 +108,6 @@ QPushButton#ListWidgetButton {
QLabel#ListWidgetInfoLabel {
color: #999;
}
QLabel#WaitingSpinner {
margin-left: auto;
margin-right: auto;
}
QPushButton#SelectViewWidgetButton {
border: none;
background-color: transparent;

View file

@ -2,6 +2,7 @@ import hashlib
import json
import pickle
import zlib
# from concurrent import futures
from logging import getLogger
from pathlib import Path
@ -9,32 +10,24 @@ from typing import TYPE_CHECKING, Optional, Set
from typing import Tuple, Dict, Union, Type, List, Callable
import requests
from PyQt5.QtCore import (
Qt,
pyqtSignal,
QObject,
QSize,
QThreadPool,
QRunnable,
QRect,
QRectF,
)
from PyQt5.QtGui import (
QPixmap,
QImage,
QPainter,
QPainterPath,
QBrush,
QTransform,
QPen,
)
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QSize, QThreadPool, QRunnable, QRect, QRectF, pyqtSlot
from PyQt5.QtGui import QPixmap, QImage, QPainter, QPainterPath, QBrush, QTransform, QPen
from PyQt5.QtWidgets import QApplication
from legendary.models.game import Game
from rare.lgndr.core import LegendaryCore
from rare.models.image import ImageSize
from rare.models.image import ImageSize, ImageType
from rare.models.signals import GlobalSignals
from rare.utils.paths import image_dir, resources_path, desktop_icon_suffix
from rare.utils.paths import (
image_dir,
image_dir_game,
image_tall_path,
image_wide_path,
image_icon_path,
resources_path,
desktop_icon_suffix,
desktop_icon_path,
)
# from requests_futures.sessions import FuturesSession
@ -66,8 +59,12 @@ class ImageManager(QObject):
def __init__(self, signals: GlobalSignals, core: LegendaryCore):
# lk: the ordering in __img_types matters for the order of fallbacks
# self.__img_types: Tuple = ("DieselGameBoxTall", "Thumbnail", "DieselGameBoxLogo", "DieselGameBox", "OfferImageTall")
self.__img_types: Tuple = ("DieselGameBoxTall", "Thumbnail", "DieselGameBoxLogo", "OfferImageTall")
# {'AndroidIcon', 'DieselGameBox', 'DieselGameBoxLogo', 'DieselGameBoxTall', 'DieselGameBoxWide',
# 'ESRB', 'Featured', 'OfferImageTall', 'OfferImageWide', 'Screenshot', 'Thumbnail'}
self.__img_tall_types: Tuple = ("DieselGameBoxTall", "OfferImageTall", "Thumbnail")
self.__img_wide_types: Tuple = ("DieselGameBoxWide", "DieselGameBox", "OfferImageWide", "Screenshot")
self.__img_logo_types: Tuple = ("DieselGameBoxLogo",)
self.__img_types: Tuple = self.__img_tall_types + self.__img_wide_types + self.__img_logo_types
self.__dl_retries = 1
self.__worker_app_names: Set[str] = set()
super(QObject, self).__init__()
@ -79,36 +76,35 @@ class ImageManager(QObject):
self.image_dir.mkdir()
logger.info(f"Created image directory at {self.image_dir}")
self.device = ImageSize.Preset(1, QApplication.instance().devicePixelRatio())
self.threadpool = QThreadPool()
self.threadpool.setMaxThreadCount(4)
self.threadpool.setMaxThreadCount(6)
def __img_dir(self, app_name: str) -> Path:
return self.image_dir.joinpath(app_name)
@staticmethod
def __img_json(app_name: str) -> Path:
return image_dir_game(app_name).joinpath("image.json")
def __img_json(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("image.json")
@staticmethod
def __img_cache(app_name: str) -> Path:
return image_dir_game(app_name).joinpath("image.cache")
def __img_cache(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("image.cache")
def __img_color(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("installed.png")
def __img_gray(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("uninstalled.png")
def __img_desktop_icon(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath(f"icon.{desktop_icon_suffix()}")
@staticmethod
def __img_all(app_name: str) -> Tuple:
return (
image_tall_path(app_name),
image_tall_path(app_name, color=False),
image_wide_path(app_name),
image_wide_path(app_name, color=False),
image_icon_path(app_name),
image_icon_path(app_name, color=False),
desktop_icon_path(app_name),
)
def __prepare_download(self, game: Game, force: bool = False) -> Tuple[List, Dict]:
if force and self.__img_dir(game.app_name).exists():
self.__img_color(game.app_name).unlink(missing_ok=True)
self.__img_gray(game.app_name).unlink(missing_ok=True)
self.__img_desktop_icon(game.app_name).unlink(missing_ok=True)
if not self.__img_dir(game.app_name).is_dir():
self.__img_dir(game.app_name).mkdir()
if force and image_dir_game(game.app_name).exists():
for file in self.__img_all(game.app_name):
file.unlink(missing_ok=True)
if not image_dir_game(game.app_name).is_dir():
image_dir_game(game.app_name).mkdir()
# Load image checksums
if not self.__img_json(game.app_name).is_file():
@ -116,39 +112,59 @@ class ImageManager(QObject):
else:
json_data = json.load(open(self.__img_json(game.app_name), "r"))
# Only download the best matching candidate for each image category
def best_match(key_images: List, image_types: Tuple) -> Dict:
matches = sorted(
filter(lambda image: image["type"] in image_types, key_images),
key=lambda x: image_types.index(x["type"]) if x["type"] in image_types else len(image_types),
reverse=False,
)
try:
best = matches[0]
except IndexError as e:
best = {}
return best
candidates = tuple(
image
for image in [
best_match(game.metadata.get("keyImages", []), self.__img_tall_types),
best_match(game.metadata.get("keyImages", []), self.__img_wide_types),
best_match(game.metadata.get("keyImages", []), self.__img_logo_types),
]
if bool(image)
)
# lk: Find updates or initialize if images are missing.
# lk: `updates` will be empty for games without images
# lk: so everything below it is skipped
updates = []
if not (
self.__img_color(game.app_name).is_file()
and self.__img_gray(game.app_name).is_file()
and self.__img_desktop_icon(game.app_name).is_file()
):
if not all(file.is_file() for file in self.__img_all(game.app_name)):
# lk: fast path for games without images, convert Rare's logo
if not game.metadata.get("keyImages", []):
if not candidates:
cache_data: Dict = dict(zip(self.__img_types, [None] * len(self.__img_types)))
cache_data["DieselGameBoxTall"] = open(
resources_path.joinpath("images", "cover.png"), "rb"
).read()
with open(resources_path.joinpath("images", "cover.png"), "rb") as fd:
cache_data["DieselGameBoxTall"] = fd.read()
fd.seek(0)
cache_data["DieselGameBoxWide"] = fd.read()
# cache_data["DieselGameBoxLogo"] = open(
# resources_path.joinpath("images", "Rare_nonsquared.png"), "rb").read()
self.__convert(game, cache_data)
json_data["cache"] = None
json_data["scale"] = ImageSize.Image.pixel_ratio
json_data["size"] = ImageSize.Image.size.__str__()
json_data["scale"] = ImageSize.Tall.pixel_ratio
json_data["size"] = {"w": ImageSize.Tall.size.width(), "h": ImageSize.Tall.size.height()}
json.dump(json_data, open(self.__img_json(game.app_name), "w"))
else:
updates = [image for image in game.metadata["keyImages"] if image["type"] in self.__img_types]
updates = [image for image in candidates if image["type"] in self.__img_types]
else:
for image in game.metadata.get("keyImages", []):
for image in candidates:
if image["type"] in self.__img_types:
if image["type"] not in json_data.keys() or json_data[image["type"]] != image["md5"]:
updates.append(image)
return updates, json_data
def __download(self, updates, json_data, game, use_async: bool = False) -> bool:
def __download(self, updates: List, json_data: Dict, game: Game, use_async: bool = False) -> bool:
# Decompress existing image.cache
if not self.__img_cache(game.app_name).is_file():
cache_data = dict(zip(self.__img_types, [None] * len(self.__img_types)))
@ -156,32 +172,45 @@ class ImageManager(QObject):
cache_data = self.__decompress(game)
# lk: filter updates again against the cache now that it is available
updates = [
# images in cache don't need to be downloaded again.
downloads = [
image
for image in updates
if cache_data.get(image["type"], None) is None or json_data[image["type"]] != image["md5"]
if (cache_data.get(image["type"], None) is None or json_data[image["type"]] != image["md5"])
]
# Download
# # lk: Keep this here, so I don't have to go looking for it again,
# # lk: it might be useful in the future.
# if use_async and len(updates) > 1:
# if use_async:
# session = FuturesSession(max_workers=len(self.__img_types))
# image_requests = []
# for image in updates:
# for image in downloads:
# logger.info(f"Downloading {image['type']} for {game.app_title}")
# json_data[image["type"]] = image["md5"]
# payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
# if image["type"] in self.__img_tall_types:
# payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
# elif image["type"] in self.__img_wide_types:
# payload = {"resize": 1, "w": ImageSize.ImageWide.size.width(), "h": ImageSize.ImageWide.size.height()}
# else:
# # Set the larger of the sizes for everything else
# payload = {"resize": 1, "w": ImageSize.ImageWide.size.width(), "h": ImageSize.ImageWide.size.height()}
# req = session.get(image["url"], params=payload)
# req.image_type = image["type"]
# image_requests.append(req)
# for req in futures.as_completed(image_requests):
# cache_data[req.image_type] = req.result().content
# else:
for image in updates:
for image in downloads:
logger.info(f"Downloading {image['type']} for {game.app_name} ({game.app_title})")
json_data[image["type"]] = image["md5"]
payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
if image["type"] in self.__img_tall_types:
payload = {"resize": 1, "w": ImageSize.Tall.size.width(), "h": ImageSize.Tall.size.height()}
elif image["type"] in self.__img_wide_types:
payload = {"resize": 1, "w": ImageSize.Wide.size.width(), "h": ImageSize.Wide.size.height()}
else:
# Set the larger of the sizes for everything else
payload = {"resize": 1, "w": ImageSize.Wide.size.width(), "h": ImageSize.Wide.size.height()}
try:
# cache_data[image["type"]] = requests.get(image["url"], params=payload).content
cache_data[image["type"]] = requests.get(image["url"], params=payload, timeout=10).content
@ -203,8 +232,8 @@ class ImageManager(QObject):
archive_hash = None
json_data["cache"] = archive_hash
json_data["scale"] = ImageSize.Image.pixel_ratio
json_data["size"] = {"w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
json_data["scale"] = ImageSize.Tall.pixel_ratio
json_data["size"] = {"w": ImageSize.Tall.size.width(), "h": ImageSize.Tall.size.height()}
# write image.json
with open(self.__img_json(game.app_name), "w") as file:
@ -219,13 +248,13 @@ class ImageManager(QObject):
if ImageManager.__icon_overlay is not None:
return ImageManager.__icon_overlay
rounded_path = QPainterPath()
margin = 0.1
margin = 0.05
rounded_path.addRoundedRect(
QRectF(
rect.width() * margin,
rect.height() * margin,
rect.width() - (rect.width() * margin * 2),
rect.height() - (rect.width() * margin * 2)
rect.height() - (rect.width() * margin * 2),
),
rect.height() * 0.2,
rect.height() * 0.2,
@ -244,7 +273,7 @@ class ImageManager(QObject):
painter.fillRect(icon.rect(), Qt.transparent)
overlay = ImageManager.__generate_icon_overlay(icon.rect())
brush = QBrush(cover)
scale = max(icon.width()/cover.width(), icon.height()/cover.height())
scale = max(icon.width() / cover.width(), icon.height() / cover.height())
transform = QTransform().scale(scale, scale)
brush.setTransform(transform)
painter.fillPath(overlay, brush)
@ -255,52 +284,68 @@ class ImageManager(QObject):
return icon
def __convert(self, game, images, force=False) -> None:
for image in [self.__img_color(game.app_name), self.__img_gray(game.app_name)]:
if force and image.exists():
image.unlink(missing_ok=True)
for file in self.__img_all(game.app_name):
if force and file.exists():
file.unlink(missing_ok=True)
cover_data = None
for image_type in self.__img_types:
if images[image_type] is not None:
cover_data = images[image_type]
break
def find_image_data(image_types: Tuple):
data = None
for image_type in image_types:
if images.get(image_type, None) is not None:
data = images[image_type]
break
return data
cover = QImage()
cover.loadFromData(cover_data)
cover.convertToFormat(QImage.Format_ARGB32_Premultiplied)
# lk: Images are not always 4/3, crop them to size
factor = min(cover.width() // 3, cover.height() // 4)
rem_w = (cover.width() - factor * 3) // 2
rem_h = (cover.height() - factor * 4) // 2
cover = cover.copy(rem_w, rem_h, factor * 3, factor * 4)
tall_data = find_image_data(self.__img_tall_types)
wide_data = find_image_data(self.__img_wide_types)
logo_data = find_image_data(self.__img_logo_types)
if images["DieselGameBoxLogo"] is not None:
logo = QImage()
logo.loadFromData(images["DieselGameBoxLogo"])
logo.convertToFormat(QImage.Format_ARGB32_Premultiplied)
if logo.width() > cover.width():
logo = logo.scaled(cover.width(), cover.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter = QPainter(cover)
painter.drawImage((cover.width() - logo.width()) // 2, cover.height() - logo.height(), logo)
painter.end()
def convert_image(image_data, logo_data, preset: ImageSize.Preset) -> QImage:
image = QImage()
image.loadFromData(image_data)
image.convertToFormat(QImage.Format_ARGB32_Premultiplied)
# lk: Images are not always at the correct aspect ratio, so crop them to size
wr, hr = preset.aspect_ratio
factor = min(image.width() // wr, image.height() // hr)
rem_w = (image.width() - factor * wr) // 2
rem_h = (image.height() - factor * hr) // 2
image = image.copy(rem_w, rem_h, factor * wr, factor * hr)
cover = cover.scaled(ImageSize.Image.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
if logo_data is not None:
logo = QImage()
logo.loadFromData(logo_data)
logo.convertToFormat(QImage.Format_ARGB32_Premultiplied)
if logo.width() > image.width():
logo = logo.scaled(image.width(), image.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter = QPainter(image)
painter.drawImage((image.width() - logo.width()) // 2, image.height() - logo.height(), logo)
painter.end()
icon = self.__convert_icon(cover)
icon.save(str(self.__img_desktop_icon(game.app_name)), format=desktop_icon_suffix().upper())
return image.scaled(preset.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# this is not required if we ever want to re-apply the alpha channel
# cover = cover.convertToFormat(QImage.Format_Indexed8)
tall = convert_image(tall_data, logo_data, ImageSize.Tall)
wide = convert_image(wide_data, logo_data, ImageSize.Wide)
# add the alpha channel back to the cover
cover = cover.convertToFormat(QImage.Format_ARGB32_Premultiplied)
icon = self.__convert_icon(tall)
icon.save(desktop_icon_path(game.app_name).as_posix(), format=desktop_icon_suffix().upper())
cover.save(str(self.__img_color(game.app_name)), format="PNG")
# quick way to convert to grayscale
cover = cover.convertToFormat(QImage.Format_Grayscale8)
# add the alpha channel back to the grayscale cover
cover = cover.convertToFormat(QImage.Format_ARGB32_Premultiplied)
cover.save(str(self.__img_gray(game.app_name)), format="PNG")
def save_image(image: QImage, color_path: Path, gray_path: Path):
# this is not required if we ever want to re-apply the alpha channel
# image = image.convertToFormat(QImage.Format_Indexed8)
# add the alpha channel back to the cover
image = image.convertToFormat(QImage.Format_ARGB32_Premultiplied)
image.save(color_path.as_posix(), format="PNG")
# quick way to convert to grayscale, but keep the alpha channel
alpha = image.convertToFormat(QImage.Format_Alpha8)
image = image.convertToFormat(QImage.Format_Grayscale8)
# add the alpha channel back to the grayscale cover
image = image.convertToFormat(QImage.Format_ARGB32_Premultiplied)
image.setAlphaChannel(alpha)
image.save(gray_path.as_posix(), format="PNG")
save_image(icon, image_icon_path(game.app_name), image_icon_path(game.app_name, color=False))
save_image(tall, image_tall_path(game.app_name), image_tall_path(game.app_name, color=False))
save_image(wide, image_wide_path(game.app_name), image_wide_path(game.app_name, color=False))
def __compress(self, game: Game, data: Dict) -> None:
archive = open(self.__img_cache(game.app_name), "wb")
@ -319,22 +364,41 @@ class ImageManager(QObject):
archive.close()
return data
def __append_to_queue(self, game: Game):
self.__worker_app_names.add(game.app_name)
@pyqtSlot(object)
def __remove_from_queue(self, game: Game):
self.__worker_app_names.remove(game.app_name)
def download_image(
self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False
self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False
) -> None:
if game.app_name in self.__worker_app_names:
return
self.__worker_app_names.add(game.app_name)
self.__append_to_queue(game)
updates, json_data = self.__prepare_download(game, force)
if not updates:
self.__worker_app_names.remove(game.app_name)
self.__remove_from_queue(game)
load_callback()
else:
image_worker = ImageManager.Worker(self.__download, updates, json_data, game)
image_worker.signals.completed.connect(lambda g: self.__worker_app_names.remove(g.app_name))
# Copy the data because we are going to be a thread and we modify them later on
image_worker = ImageManager.Worker(self.__download, updates.copy(), json_data.copy(), game)
image_worker.signals.completed.connect(self.__remove_from_queue)
image_worker.signals.completed.connect(load_callback)
self.threadpool.start(image_worker, priority)
def download_image_launch(
self, game: Game, callback: Callable[[Game], None], priority: int, force: bool = False
) -> None:
if self.__img_cache(game.app_name).is_file() and not force:
return
def _callback():
callback(game)
self.download_image(game, _callback, priority, force)
def download_image_blocking(self, game: Game, force: bool = False) -> None:
updates, json_data = self.__prepare_download(game, force)
if not updates:
@ -342,44 +406,56 @@ class ImageManager(QObject):
if updates:
self.__download(updates, json_data, game, use_async=True)
@staticmethod
def __get_cover(
self, container: Union[Type[QPixmap], Type[QImage]], app_name: str, color: bool = True
container: Union[Type[QPixmap], Type[QImage]], app_name: str, preset: ImageSize.Preset, color: bool,
) -> Union[QPixmap, QImage]:
ret = container()
if not app_name:
raise RuntimeError("app_name is an empty string")
if color:
if self.__img_color(app_name).is_file():
ret.load(str(self.__img_color(app_name)))
if preset.orientation == ImageType.Icon:
if image_icon_path(app_name, color).is_file():
ret.load(image_icon_path(app_name, color).as_posix())
elif preset.orientation == ImageType.Tall:
if image_tall_path(app_name, color).is_file():
ret.load(image_tall_path(app_name, color).as_posix())
elif preset.orientation == ImageType.Wide:
if image_wide_path(app_name, color).is_file():
ret.load(image_wide_path(app_name, color).as_posix())
else:
if self.__img_gray(app_name).is_file():
ret.load(str(self.__img_gray(app_name)))
raise RuntimeError("Unknown image preset")
if not ret.isNull():
ret.setDevicePixelRatio(ImageSize.Image.pixel_ratio)
device = ImageSize.Preset(
divisor=preset.base.divisor,
pixel_ratio=QApplication.instance().devicePixelRatio(),
orientation=preset.base.orientation,
base=preset
)
ret.setDevicePixelRatio(preset.pixel_ratio)
# lk: Scaling happens at painting. It might be inefficient so leave this here as an alternative
# lk: If this is uncommented, the transformation in ImageWidget should be adjusted also
ret = ret.scaled(self.device.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
ret.setDevicePixelRatio(self.device.pixel_ratio)
ret = ret.scaled(device.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
ret.setDevicePixelRatio(device.pixel_ratio)
return ret
def get_pixmap(self, app_name: str, color: bool = True) -> QPixmap:
def get_pixmap(self, app_name: str, preset: ImageSize.Preset, color: bool = True) -> QPixmap:
"""
Use when the image is to be presented directly on the screen.
@param app_name: The RareGame object for this game
@param preset:
@param color: True to load the colored pixmap, False to load the grayscale
@return: QPixmap
"""
pixmap: QPixmap = self.__get_cover(QPixmap, app_name, color)
pixmap: QPixmap = self.__get_cover(QPixmap, app_name, preset, color)
return pixmap
def get_image(self, app_name: str, color: bool = True) -> QImage:
def get_image(self, app_name: str, preset: ImageSize.Preset, color: bool = True) -> QImage:
"""
Use when the image has to be manipulated before being rendered.
@param app_name: The RareGame object for this game
@param preset:
@param color: True to load the colored image, False to load the grayscale
@return: QImage
"""
image: QImage = self.__get_cover(QImage, app_name, color)
image: QImage = self.__get_cover(QImage, app_name, preset, color)
return image

View file

@ -17,6 +17,7 @@ from rare.models.game import RareGame, RareEosOverlay
from rare.models.signals import GlobalSignals
from rare.utils.metrics import timelogger
from rare.utils import config_helper
from rare.utils.steam_shortcuts import load_steam_shortcuts
from .image_manager import ImageManager
from .workers import (
QueueWorker,
@ -282,12 +283,19 @@ class RareCore(QObject):
def __add_game(self, rgame: RareGame) -> None:
rgame.signals.download.enqueue.connect(self.__signals.download.enqueue)
rgame.signals.download.dequeue.connect(self.__signals.download.dequeue)
rgame.signals.game.install.connect(self.__signals.game.install)
rgame.signals.game.installed.connect(self.__signals.game.installed)
rgame.signals.game.uninstall.connect(self.__signals.game.uninstall)
rgame.signals.game.uninstalled.connect(self.__signals.game.uninstalled)
rgame.signals.game.launched.connect(self.__signals.application.update_tray)
rgame.signals.game.launched.connect(self.__signals.discord_rpc.update_presence)
rgame.signals.game.finished.connect(self.__signals.application.update_tray)
rgame.signals.game.finished.connect(lambda: self.__signals.discord_rpc.set_title.emit(""))
rgame.signals.game.finished.connect(self.__signals.discord_rpc.remove_presence)
self.__library[rgame.app_name] = rgame
def __filter_games(self, condition: Callable[[RareGame], bool]) -> Iterator[RareGame]:
@ -347,6 +355,7 @@ class RareCore(QObject):
self.__wrappers.import_wrappers(
self.__core, self.__settings, [rgame.app_name for rgame in self.games]
)
load_steam_shortcuts()
self.progress.emit(100, self.tr("Launching Rare"))
self.completed.emit()
QTimer.singleShot(100, self.__post_init)

View file

@ -55,6 +55,7 @@ class WinePathResolver(Worker):
disable_wine=config.get_boolean(app_name, "no_wine")
)
env = core.get_app_environment(app_name, disable_wine=config.get_boolean(app_name, "no_wine"))
# pylint: disable=E0606
env = compat_utils.get_host_environment(env, silent=True)
return cmd, env
@ -156,17 +157,17 @@ class OriginWineWorker(WinePathResolver):
if install_dir:
logger.debug("Found Unix install directory %s", install_dir)
else:
logger.info("Could not find Unix install directory for %s", rgame.app_title)
logger.info("Could not find Unix install directory for '%s'", rgame.app_title)
else:
logger.info("Could not find Wine install directory for %s", rgame.app_title)
logger.info("Could not find Wine install directory for '%s'", rgame.app_title)
if install_dir:
if os.path.isdir(install_dir):
install_size = path_size(install_dir)
rgame.set_origin_attributes(install_dir, install_size)
logger.info("Origin game %s (%s, %s)", rgame.app_title, install_dir, format_size(install_size))
logger.info("Origin game '%s' (%s, %s)", rgame.app_title, install_dir, format_size(install_size))
else:
logger.warning("Origin game %s (%s does not exist)", rgame.app_title, install_dir)
logger.warning("Origin game '%s' (%s does not exist)", rgame.app_title, install_dir)
else:
logger.info("Origin game %s is not installed", rgame.app_title)
logger.info("Origin game '%s' is not installed", rgame.app_title)
logger.info("Origin worker finished in %ss", time.time() - t)

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/game_info/game_info.ui'
# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/game_info/details.ui'
#
# Created by: PyQt5 UI code generator 5.15.10
#
@ -11,15 +11,16 @@
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GameInfo(object):
def setupUi(self, GameInfo):
GameInfo.setObjectName("GameInfo")
GameInfo.resize(600, 404)
self.main_layout = QtWidgets.QHBoxLayout(GameInfo)
class Ui_GameDetails(object):
def setupUi(self, GameDetails):
GameDetails.setObjectName("GameDetails")
GameDetails.resize(814, 470)
GameDetails.setWindowTitle("GameDetails")
self.main_layout = QtWidgets.QHBoxLayout(GameDetails)
self.main_layout.setObjectName("main_layout")
self.left_layout = QtWidgets.QVBoxLayout()
self.left_layout.setObjectName("left_layout")
self.tags_group = QtWidgets.QGroupBox(GameInfo)
self.tags_group = QtWidgets.QGroupBox(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -59,12 +60,14 @@ class Ui_GameInfo(object):
self.main_layout.addLayout(self.left_layout)
self.right_layout = QtWidgets.QVBoxLayout()
self.right_layout.setObjectName("right_layout")
self.info_layout = QtWidgets.QFormLayout()
self.info_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.info_layout.setContentsMargins(6, 6, 6, 6)
self.info_layout.setSpacing(12)
self.info_layout.setObjectName("info_layout")
self.lbl_dev = QtWidgets.QLabel(GameInfo)
self.details_layout = QtWidgets.QFormLayout()
self.details_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.details_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
self.details_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.details_layout.setContentsMargins(6, 6, 6, 6)
self.details_layout.setSpacing(12)
self.details_layout.setObjectName("details_layout")
self.lbl_dev = QtWidgets.QLabel(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -76,13 +79,13 @@ class Ui_GameInfo(object):
self.lbl_dev.setFont(font)
self.lbl_dev.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.lbl_dev.setObjectName("lbl_dev")
self.info_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lbl_dev)
self.dev = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lbl_dev)
self.dev = QtWidgets.QLabel(GameDetails)
self.dev.setText("error")
self.dev.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.dev.setObjectName("dev")
self.info_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.dev)
self.lbl_app_name = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.dev)
self.lbl_app_name = QtWidgets.QLabel(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -94,13 +97,13 @@ class Ui_GameInfo(object):
self.lbl_app_name.setFont(font)
self.lbl_app_name.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.lbl_app_name.setObjectName("lbl_app_name")
self.info_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lbl_app_name)
self.app_name = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lbl_app_name)
self.app_name = QtWidgets.QLabel(GameDetails)
self.app_name.setText("error")
self.app_name.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.app_name.setObjectName("app_name")
self.info_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.app_name)
self.lbl_version = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.app_name)
self.lbl_version = QtWidgets.QLabel(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -112,13 +115,13 @@ class Ui_GameInfo(object):
self.lbl_version.setFont(font)
self.lbl_version.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.lbl_version.setObjectName("lbl_version")
self.info_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.lbl_version)
self.version = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.lbl_version)
self.version = QtWidgets.QLabel(GameDetails)
self.version.setText("error")
self.version.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.version.setObjectName("version")
self.info_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.version)
self.lbl_grade = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.version)
self.lbl_grade = QtWidgets.QLabel(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -130,14 +133,14 @@ class Ui_GameInfo(object):
self.lbl_grade.setFont(font)
self.lbl_grade.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.lbl_grade.setObjectName("lbl_grade")
self.info_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.lbl_grade)
self.grade = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.lbl_grade)
self.grade = QtWidgets.QLabel(GameDetails)
self.grade.setText("error")
self.grade.setOpenExternalLinks(True)
self.grade.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.grade.setObjectName("grade")
self.info_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.grade)
self.lbl_install_size = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.grade)
self.lbl_install_size = QtWidgets.QLabel(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -149,13 +152,13 @@ class Ui_GameInfo(object):
self.lbl_install_size.setFont(font)
self.lbl_install_size.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.lbl_install_size.setObjectName("lbl_install_size")
self.info_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.lbl_install_size)
self.install_size = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.lbl_install_size)
self.install_size = QtWidgets.QLabel(GameDetails)
self.install_size.setText("error")
self.install_size.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.install_size.setObjectName("install_size")
self.info_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.install_size)
self.lbl_install_path = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.install_size)
self.lbl_install_path = QtWidgets.QLabel(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -167,14 +170,14 @@ class Ui_GameInfo(object):
self.lbl_install_path.setFont(font)
self.lbl_install_path.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.lbl_install_path.setObjectName("lbl_install_path")
self.info_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.lbl_install_path)
self.install_path = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.lbl_install_path)
self.install_path = QtWidgets.QLabel(GameDetails)
self.install_path.setText("error")
self.install_path.setWordWrap(True)
self.install_path.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.install_path.setObjectName("install_path")
self.info_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.install_path)
self.lbl_platform = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.install_path)
self.lbl_platform = QtWidgets.QLabel(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -186,12 +189,12 @@ class Ui_GameInfo(object):
self.lbl_platform.setFont(font)
self.lbl_platform.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.lbl_platform.setObjectName("lbl_platform")
self.info_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.lbl_platform)
self.platform = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.lbl_platform)
self.platform = QtWidgets.QLabel(GameDetails)
self.platform.setText("error")
self.platform.setObjectName("platform")
self.info_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.platform)
self.lbl_game_actions = QtWidgets.QLabel(GameInfo)
self.details_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.platform)
self.lbl_game_actions = QtWidgets.QLabel(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -203,8 +206,8 @@ class Ui_GameInfo(object):
self.lbl_game_actions.setFont(font)
self.lbl_game_actions.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.lbl_game_actions.setObjectName("lbl_game_actions")
self.info_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.lbl_game_actions)
self.game_actions_stack = QtWidgets.QStackedWidget(GameInfo)
self.details_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.lbl_game_actions)
self.game_actions_stack = QtWidgets.QStackedWidget(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -295,9 +298,9 @@ class Ui_GameInfo(object):
spacerItem = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.uninstalled_layout.addItem(spacerItem)
self.game_actions_stack.addWidget(self.uninstalled_page)
self.info_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.game_actions_stack)
self.right_layout.addLayout(self.info_layout)
self.requirements_group = QtWidgets.QFrame(GameInfo)
self.details_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.game_actions_stack)
self.right_layout.addLayout(self.details_layout)
self.requirements_group = QtWidgets.QFrame(GameDetails)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -313,41 +316,40 @@ class Ui_GameInfo(object):
self.main_layout.addLayout(self.right_layout)
self.main_layout.setStretch(1, 1)
self.retranslateUi(GameInfo)
self.retranslateUi(GameDetails)
self.game_actions_stack.setCurrentIndex(0)
self.verify_stack.setCurrentIndex(0)
self.move_stack.setCurrentIndex(0)
def retranslateUi(self, GameInfo):
def retranslateUi(self, GameDetails):
_translate = QtCore.QCoreApplication.translate
GameInfo.setWindowTitle(_translate("GameInfo", "Game Info"))
self.tags_group.setTitle(_translate("GameInfo", "Tags"))
self.completed_check.setText(_translate("GameInfo", "Completed"))
self.hidden_check.setText(_translate("GameInfo", "Hidden"))
self.favorites_check.setText(_translate("GameInfo", "Favorites"))
self.backlog_check.setText(_translate("GameInfo", "Backlog"))
self.lbl_dev.setText(_translate("GameInfo", "Developer"))
self.lbl_app_name.setText(_translate("GameInfo", "Application Name"))
self.lbl_version.setText(_translate("GameInfo", "Version"))
self.lbl_grade.setText(_translate("GameInfo", "ProtonDB Grade"))
self.lbl_install_size.setText(_translate("GameInfo", "Installation Size"))
self.lbl_install_path.setText(_translate("GameInfo", "Installation Path"))
self.lbl_platform.setText(_translate("GameInfo", "Platform"))
self.lbl_game_actions.setText(_translate("GameInfo", "Actions"))
self.modify_button.setText(_translate("GameInfo", "Modify"))
self.verify_button.setText(_translate("GameInfo", "Verify"))
self.repair_button.setText(_translate("GameInfo", "Repair"))
self.move_button.setText(_translate("GameInfo", "Move"))
self.uninstall_button.setText(_translate("GameInfo", "Uninstall"))
self.install_button.setText(_translate("GameInfo", "Install"))
self.import_button.setText(_translate("GameInfo", "Import"))
self.tags_group.setTitle(_translate("GameDetails", "Tags"))
self.completed_check.setText(_translate("GameDetails", "Completed"))
self.hidden_check.setText(_translate("GameDetails", "Hidden"))
self.favorites_check.setText(_translate("GameDetails", "Favorites"))
self.backlog_check.setText(_translate("GameDetails", "Backlog"))
self.lbl_dev.setText(_translate("GameDetails", "Developer"))
self.lbl_app_name.setText(_translate("GameDetails", "Application name"))
self.lbl_version.setText(_translate("GameDetails", "Version"))
self.lbl_grade.setText(_translate("GameDetails", "ProtonDB grade"))
self.lbl_install_size.setText(_translate("GameDetails", "Installation size"))
self.lbl_install_path.setText(_translate("GameDetails", "Installation path"))
self.lbl_platform.setText(_translate("GameDetails", "Platform"))
self.lbl_game_actions.setText(_translate("GameDetails", "Actions"))
self.modify_button.setText(_translate("GameDetails", "Modify"))
self.verify_button.setText(_translate("GameDetails", "Verify"))
self.repair_button.setText(_translate("GameDetails", "Repair"))
self.move_button.setText(_translate("GameDetails", "Move"))
self.uninstall_button.setText(_translate("GameDetails", "Uninstall"))
self.install_button.setText(_translate("GameDetails", "Install"))
self.import_button.setText(_translate("GameDetails", "Import"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
GameInfo = QtWidgets.QWidget()
ui = Ui_GameInfo()
ui.setupUi(GameInfo)
GameInfo.show()
GameDetails = QtWidgets.QWidget()
ui = Ui_GameDetails()
ui.setupUi(GameDetails)
GameDetails.show()
sys.exit(app.exec_())

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameInfo</class>
<widget class="QWidget" name="GameInfo">
<class>GameDetails</class>
<widget class="QWidget" name="GameDetails">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>404</height>
<width>814</width>
<height>470</height>
</rect>
</property>
<property name="windowTitle">
<string>Game Info</string>
<string notr="true">GameDetails</string>
</property>
<layout class="QHBoxLayout" name="main_layout" stretch="0,1">
<item>
@ -87,7 +87,13 @@
<item>
<layout class="QVBoxLayout" name="right_layout">
<item>
<layout class="QFormLayout" name="info_layout">
<layout class="QFormLayout" name="details_layout">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
@ -156,7 +162,7 @@
</font>
</property>
<property name="text">
<string>Application Name</string>
<string>Application name</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -220,7 +226,7 @@
</font>
</property>
<property name="text">
<string>ProtonDB Grade</string>
<string>ProtonDB grade</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -255,7 +261,7 @@
</font>
</property>
<property name="text">
<string>Installation Size</string>
<string>Installation size</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -287,7 +293,7 @@
</font>
</property>
<property name="text">
<string>Installation Path</string>
<string>Installation path</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/game_info/game_dlc_widget.ui'
# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/game_info/dlc_widget.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/game_info/game_dlc.ui'
# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/game_info/dlcs.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@ -11,16 +11,16 @@
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GameDlc(object):
def setupUi(self, GameDlc):
GameDlc.setObjectName("GameDlc")
GameDlc.resize(271, 139)
GameDlc.setWindowTitle("GameDlc")
GameDlc.setFrameShape(QtWidgets.QFrame.StyledPanel)
GameDlc.setFrameShadow(QtWidgets.QFrame.Sunken)
GameDlc.setLineWidth(0)
class Ui_GameDlcs(object):
def setupUi(self, GameDlcs):
GameDlcs.setObjectName("GameDlcs")
GameDlcs.resize(271, 141)
GameDlcs.setWindowTitle("GameDlcs")
GameDlcs.setFrameShape(QtWidgets.QFrame.StyledPanel)
GameDlcs.setFrameShadow(QtWidgets.QFrame.Sunken)
GameDlcs.setLineWidth(0)
self.installed_dlc_page = QtWidgets.QWidget()
self.installed_dlc_page.setGeometry(QtCore.QRect(0, 0, 267, 79))
self.installed_dlc_page.setGeometry(QtCore.QRect(0, 0, 287, 62))
self.installed_dlc_page.setObjectName("installed_dlc_page")
self.installed_dlc_page_layout = QtWidgets.QVBoxLayout(self.installed_dlc_page)
self.installed_dlc_page_layout.setContentsMargins(0, 0, 0, 0)
@ -35,9 +35,9 @@ class Ui_GameDlc(object):
self.installed_dlc_container_layout.setObjectName("installed_dlc_container_layout")
self.installed_dlc_page_layout.addWidget(self.installed_dlc_container, 0, QtCore.Qt.AlignTop)
self.installed_dlc_page_layout.setStretch(1, 1)
GameDlc.addItem(self.installed_dlc_page, "")
GameDlcs.addItem(self.installed_dlc_page, "")
self.available_dlc_page = QtWidgets.QWidget()
self.available_dlc_page.setGeometry(QtCore.QRect(0, 0, 267, 79))
self.available_dlc_page.setGeometry(QtCore.QRect(0, 0, 271, 83))
self.available_dlc_page.setObjectName("available_dlc_page")
self.available_dlc_page_layou = QtWidgets.QVBoxLayout(self.available_dlc_page)
self.available_dlc_page_layou.setContentsMargins(0, 0, 0, 0)
@ -52,23 +52,23 @@ class Ui_GameDlc(object):
self.available_dlc_container_layout.setObjectName("available_dlc_container_layout")
self.available_dlc_page_layou.addWidget(self.available_dlc_container, 0, QtCore.Qt.AlignTop)
self.available_dlc_page_layou.setStretch(1, 1)
GameDlc.addItem(self.available_dlc_page, "")
GameDlcs.addItem(self.available_dlc_page, "")
self.retranslateUi(GameDlc)
self.retranslateUi(GameDlcs)
def retranslateUi(self, GameDlc):
def retranslateUi(self, GameDlcs):
_translate = QtCore.QCoreApplication.translate
self.installed_dlc_label.setText(_translate("GameDlc", "No Downloadable Content has been installed."))
GameDlc.setItemText(GameDlc.indexOf(self.installed_dlc_page), _translate("GameDlc", "Installed DLCs"))
self.available_dlc_label.setText(_translate("GameDlc", "No Downloadable Content is available"))
GameDlc.setItemText(GameDlc.indexOf(self.available_dlc_page), _translate("GameDlc", "Available DLCs"))
self.installed_dlc_label.setText(_translate("GameDlcs", "No Downloadable Content has been installed."))
GameDlcs.setItemText(GameDlcs.indexOf(self.installed_dlc_page), _translate("GameDlcs", "Installed DLCs"))
self.available_dlc_label.setText(_translate("GameDlcs", "No Downloadable Content is available"))
GameDlcs.setItemText(GameDlcs.indexOf(self.available_dlc_page), _translate("GameDlcs", "Available DLCs"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
GameDlc = QtWidgets.QToolBox()
ui = Ui_GameDlc()
ui.setupUi(GameDlc)
GameDlc.show()
GameDlcs = QtWidgets.QToolBox()
ui = Ui_GameDlcs()
ui.setupUi(GameDlcs)
GameDlcs.show()
sys.exit(app.exec_())

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameDlc</class>
<widget class="QToolBox" name="GameDlc">
<class>GameDlcs</class>
<widget class="QToolBox" name="GameDlcs">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>271</width>
<height>139</height>
<height>141</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">GameDlc</string>
<string notr="true">GameDlcs</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
@ -27,8 +27,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>267</width>
<height>79</height>
<width>287</width>
<height>62</height>
</rect>
</property>
<attribute name="label">
@ -79,8 +79,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>267</width>
<height>79</height>
<width>271</width>
<height>83</height>
</rect>
</property>
<attribute name="label">

View file

@ -16,8 +16,8 @@ class Ui_RareSettings(object):
RareSettings.setObjectName("RareSettings")
RareSettings.resize(629, 447)
RareSettings.setWindowTitle("RareSettings")
self.rare_layout = QtWidgets.QHBoxLayout(RareSettings)
self.rare_layout.setObjectName("rare_layout")
self.main_layout = QtWidgets.QHBoxLayout(RareSettings)
self.main_layout.setObjectName("main_layout")
self.left_layout = QtWidgets.QVBoxLayout()
self.left_layout.setObjectName("left_layout")
self.interface_group = QtWidgets.QGroupBox(RareSettings)
@ -93,7 +93,7 @@ class Ui_RareSettings(object):
self.left_layout.addWidget(self.settings_group)
spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.left_layout.addItem(spacerItem1)
self.rare_layout.addLayout(self.left_layout)
self.main_layout.addLayout(self.left_layout)
self.right_layout = QtWidgets.QVBoxLayout()
self.right_layout.setObjectName("right_layout")
self.log_dir_group = QtWidgets.QGroupBox(RareSettings)
@ -130,7 +130,7 @@ class Ui_RareSettings(object):
self.right_layout.addWidget(self.groupBox)
spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.right_layout.addItem(spacerItem2)
self.rare_layout.addLayout(self.right_layout)
self.main_layout.addLayout(self.right_layout)
self.retranslateUi(RareSettings)
@ -145,7 +145,7 @@ class Ui_RareSettings(object):
self.settings_group.setTitle(_translate("RareSettings", "Behavior"))
self.save_size.setText(_translate("RareSettings", "Restore window size on application startup"))
self.notification.setText(_translate("RareSettings", "Show notifications when downloads complete"))
self.log_games.setText(_translate("RareSettings", "Show console windows when launching games"))
self.log_games.setText(_translate("RareSettings", "Show console window when launching games"))
self.sys_tray.setText(_translate("RareSettings", "Exit to system tray"))
self.auto_update.setText(_translate("RareSettings", "Queue game updates on application startup"))
self.confirm_start.setText(_translate("RareSettings", "Confirm before launching games"))

View file

@ -13,7 +13,7 @@
<property name="windowTitle">
<string notr="true">RareSettings</string>
</property>
<layout class="QHBoxLayout" name="rare_layout">
<layout class="QHBoxLayout" name="main_layout">
<item>
<layout class="QVBoxLayout" name="left_layout">
<item>
@ -115,7 +115,7 @@
<item row="6" column="0">
<widget class="QCheckBox" name="log_games">
<property name="text">
<string>Show console windows when launching games</string>
<string>Show console window when launching games</string>
</property>
</widget>
</item>

View file

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/widgets/discord_rpc.ui'
#
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_DiscordRPCSettings(object):
def setupUi(self, DiscordRPCSettings):
DiscordRPCSettings.setObjectName("DiscordRPCSettings")
DiscordRPCSettings.resize(370, 149)
DiscordRPCSettings.setWindowTitle("DiscordRPCSettings")
self.main_layout = QtWidgets.QGridLayout(DiscordRPCSettings)
self.main_layout.setObjectName("main_layout")
self.enable = QtWidgets.QComboBox(DiscordRPCSettings)
self.enable.setObjectName("enable")
self.enable.addItem("")
self.enable.addItem("")
self.enable.addItem("")
self.main_layout.addWidget(self.enable, 0, 1, 1, 1)
self.label = QtWidgets.QLabel(DiscordRPCSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
self.label.setSizePolicy(sizePolicy)
self.label.setObjectName("label")
self.main_layout.addWidget(self.label, 0, 0, 1, 1)
self.show_game = QtWidgets.QCheckBox(DiscordRPCSettings)
self.show_game.setObjectName("show_game")
self.main_layout.addWidget(self.show_game, 1, 0, 1, 2)
self.show_os = QtWidgets.QCheckBox(DiscordRPCSettings)
self.show_os.setObjectName("show_os")
self.main_layout.addWidget(self.show_os, 2, 0, 1, 2)
self.show_time = QtWidgets.QCheckBox(DiscordRPCSettings)
self.show_time.setObjectName("show_time")
self.main_layout.addWidget(self.show_time, 3, 0, 1, 2)
self.retranslateUi(DiscordRPCSettings)
def retranslateUi(self, DiscordRPCSettings):
_translate = QtCore.QCoreApplication.translate
DiscordRPCSettings.setTitle(_translate("DiscordRPCSettings", "Discord RPC"))
self.enable.setItemText(0, _translate("DiscordRPCSettings", "When Playing"))
self.enable.setItemText(1, _translate("DiscordRPCSettings", "Always"))
self.enable.setItemText(2, _translate("DiscordRPCSettings", "Never"))
self.label.setText(_translate("DiscordRPCSettings", "Show"))
self.show_game.setText(_translate("DiscordRPCSettings", "Show Game"))
self.show_os.setText(_translate("DiscordRPCSettings", "Show OS"))
self.show_time.setText(_translate("DiscordRPCSettings", "Show Time playing"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
DiscordRPCSettings = QtWidgets.QGroupBox()
ui = Ui_DiscordRPCSettings()
ui.setupUi(DiscordRPCSettings)
DiscordRPCSettings.show()
sys.exit(app.exec_())

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DiscordRPCSettings</class>
<widget class="QGroupBox" name="DiscordRPCSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>370</width>
<height>149</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">DiscordRPCSettings</string>
</property>
<property name="title">
<string>Discord RPC</string>
</property>
<layout class="QGridLayout" name="main_layout">
<item row="0" column="1">
<widget class="QComboBox" name="enable">
<item>
<property name="text">
<string>When Playing</string>
</property>
</item>
<item>
<property name="text">
<string>Always</string>
</property>
</item>
<item>
<property name="text">
<string>Never</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Show</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="show_game">
<property name="text">
<string>Show Game</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="show_os">
<property name="text">
<string>Show OS</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="show_time">
<property name="text">
<string>Show Time playing</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/widgets/rpc.ui'
#
# Created by: PyQt5 UI code generator 5.15.6
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_RPCSettings(object):
def setupUi(self, RPCSettings):
RPCSettings.setObjectName("RPCSettings")
RPCSettings.resize(174, 146)
RPCSettings.setWindowTitle("DiscordRPC")
self.layout = QtWidgets.QGridLayout(RPCSettings)
self.layout.setObjectName("layout")
self.enable = QtWidgets.QComboBox(RPCSettings)
self.enable.setObjectName("enable")
self.enable.addItem("")
self.enable.addItem("")
self.enable.addItem("")
self.layout.addWidget(self.enable, 0, 1, 1, 1)
self.label = QtWidgets.QLabel(RPCSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
self.label.setSizePolicy(sizePolicy)
self.label.setObjectName("label")
self.layout.addWidget(self.label, 0, 0, 1, 1)
self.show_game = QtWidgets.QCheckBox(RPCSettings)
self.show_game.setObjectName("show_game")
self.layout.addWidget(self.show_game, 1, 0, 1, 2)
self.show_os = QtWidgets.QCheckBox(RPCSettings)
self.show_os.setObjectName("show_os")
self.layout.addWidget(self.show_os, 2, 0, 1, 2)
self.show_time = QtWidgets.QCheckBox(RPCSettings)
self.show_time.setObjectName("show_time")
self.layout.addWidget(self.show_time, 3, 0, 1, 2)
self.retranslateUi(RPCSettings)
def retranslateUi(self, RPCSettings):
_translate = QtCore.QCoreApplication.translate
RPCSettings.setTitle(_translate("RPCSettings", "Discord RPC"))
self.enable.setItemText(0, _translate("RPCSettings", "When Playing"))
self.enable.setItemText(1, _translate("RPCSettings", "Always"))
self.enable.setItemText(2, _translate("RPCSettings", "Never"))
self.label.setText(_translate("RPCSettings", "Show"))
self.show_game.setText(_translate("RPCSettings", "Show Game"))
self.show_os.setText(_translate("RPCSettings", "Show OS"))
self.show_time.setText(_translate("RPCSettings", "Show Time playing"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
RPCSettings = QtWidgets.QGroupBox()
ui = Ui_RPCSettings()
ui.setupUi(RPCSettings)
RPCSettings.show()
sys.exit(app.exec_())

View file

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RPCSettings</class>
<widget class="QGroupBox" name="RPCSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>174</width>
<height>146</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">DiscordRPC</string>
</property>
<property name="title">
<string>Discord RPC</string>
</property>
<layout class="QGridLayout" name="layout">
<item row="0" column="1">
<widget class="QComboBox" name="enable">
<item>
<property name="text">
<string>When Playing</string>
</property>
</item>
<item>
<property name="text">
<string>Always</string>
</property>
</item>
<item>
<property name="text">
<string>Never</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Show</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="show_game">
<property name="text">
<string>Show Game</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="show_os">
<property name="text">
<string>Show OS</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="show_time">
<property name="text">
<string>Show Time playing</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -11,16 +11,16 @@
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_DetailsWidget(object):
def setupUi(self, DetailsWidget):
DetailsWidget.setObjectName("DetailsWidget")
DetailsWidget.resize(630, 371)
DetailsWidget.setWindowTitle("DetailsWidget")
self.main_layout = QtWidgets.QHBoxLayout(DetailsWidget)
class Ui_StoreDetailsWidget(object):
def setupUi(self, StoreDetailsWidget):
StoreDetailsWidget.setObjectName("StoreDetailsWidget")
StoreDetailsWidget.resize(630, 382)
StoreDetailsWidget.setWindowTitle("StoreDetailsWidget")
self.main_layout = QtWidgets.QHBoxLayout(StoreDetailsWidget)
self.main_layout.setObjectName("main_layout")
self.left_layout = QtWidgets.QVBoxLayout()
self.left_layout.setObjectName("left_layout")
self.back_button = QtWidgets.QPushButton(DetailsWidget)
self.back_button = QtWidgets.QPushButton(StoreDetailsWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -36,12 +36,12 @@ class Ui_DetailsWidget(object):
self.right_layout.setObjectName("right_layout")
self.details_layout = QtWidgets.QFormLayout()
self.details_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.details_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.FieldsStayAtSizeHint)
self.details_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
self.details_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.details_layout.setContentsMargins(6, 6, 6, 6)
self.details_layout.setSpacing(12)
self.details_layout.setObjectName("details_layout")
self.title_label = QtWidgets.QLabel(DetailsWidget)
self.title_label = QtWidgets.QLabel(StoreDetailsWidget)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
@ -49,12 +49,12 @@ class Ui_DetailsWidget(object):
self.title_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.title_label.setObjectName("title_label")
self.details_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.title_label)
self.title = QtWidgets.QLabel(DetailsWidget)
self.title = QtWidgets.QLabel(StoreDetailsWidget)
self.title.setText("title")
self.title.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.title.setObjectName("title")
self.details_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.title)
self.developer_label = QtWidgets.QLabel(DetailsWidget)
self.developer_label = QtWidgets.QLabel(StoreDetailsWidget)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
@ -62,12 +62,12 @@ class Ui_DetailsWidget(object):
self.developer_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.developer_label.setObjectName("developer_label")
self.details_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.developer_label)
self.developer = QtWidgets.QLabel(DetailsWidget)
self.developer = QtWidgets.QLabel(StoreDetailsWidget)
self.developer.setText("developer")
self.developer.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.developer.setObjectName("developer")
self.details_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.developer)
self.publisher_label = QtWidgets.QLabel(DetailsWidget)
self.publisher_label = QtWidgets.QLabel(StoreDetailsWidget)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
@ -75,12 +75,12 @@ class Ui_DetailsWidget(object):
self.publisher_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.publisher_label.setObjectName("publisher_label")
self.details_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.publisher_label)
self.publisher = QtWidgets.QLabel(DetailsWidget)
self.publisher = QtWidgets.QLabel(StoreDetailsWidget)
self.publisher.setText("publisher")
self.publisher.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.publisher.setObjectName("publisher")
self.details_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.publisher)
self.status_label = QtWidgets.QLabel(DetailsWidget)
self.status_label = QtWidgets.QLabel(StoreDetailsWidget)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
@ -88,10 +88,10 @@ class Ui_DetailsWidget(object):
self.status_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.status_label.setObjectName("status_label")
self.details_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.status_label)
self.status = QtWidgets.QLabel(DetailsWidget)
self.status = QtWidgets.QLabel(StoreDetailsWidget)
self.status.setObjectName("status")
self.details_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.status)
self.price_label = QtWidgets.QLabel(DetailsWidget)
self.price_label = QtWidgets.QLabel(StoreDetailsWidget)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
@ -99,7 +99,7 @@ class Ui_DetailsWidget(object):
self.price_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.price_label.setObjectName("price_label")
self.details_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.price_label)
self.tags_label = QtWidgets.QLabel(DetailsWidget)
self.tags_label = QtWidgets.QLabel(StoreDetailsWidget)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
@ -107,11 +107,11 @@ class Ui_DetailsWidget(object):
self.tags_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.tags_label.setObjectName("tags_label")
self.details_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.tags_label)
self.tags = QtWidgets.QLabel(DetailsWidget)
self.tags = QtWidgets.QLabel(StoreDetailsWidget)
self.tags.setText("tags")
self.tags.setObjectName("tags")
self.details_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.tags)
self.social_links_label = QtWidgets.QLabel(DetailsWidget)
self.social_links_label = QtWidgets.QLabel(StoreDetailsWidget)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
@ -119,7 +119,7 @@ class Ui_DetailsWidget(object):
self.social_links_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.social_links_label.setObjectName("social_links_label")
self.details_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.social_links_label)
self.actions_label = QtWidgets.QLabel(DetailsWidget)
self.actions_label = QtWidgets.QLabel(StoreDetailsWidget)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
@ -127,14 +127,14 @@ class Ui_DetailsWidget(object):
self.actions_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.actions_label.setObjectName("actions_label")
self.details_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.actions_label)
self.social_links = QtWidgets.QWidget(DetailsWidget)
self.social_links = QtWidgets.QWidget(StoreDetailsWidget)
self.social_links.setObjectName("social_links")
self.social_links_layout = QtWidgets.QHBoxLayout(self.social_links)
self.social_links_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.social_links_layout.setContentsMargins(0, 0, 0, 0)
self.social_links_layout.setObjectName("social_links_layout")
self.details_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.social_links)
self.actions = QtWidgets.QWidget(DetailsWidget)
self.actions = QtWidgets.QWidget(StoreDetailsWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -152,7 +152,12 @@ class Ui_DetailsWidget(object):
self.wishlist_button.setObjectName("wishlist_button")
self.actions_layout.addWidget(self.wishlist_button)
self.details_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.actions)
self.price = QtWidgets.QWidget(DetailsWidget)
self.price = QtWidgets.QWidget(StoreDetailsWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.price.sizePolicy().hasHeightForWidth())
self.price.setSizePolicy(sizePolicy)
self.price.setObjectName("price")
self.price_layout = QtWidgets.QHBoxLayout(self.price)
self.price_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
@ -169,7 +174,7 @@ class Ui_DetailsWidget(object):
self.price_layout.addWidget(self.discount_price)
self.details_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.price)
self.right_layout.addLayout(self.details_layout)
self.requirements_frame = QtWidgets.QFrame(DetailsWidget)
self.requirements_frame = QtWidgets.QFrame(StoreDetailsWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -182,35 +187,35 @@ class Ui_DetailsWidget(object):
self.requirements_layout.setContentsMargins(0, 0, 0, 0)
self.requirements_layout.setObjectName("requirements_layout")
self.right_layout.addWidget(self.requirements_frame)
self.description_label = QtWidgets.QTextBrowser(DetailsWidget)
self.description_label = QtWidgets.QTextBrowser(StoreDetailsWidget)
self.description_label.setOpenExternalLinks(True)
self.description_label.setObjectName("description_label")
self.right_layout.addWidget(self.description_label)
self.main_layout.addLayout(self.right_layout)
self.main_layout.setStretch(1, 1)
self.retranslateUi(DetailsWidget)
self.retranslateUi(StoreDetailsWidget)
def retranslateUi(self, DetailsWidget):
def retranslateUi(self, StoreDetailsWidget):
_translate = QtCore.QCoreApplication.translate
self.title_label.setText(_translate("DetailsWidget", "Title"))
self.developer_label.setText(_translate("DetailsWidget", "Developer"))
self.publisher_label.setText(_translate("DetailsWidget", "Publisher"))
self.status_label.setText(_translate("DetailsWidget", "Status"))
self.status.setText(_translate("DetailsWidget", "You already own this game"))
self.price_label.setText(_translate("DetailsWidget", "Price"))
self.tags_label.setText(_translate("DetailsWidget", "Tags"))
self.social_links_label.setText(_translate("DetailsWidget", "Links"))
self.actions_label.setText(_translate("DetailsWidget", "Actions"))
self.store_button.setText(_translate("DetailsWidget", "Buy in Epic Games Store"))
self.wishlist_button.setText(_translate("DetailsWidget", "Add to wishlist"))
self.title_label.setText(_translate("StoreDetailsWidget", "Title"))
self.developer_label.setText(_translate("StoreDetailsWidget", "Developer"))
self.publisher_label.setText(_translate("StoreDetailsWidget", "Publisher"))
self.status_label.setText(_translate("StoreDetailsWidget", "Status"))
self.status.setText(_translate("StoreDetailsWidget", "You already own this game"))
self.price_label.setText(_translate("StoreDetailsWidget", "Price"))
self.tags_label.setText(_translate("StoreDetailsWidget", "Tags"))
self.social_links_label.setText(_translate("StoreDetailsWidget", "Links"))
self.actions_label.setText(_translate("StoreDetailsWidget", "Actions"))
self.store_button.setText(_translate("StoreDetailsWidget", "Buy in Epic Games Store"))
self.wishlist_button.setText(_translate("StoreDetailsWidget", "Add to wishlist"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
DetailsWidget = QtWidgets.QWidget()
ui = Ui_DetailsWidget()
ui.setupUi(DetailsWidget)
DetailsWidget.show()
StoreDetailsWidget = QtWidgets.QWidget()
ui = Ui_StoreDetailsWidget()
ui.setupUi(StoreDetailsWidget)
StoreDetailsWidget.show()
sys.exit(app.exec_())

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DetailsWidget</class>
<widget class="QWidget" name="DetailsWidget">
<class>StoreDetailsWidget</class>
<widget class="QWidget" name="StoreDetailsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>630</width>
<height>371</height>
<height>382</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">DetailsWidget</string>
<string notr="true">StoreDetailsWidget</string>
</property>
<layout class="QHBoxLayout" name="main_layout" stretch="0,1">
<item>
@ -48,7 +48,7 @@
<enum>QLayout::SetFixedSize</enum>
</property>
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldsStayAtSizeHint</enum>
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -310,6 +310,12 @@
</item>
<item row="4" column="1">
<widget class="QWidget" name="price" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="price_layout">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>

View file

@ -1,4 +1,3 @@
import platform as pf
import os
import shlex
from dataclasses import dataclass
@ -7,25 +6,23 @@ from hashlib import md5
from logging import getLogger
from typing import Optional, Union, List, Dict, Set
if pf.system() in {"Linux", "FreeBSD"}:
# noinspection PyUnresolvedReferences
import vdf # pylint: disable=E0401
import vdf
logger = getLogger("Proton")
logger = getLogger("SteamTools")
steam_compat_client_install_paths = [os.path.expanduser("~/.local/share/Steam")]
steam_client_install_paths = [os.path.expanduser("~/.local/share/Steam")]
def find_steam() -> Optional[str]:
# return the first valid path
for path in steam_compat_client_install_paths:
for path in steam_client_install_paths:
if os.path.isdir(path) and os.path.isfile(os.path.join(path, "steam.sh")):
return path
return None
def find_libraries(steam_path: str) -> Set[str]:
vdf_path = os.path.join(steam_path, "steamapps", "libraryfolders.vdf")
vdf_path = os.path.join(steam_path, "config", "libraryfolders.vdf")
with open(vdf_path, "r") as f:
libraryfolders = vdf.load(f)["libraryfolders"]
# libraries = [os.path.join(folder["path"], "steamapps") for key, folder in libraryfolders.items()]
@ -149,6 +146,31 @@ def find_appmanifests(library: str) -> List[dict]:
return appmanifests
def find_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]:
runtimes = {}
appmanifests = find_appmanifests(library)
common = os.path.join(library, "common")
for appmanifest in appmanifests:
folder = appmanifest["AppState"]["installdir"]
tool_path = os.path.join(common, folder)
if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")):
with open(vdf_file, "r") as f:
toolmanifest = vdf.load(f)
if toolmanifest["manifest"]["compatmanager_layer_name"] == "container-runtime":
runtimes.update(
{
appmanifest["AppState"]["appid"]: SteamRuntime(
steam_path=steam_path,
steam_library=library,
appmanifest=appmanifest,
tool_path=tool_path,
toolmanifest=toolmanifest["manifest"],
)
}
)
return runtimes
def find_protons(steam_path: str, library: str) -> List[ProtonTool]:
protons = []
appmanifests = find_appmanifests(library)
@ -225,33 +247,7 @@ def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]:
return tools
def find_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]:
runtimes = {}
appmanifests = find_appmanifests(library)
common = os.path.join(library, "common")
for appmanifest in appmanifests:
folder = appmanifest["AppState"]["installdir"]
tool_path = os.path.join(common, folder)
if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")):
with open(vdf_file, "r") as f:
toolmanifest = vdf.load(f)
if toolmanifest["manifest"]["compatmanager_layer_name"] == "container-runtime":
print(toolmanifest["manifest"])
runtimes.update(
{
appmanifest["AppState"]["appid"]: SteamRuntime(
steam_path=steam_path,
steam_library=library,
appmanifest=appmanifest,
tool_path=tool_path,
toolmanifest=toolmanifest["manifest"],
)
}
)
return runtimes
def find_runtime(
def get_runtime(
tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, SteamRuntime]
) -> Optional[SteamRuntime]:
required_tool = tool.required_tool
@ -305,7 +301,7 @@ def get_steam_environment(
return environ
def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
def _find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
steam_path = find_steam()
if steam_path is None:
logger.info("Steam could not be found")
@ -326,7 +322,7 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
tools.extend(find_compatibility_tools(steam_path))
for tool in tools:
runtime = find_runtime(tool, runtimes)
runtime = get_runtime(tool, runtimes)
tool.runtime = runtime
tools = list(filter(lambda t: bool(t), tools))
@ -334,14 +330,34 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
return tools
_tools: Optional[List[Union[ProtonTool, CompatibilityTool]]] = None
def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
global _tools
if _tools is None:
_tools = _find_tools()
return list(filter(lambda t: t.layer != "umu-launcher", _tools))
def find_umu_launcher() -> Optional[CompatibilityTool]:
global _tools
if _tools is None:
_tools = _find_tools()
_umu = list(filter(lambda t: t.layer == "umu-launcher", _tools))
return _umu[0] if _umu else None
if __name__ == "__main__":
from pprint import pprint
_tools = find_tools()
pprint(_tools)
tools = find_tools()
pprint(tools)
umu = find_umu_launcher()
pprint(umu)
for _tool in _tools:
print(get_steam_environment(_tool))
print(_tool.name)
print(_tool.command(SteamVerb.RUN))
print(" ".join(_tool.command(SteamVerb.RUN_IN_PREFIX)))
for tool in tools:
print(get_steam_environment(tool))
print(tool.name)
print(tool.command(SteamVerb.RUN))
print(" ".join(tool.command(SteamVerb.RUN_IN_PREFIX)))

View file

@ -1,5 +1,5 @@
import os
import platform
import platform as pf
import subprocess
from configparser import ConfigParser
from logging import getLogger
@ -8,9 +8,9 @@ from typing import Mapping, Dict, List, Tuple
from PyQt5.QtCore import QProcess, QProcessEnvironment
from rare.utils import config_helper as config
if platform.system() != "Windows":
if pf.system() != "Windows":
from . import wine
if platform.system() != "Darwin":
if pf.system() in {"Linux", "FreeBSD"}:
from . import steam
logger = getLogger("CompatUtils")
@ -29,7 +29,7 @@ def read_registry(registry: str, prefix: str) -> ConfigParser:
def get_configured_qprocess(command: List[str], environment: Mapping) -> QProcess:
logger.debug("Executing command: %s", command)
logger.debug("Executing command: %s", command)
proc = QProcess()
proc.setProcessChannelMode(QProcess.SeparateChannels)
penv = QProcessEnvironment()
@ -42,7 +42,7 @@ def get_configured_qprocess(command: List[str], environment: Mapping) -> QProces
def get_configured_subprocess(command: List[str], environment: Mapping) -> subprocess.Popen:
logger.debug("Executing command: %s", command)
logger.debug("Executing command: %s", command)
return subprocess.Popen(
command,
stdin=None,
@ -61,7 +61,7 @@ def execute_subprocess(command: List[str], arguments: List[str], environment: Ma
out, err = out.decode("utf-8", "ignore") if out else "", err.decode("utf-8", "ignore") if err else ""
# lk: the following is a work-around for wineserver sometimes hanging around after
proc = get_configured_subprocess(command + ["wineboot", "-k"], environment)
proc = get_configured_subprocess(command + ["wineboot", "-e"], environment)
_, _ = proc.communicate()
return out, err
@ -77,7 +77,7 @@ def execute_qprocess(command: List[str], arguments: List[str], environment: Mapp
proc.deleteLater()
# lk: the following is a work-around for wineserver sometimes hanging around after
proc = get_configured_qprocess(command + ["wineboot", "-k"], environment)
proc = get_configured_qprocess(command + ["wineboot", "-e"], environment)
proc.start()
proc.waitForFinished(-1)
proc.deleteLater()
@ -90,8 +90,7 @@ def execute(command: List[str], arguments: List[str], environment: Mapping) -> T
# In flatpak our environment is passed through `flatpak-spawn` arguments
if os.environ.get("container") == "flatpak":
flatpak_command = ["flatpak-spawn", "--host"]
for name, value in environment.items():
flatpak_command.append(f"--env={name}={value}")
flatpak_command.extend(f"--env={name}={value}" for name, value in environment.items())
_command = flatpak_command + command
_environment = os.environ.copy()
else:
@ -166,5 +165,5 @@ def get_host_environment(app_environment: Dict, silent: bool = True) -> Dict:
_environ["WINEDEBUG"] = "-all"
_environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
# lk: pressure-vessel complains about this but it doesn't fail due to it
_environ["DISPLAY"] = ""
#_environ["DISPLAY"] = ""
return _environ

View file

@ -1,39 +1,47 @@
import platform
import time
from logging import getLogger
from typing import List
import pypresence.exceptions
from PyQt5.QtCore import QObject, QSettings
from pypresence import Presence
from PyQt5.QtCore import QObject, QSettings, pyqtSlot
from pypresence import Presence, exceptions
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.models.options import options
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
client_id = "830732538225360908"
logger = getLogger("RPC")
logger = getLogger("DiscordRPC")
class DiscordRPC(QObject):
def __init__(self):
super(DiscordRPC, self).__init__()
self.RPC = None
self.rpc = None
self.state = 1 # 0: game, 1: always active, 2: off
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.settings = QSettings()
if self.settings.value(*options.rpc_enable) == 1: # show always
if self.settings.value(*options.discord_rpc_mode) == 1: # show always
self.state = 2
self.set_discord_rpc()
self.signals.discord_rpc.set_title.connect(self.update_presence)
self.signals.discord_rpc.apply_settings.connect(self.changed_settings)
self.signals.discord_rpc.update_presence.connect(self.update_presence)
self.signals.discord_rpc.remove_presence.connect(self.remove_presence)
self.signals.discord_rpc.update_settings.connect(self.update_settings)
def update_presence(self, app_name):
@pyqtSlot(str)
def update_presence(self, app_name: str):
self.set_discord_rpc(app_name)
def changed_settings(self, game_running: list = None):
value = self.settings.value(*options.rpc_enable)
@pyqtSlot(str)
def remove_presence(self, app_name: str):
self.set_discord_rpc(None)
@pyqtSlot()
@pyqtSlot(list)
def update_settings(self, game_running: List = None):
value = self.settings.value(*options.discord_rpc_mode)
if value == 2:
self.remove_rpc()
return
@ -45,15 +53,15 @@ class DiscordRPC(QObject):
self.set_discord_rpc(game_running[0])
def remove_rpc(self):
if self.settings.value(*options.rpc_enable) != 1:
if not self.RPC:
if self.settings.value(*options.discord_rpc_mode) != 1:
if not self.rpc:
return
try:
self.RPC.close()
self.rpc.close()
except Exception:
logger.warning("Already closed")
del self.RPC
self.RPC = None
del self.rpc
self.rpc = None
logger.info("Remove RPC")
self.state = 2
else:
@ -61,56 +69,50 @@ class DiscordRPC(QObject):
self.set_discord_rpc()
def set_discord_rpc(self, app_name=None):
if not self.RPC:
if not self.rpc:
try:
self.RPC = Presence(
client_id
) # Rare app: https://discord.com/developers/applications
self.RPC.connect()
self.rpc = Presence(client_id) # Rare app: https://discord.com/developers/applications
self.rpc.connect()
except ConnectionRefusedError as e:
logger.warning(f"Discord is not active\n{e}")
self.RPC = None
self.rpc = None
return
except FileNotFoundError as e:
logger.warning(f"File not found error\n{e}")
self.RPC = None
self.rpc = None
return
except pypresence.exceptions.InvalidPipe as e:
except exceptions.InvalidPipe as e:
logger.error(f"Is Discord running? \n{e}")
self.RPC = None
self.rpc = None
return
except Exception as e:
logger.error(str(e))
self.RPC = None
self.rpc = None
return
self.update_rpc(app_name)
def update_rpc(self, app_name=None):
if self.settings.value(*options.rpc_enable) == 2 or (
not app_name and self.settings.value(*options.rpc_enable) == 0
if self.settings.value(*options.discord_rpc_mode) == 2 or (
not app_name and self.settings.value(*options.discord_rpc_mode) == 0
):
self.remove_rpc()
return
title = None
if not app_name:
self.RPC.update(
large_image="logo", details="https://github.com/RareDevs/Rare"
)
self.rpc.update(large_image="logo", details="https://github.com/RareDevs/Rare")
return
if self.settings.value(*options.rpc_name):
if self.settings.value(*options.discord_rpc_game):
try:
title = self.core.get_installed_game(app_name).title
except AttributeError:
logger.error(f"Could not get title of game: {app_name}")
title = app_name
start = None
if self.settings.value(*options.rpc_time):
if self.settings.value(*options.discord_rpc_time):
start = str(time.time()).split(".")[0]
os = None
if self.settings.value(*options.rpc_os):
if self.settings.value(*options.discord_rpc_os):
os = f"via Rare on {platform.system()}"
self.RPC.update(
large_image="logo", details=title, large_text=title, state=os, start=start
)
self.rpc.update(large_image="logo", details=title, large_text=title, state=os, start=start)
self.state = 0

View file

@ -1,147 +0,0 @@
from logging import getLogger
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap, QImage, QMovie
from PyQt5.QtWidgets import (
QStyle,
QLabel,
QHBoxLayout,
QWidget,
QPushButton,
QLineEdit,
)
from rare.utils.misc import qta_icon
from rare.utils.paths import cache_dir
from rare.utils.qt_requests import QtRequests
logger = getLogger("ExtraWidgets")
# FIXME: move this?
class WaitingSpinner(QLabel):
def __init__(self, parent=None):
super(WaitingSpinner, self).__init__(parent=parent)
self.setObjectName(type(self).__name__)
self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.movie = QMovie(":/images/loader.gif")
self.setMovie(self.movie)
self.movie.start()
class SelectViewWidget(QWidget):
toggled = pyqtSignal(bool)
def __init__(self, icon_view: bool, parent=None):
super(SelectViewWidget, self).__init__(parent=parent)
self.icon_button = QPushButton(self)
self.icon_button.setObjectName(f"{type(self).__name__}Button")
self.list_button = QPushButton(self)
self.list_button.setObjectName(f"{type(self).__name__}Button")
if icon_view:
self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="orange"))
self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="#eee"))
else:
self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="#eee"))
self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="orange"))
self.icon_button.clicked.connect(self.icon)
self.list_button.clicked.connect(self.list)
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.icon_button)
layout.addWidget(self.list_button)
self.setLayout(layout)
def icon(self):
self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="orange"))
self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="#eee"))
self.toggled.emit(True)
def list(self):
self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="#eee"))
self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="orange"))
self.toggled.emit(False)
class ImageLabel(QLabel):
image = None
img_size = None
name = ""
def __init__(self, parent=None):
super(ImageLabel, self).__init__(parent=parent)
self.manager = QtRequests(
cache=str(cache_dir().joinpath("store")),
parent=self
)
def update_image(self, url, name="", size: tuple = (240, 320)):
self.setFixedSize(*size)
self.img_size = size
self.name = name
for c in r'<>?":|\/* ':
self.name = self.name.replace(c, "")
if self.img_size[0] > self.img_size[1]:
name_extension = "wide"
else:
name_extension = "tall"
self.name = f"{self.name}_{name_extension}.png"
self.manager.get(url, self.image_ready)
def image_ready(self, data):
try:
self.setPixmap(QPixmap())
except Exception:
logger.warning("C++ object already removed, when image ready")
return
image = QImage()
image.loadFromData(data)
image = image.scaled(
*self.img_size[:2],
Qt.KeepAspectRatio,
transformMode=Qt.SmoothTransformation,
)
pixmap = QPixmap().fromImage(image)
self.setPixmap(pixmap)
class ButtonLineEdit(QLineEdit):
buttonClicked = pyqtSignal()
def __init__(self, icon_name, placeholder_text: str, parent=None):
super(ButtonLineEdit, self).__init__(parent=parent)
self.setObjectName(type(self).__name__)
self.button = QPushButton(self)
self.button.setObjectName(f"{type(self).__name__}Button")
self.button.setIcon(qta_icon(icon_name))
self.button.setCursor(Qt.ArrowCursor)
self.button.clicked.connect(self.buttonClicked.emit)
self.setPlaceholderText(placeholder_text)
# frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
# button_size = self.button.sizeHint()
#
# self.setStyleSheet(
# f"QLineEdit#{self.objectName()} {{padding-right: {(button_size.width() + frame_width + 1)}px; }}"
# )
# self.setMinimumSize(
# max(self.minimumSizeHint().width(), button_size.width() + frame_width * 2 + 2),
# max(
# self.minimumSizeHint().height(),
# button_size.height() + frame_width * 2 + 2,
# ),
# )
def resizeEvent(self, event):
frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
button_size = self.button.sizeHint()
self.button.move(
self.rect().right() - frame_width - button_size.width(),
(self.rect().bottom() - button_size.height() + 1) // 2,
)
super(ButtonLineEdit, self).resizeEvent(event)

View file

@ -280,29 +280,29 @@ if __name__ == "__main__":
view.setModel(model)
document = json.loads(
"""\
{
"firstName": "John",
"lastName": "Smith",
"age": 25,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021"
},
"phoneNumber": [
{
"type": "home",
"number": "212 555-1234"
"""
{
"firstName": "John",
"lastName": "Smith",
"age": 25,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021"
},
{
"type": "fax",
"number": "646 555-4567"
}
]
}
"""
"phoneNumber": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "fax",
"number": "646 555-4567"
}
]
}
"""
)
model.load(document)
@ -310,9 +310,7 @@ if __name__ == "__main__":
model.load(document)
# Sanity check
assert json.dumps(model.json(), sort_keys=True) == json.dumps(
document, sort_keys=True
)
assert json.dumps(model.json(), sort_keys=True) == json.dumps(document, sort_keys=True)
view.show()
view.resize(500, 300)

View file

@ -55,6 +55,22 @@ def image_dir() -> Path:
return data_dir().joinpath("images")
def image_dir_game(app_name: str) -> Path:
return image_dir().joinpath(app_name)
def image_tall_path(app_name: str, color: bool = True) -> Path:
return image_dir_game(app_name).joinpath("tall.png" if color else "tall_gray.png")
def image_wide_path(app_name: str, color: bool = True) -> Path:
return image_dir_game(app_name).joinpath("wide.png" if color else "wide_gray.png")
def image_icon_path(app_name: str, color: bool = True) -> Path:
return image_dir_game(app_name).joinpath("icon.png" if color else "icon_gray.png")
def log_dir() -> Path:
return cache_dir().joinpath("logs")
@ -105,19 +121,25 @@ __link_suffix = {
"icon": "png",
},
"Darwin": {
"link": "",
"icon": "icns",
"link": "",
"icon": "icns",
},
}
def desktop_links_supported() -> bool:
supported_systems = [k for (k, v) in __link_suffix.items() if v["link"]]
return platform.system() in supported_systems
def desktop_icon_suffix() -> str:
return __link_suffix[platform.system()]["icon"]
def desktop_icon_path(app_name: str) -> Path:
return image_dir_game(app_name).joinpath(f"desktop_icon.{desktop_icon_suffix()}")
__link_type = {
"desktop": desktop_dir(),
# lk: for some undocumented reason, on Windows we used the parent directory
@ -125,8 +147,11 @@ __link_type = {
"start_menu": applications_dir().parent if platform.system() == "Windows" else applications_dir(),
}
def desktop_link_types() -> List:
return list(__link_type.keys())
# fmt: on
@ -212,7 +237,7 @@ def create_desktop_link(app_name: str, app_title: str = "", link_name: str = "",
app_title = "Rare"
link_name = "Rare"
else:
icon_path = image_dir().joinpath(app_name, f"icon.{desktop_icon_suffix()}")
icon_path = desktop_icon_path(app_name)
if not app_title or not link_name:
logger.error("Missing app_title or link_name")
return False

View file

@ -1,7 +1,8 @@
import difflib
import os
from datetime import datetime
from enum import StrEnum, Enum
from enum import Enum
from logging import getLogger
from typing import Tuple
import orjson
@ -10,6 +11,8 @@ import requests
from rare.lgndr.core import LegendaryCore
from rare.utils.paths import cache_dir
logger = getLogger("SteamGrades")
replace_chars = ",;.:-_ "
steamapi_url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
protondb_url = "https://www.protondb.com/api/v1/reports/summaries/"
@ -51,7 +54,8 @@ def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]:
if not steam_id:
raise Exception
grade = get_grade(steam_id)
except Exception as e:
except Exception:
logger.error("Failed to get ProtonDB rating for %s", game.app_title)
return 0, "fail"
else:
return steam_id, grade
@ -64,28 +68,31 @@ def get_grade(steam_code):
steam_code = str(steam_code)
res = requests.get(f"{protondb_url}{steam_code}.json")
try:
lista = orjson.loads(res.text)
except orjson.JSONDecodeError:
app = orjson.loads(res.text)
except orjson.JSONDecodeError as e:
logger.error("Failed to get ProtonDB response for %s", steam_code)
return "fail"
return lista["tier"]
return app.get("tier", "fail")
def load_json() -> dict:
file = os.path.join(cache_dir(), "game_list.json")
mod_time = datetime.fromtimestamp(os.path.getmtime(file))
elapsed_time = abs(datetime.now() - mod_time)
global __active_download
if __active_download:
return {}
if not os.path.exists(file) or elapsed_time.days > 7:
file = os.path.join(cache_dir(), "steam_appids.json")
elapsed_days = 0
if os.path.exists(file):
mod_time = datetime.fromtimestamp(os.path.getmtime(file))
elapsed_days = abs(datetime.now() - mod_time).days
if not os.path.exists(file) or elapsed_days > 7:
__active_download = True
response = requests.get(steamapi_url)
__active_download = False
steam_ids = orjson.loads(response.text)["applist"]["apps"]
apps = orjson.loads(response.text).get("applist", {}).get("apps", {})
ids = {}
for game in steam_ids:
ids[game["name"]] = game["appid"]
for app in apps:
ids[app["name"]] = app["appid"]
with open(file, "w") as f:
f.write(orjson.dumps(ids).decode("utf-8"))
return ids

View file

@ -0,0 +1,194 @@
import os
import platform
import shutil
from logging import getLogger
from pathlib import Path
from typing import Optional, List, Dict
from dataclasses import asdict
import vdf
from rare.utils.paths import image_icon_path, image_wide_path, image_tall_path, desktop_icon_path, get_rare_executable
from rare.models.steam import SteamUser, SteamShortcut
if platform.system() == "Windows":
# noinspection PyUnresolvedReferences
import winreg # pylint: disable=E0401
logger = getLogger("SteamShortcuts")
steam_client_install_paths = [os.path.expanduser("~/.local/share/Steam")]
def find_steam() -> Optional[str]:
if platform.system() == "Windows":
# Find the Steam install directory or raise an error
try: # 32-bit
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Valve\\Steams")
except FileNotFoundError:
try: # 64-bit
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Wow6432Node\\Valve\\Steam")
except FileNotFoundError as e:
return None
return winreg.QueryValueEx(key, "InstallPath")[0]
# return the first valid path
elif platform.system() in {"Linux", "FreeBSD"}:
for path in steam_client_install_paths:
if os.path.isdir(path) and os.path.isfile(os.path.join(path, "steam.sh")):
return path
return None
def find_steam_users(steam_path: str) -> List[SteamUser]:
_users = []
vdf_path = os.path.join(steam_path, "config", "loginusers.vdf")
with open(vdf_path, 'r') as f:
users = vdf.load(f).get("users", {})
for long_id, user in users.items():
_users.append(SteamUser(long_id, user))
return _users
def _load_shortcuts(steam_path: str, user: SteamUser) -> Dict[str, SteamShortcut]:
_shortcuts = {}
vdf_path = os.path.join(steam_path, "userdata", str(user.short_id), "config", "shortcuts.vdf")
with open(vdf_path, 'rb') as f:
shortcuts = vdf.binary_load(f).get("shortcuts", {})
for idx, shortcut in shortcuts.items():
_shortcuts[idx] = SteamShortcut.from_dict(shortcut)
return _shortcuts
def _save_shortcuts(steam_path: str, user: SteamUser, shortcuts: Dict[str, SteamShortcut]) -> None:
_shortcuts = {k: asdict(v) for k, v in shortcuts.items()}
vdf_path = os.path.join(steam_path, "userdata", str(user.short_id), "config", "shortcuts.vdf")
with open(vdf_path, 'wb') as f:
vdf.binary_dump({"shortcuts": _shortcuts}, f)
__steam_dir: Optional[str] = None
__steam_user: Optional[SteamUser] = None
__steam_shortcuts: Optional[Dict] = None
def steam_shortcuts_supported() -> bool:
return __steam_dir is not None and __steam_user is not None and __steam_shortcuts is not None
def load_steam_shortcuts():
global __steam_shortcuts, __steam_dir, __steam_user
if __steam_shortcuts is not None:
return
steam_dir = find_steam()
if not steam_dir:
logger.error("Failed to find Steam install directory")
return
__steam_dir = steam_dir
steam_users = find_steam_users(steam_dir)
if not steam_users:
logger.error("Failed to find any Steam users")
return
else:
steam_user = next(filter(lambda x: x.most_recent, steam_users))
logger.info("Found most recently logged-in user %s(%s)", steam_user.account_name, steam_user.persona_name)
__steam_user = steam_user
__steam_shortcuts = _load_shortcuts(steam_dir, steam_user)
def save_steam_shortcuts():
if __steam_shortcuts:
_save_shortcuts(__steam_dir, __steam_user, __steam_shortcuts)
logger.info("Saved Steam shortcuts for user %s(%s)", __steam_user.account_name, __steam_user.persona_name)
else:
logger.error("Failed to save Steam shortcuts")
def steam_shortcut_exists(app_name: str) -> bool:
return SteamShortcut.calculate_appid(app_name) in {s.appid for s in __steam_shortcuts.values()}
def remove_steam_shortcut(app_name: str) -> Optional[SteamShortcut]:
global __steam_shortcuts
if not steam_shortcut_exists(app_name):
logger.error("Game %s doesn't have an associated Steam shortcut", app_name)
return None
appid = SteamShortcut.calculate_appid(app_name)
removed = next(filter(lambda item: item[1].appid == appid, __steam_shortcuts.items()))
shortcuts = dict(filter(lambda item: item[1].appid != appid, __steam_shortcuts.items()))
__steam_shortcuts = shortcuts
return removed[1]
def add_steam_shortcut(app_name: str, app_title: str) -> SteamShortcut:
global __steam_shortcuts
if steam_shortcut_exists(app_name):
logger.info("Removing old Steam shortcut for %s", app_name)
remove_steam_shortcut(app_name)
command = get_rare_executable()
arguments = ["launch", app_name]
if len(command) > 1:
arguments = command[1:] + arguments
shortcut = SteamShortcut.create(
app_name=app_name,
app_title=f"{app_title} (Rare)",
executable=command[0],
start_dir=os.path.dirname(command[0]),
icon=desktop_icon_path(app_name).as_posix(),
launch_options=arguments,
)
key = int(max(__steam_shortcuts.keys(), default="0"))
__steam_shortcuts[str(key+1)] = shortcut
return shortcut
def add_steam_coverart(app_name: str, shortcut: SteamShortcut):
steam_grid_dir = os.path.join(__steam_dir, "userdata", str(__steam_user.short_id), "config", "grid")
shutil.copy(image_wide_path(app_name), os.path.join(steam_grid_dir, shortcut.game_hero))
shutil.copy(image_icon_path(app_name), os.path.join(steam_grid_dir, shortcut.game_logo))
shutil.copy(image_wide_path(app_name), os.path.join(steam_grid_dir, shortcut.grid_wide))
shutil.copy(image_tall_path(app_name), os.path.join(steam_grid_dir, shortcut.grid_tall))
def remove_steam_coverart(shortcut: SteamShortcut):
steam_grid_dir = os.path.join(__steam_dir, "userdata", str(__steam_user.short_id), "config", "grid")
Path(steam_grid_dir).joinpath(shortcut.game_hero).unlink(missing_ok=True)
Path(steam_grid_dir).joinpath(shortcut.game_logo).unlink(missing_ok=True)
Path(steam_grid_dir).joinpath(shortcut.grid_wide).unlink(missing_ok=True)
Path(steam_grid_dir).joinpath(shortcut.grid_tall).unlink(missing_ok=True)
if __name__ == "__main__":
from pprint import pprint
load_steam_shortcuts()
print(__steam_dir)
print(__steam_user)
print(__steam_shortcuts)
def print_shortcuts():
for k, s in __steam_shortcuts.items():
print({k: asdict(s)})
print(vars(s))
print()
print_shortcuts()
add_steam_shortcut("test1", "Test1")
add_steam_shortcut("test2", "Test2")
add_steam_shortcut("test3", "Test3")
add_steam_shortcut("test1", "Test1")
remove_steam_shortcut("test2")
print_shortcuts()

View file

@ -0,0 +1,42 @@
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QStyle, QPushButton, QLineEdit
from rare.utils.misc import qta_icon
class ButtonLineEdit(QLineEdit):
buttonClicked = pyqtSignal()
def __init__(self, icon_name, placeholder_text: str, parent=None):
super(ButtonLineEdit, self).__init__(parent=parent)
self.setObjectName(type(self).__name__)
self.button = QPushButton(self)
self.button.setObjectName(f"{type(self).__name__}Button")
self.button.setIcon(qta_icon(icon_name))
self.button.setCursor(Qt.ArrowCursor)
self.button.clicked.connect(self.buttonClicked.emit)
self.setPlaceholderText(placeholder_text)
# frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
# button_size = self.button.sizeHint()
#
# self.setStyleSheet(
# f"QLineEdit#{self.objectName()} {{padding-right: {(button_size.width() + frame_width + 1)}px; }}"
# )
# self.setMinimumSize(
# max(self.minimumSizeHint().width(), button_size.width() + frame_width * 2 + 2),
# max(
# self.minimumSizeHint().height(),
# button_size.height() + frame_width * 2 + 2,
# ),
# )
def resizeEvent(self, event):
frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
button_size = self.button.sizeHint()
self.button.move(
self.rect().right() - frame_width - button_size.width(),
(self.rect().bottom() - button_size.height() + 1) // 2,
)
super(ButtonLineEdit, self).resizeEvent(event)

View file

@ -1,6 +1,7 @@
import os
from enum import IntEnum
from logging import getLogger
import shlex
from typing import Callable, Tuple, Optional, Dict, List
from PyQt5.QtCore import (
@ -336,6 +337,8 @@ class PathEdit(IndicatorLineEdit):
if self.__name_filter:
dlg.setNameFilter(" ".join(self.__name_filter))
if dlg.exec_():
names = dlg.selectedFiles()
self.line_edit.setText(names[0])
self.__completer_model.setRootPath(names[0])
name = dlg.selectedFiles()[0]
if " " in name:
name = shlex.quote(name)
self.line_edit.setText(name)
self.__completer_model.setRootPath(name)

View file

@ -14,8 +14,7 @@ class LoadingWidget(QLabel):
self.setMovie(self.movie)
if self.parent() is not None:
self.parent().installEventFilter(self)
if autostart:
self.movie.start()
self.autostart = autostart
def __center_on_parent(self):
rect = self.rect()
@ -34,6 +33,9 @@ class LoadingWidget(QLabel):
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
if self.autostart:
self.movie.start()
self.autostart = False
self.__center_on_parent()
super().showEvent(a0)
@ -45,7 +47,8 @@ class LoadingWidget(QLabel):
def start(self):
self.setVisible(True)
self.movie.start()
if not self.autostart:
self.movie.start()
def stop(self):
self.setVisible(False)

View file

@ -3,4 +3,5 @@ QtAwesome
setuptools
legendary-gl>=0.20.34
# orjson # needs the binary release, use req2flatpak
vdf
pypresence

View file

@ -5,7 +5,7 @@ setuptools
legendary-gl>=0.20.34; platform_system != "Windows" or platform_system != "Darwin"
legendary-gl @ git+https://github.com/derrod/legendary@96e07ff ; platform_system == "Windows" or platform_system == "Darwin"
orjson
vdf; platform_system != "Windows"
vdf
pywin32; platform_system == "Windows"
pywebview[qt]; platform_system == "Linux"
pywebview[qt]; platform_system == "FreeBSD"

View file

@ -5,6 +5,6 @@ setuptools
legendary-gl>=0.20.34; platform_system != "Windows" or platform_system != "Darwin"
legendary-gl @ git+https://github.com/derrod/legendary@96e07ff ; platform_system == "Windows" or platform_system == "Darwin"
orjson
vdf; platform_system == "Linux" or platform_system == "FreeBSD"
vdf
pywin32; platform_system == "Windows"