Compare commits
2 Commits
add-import
...
gamepad-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3df85e9443 | ||
|
|
2d72f22bbf |
19
.github/workflows/flatpak-builder.yml
vendored
Normal file
19
.github/workflows/flatpak-builder.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
name: CI
|
||||||
|
jobs:
|
||||||
|
flatpak:
|
||||||
|
name: "Flatpak"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: bilelmoussaoui/flatpak-github-actions:gnome-44
|
||||||
|
options: --privileged
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
|
||||||
|
with:
|
||||||
|
bundle: hu.kramo.Cartridges.Devel.flatpak
|
||||||
|
manifest-path: flatpak/hu.kramo.Cartridges.Devel.json
|
||||||
|
cache-key: flatpak-builder-${{ github.sha }}
|
||||||
45
.github/workflows/publish-release.yml
vendored
45
.github/workflows/publish-release.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
"*"
|
|
||||||
name: Publish Release
|
|
||||||
concurrency:
|
|
||||||
group: release-${{ github.sha }}
|
|
||||||
jobs:
|
|
||||||
publish-release:
|
|
||||||
name: Publish Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Download workflow artifact
|
|
||||||
uses: dawidd6/action-download-artifact@v2.27.0
|
|
||||||
with:
|
|
||||||
workflow: ci.yml
|
|
||||||
commit: ${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Get release notes
|
|
||||||
shell: python
|
|
||||||
run: |
|
|
||||||
import re, textwrap
|
|
||||||
open_file = open("./data/hu.kramo.Cartridges.metainfo.xml.in", "r", encoding="utf-8")
|
|
||||||
string = open_file.read()
|
|
||||||
open_file.close()
|
|
||||||
string = re.findall("<release.*>\s*<description.*>\n([\s\S]*?)\s*</description>\s*<\/release>", string)[0]
|
|
||||||
string = textwrap.dedent(string)
|
|
||||||
open_file = open("release_notes", "w", encoding="utf-8")
|
|
||||||
open_file.write(string)
|
|
||||||
open_file.close()
|
|
||||||
|
|
||||||
- name: Get tag name
|
|
||||||
id: get_tag_name
|
|
||||||
run: echo tag_name=${GITHUB_REF#refs/tags/} >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Publish release
|
|
||||||
uses: softprops/action-gh-release@v0.1.15
|
|
||||||
with:
|
|
||||||
files: Windows Installer/Cartridges Setup.exe
|
|
||||||
fail_on_unmatched_files: true
|
|
||||||
tag_name: ${{ steps.get_tag_name.outputs.tag_name }}
|
|
||||||
body_path: release_notes
|
|
||||||
@@ -2,40 +2,20 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
name: CI
|
name: "Build for Windows"
|
||||||
concurrency:
|
|
||||||
group: release-${{ github.sha }}
|
|
||||||
jobs:
|
jobs:
|
||||||
flatpak:
|
|
||||||
name: Flatpak
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: bilelmoussaoui/flatpak-github-actions:gnome-44
|
|
||||||
options: --privileged
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Flatpak Builder
|
|
||||||
uses: flatpak/flatpak-github-actions/flatpak-builder@v6.1
|
|
||||||
with:
|
|
||||||
bundle: hu.kramo.Cartridges.Devel.flatpak
|
|
||||||
manifest-path: flatpak/hu.kramo.Cartridges.Devel.json
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
name: Windows
|
name: "Build"
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
- name: MSYS2
|
||||||
- name: Setup MSYS2
|
|
||||||
uses: msys2/setup-msys2@v2
|
uses: msys2/setup-msys2@v2
|
||||||
with:
|
with:
|
||||||
msystem: UCRT64
|
msystem: UCRT64
|
||||||
update: true
|
update: true
|
||||||
install: mingw-w64-ucrt-x86_64-gtk4 mingw-w64-ucrt-x86_64-libadwaita mingw-w64-ucrt-x86_64-python-gobject mingw-w64-ucrt-x86_64-python-yaml mingw-w64-ucrt-x86_64-python-requests mingw-w64-ucrt-x86_64-python-pillow mingw-w64-ucrt-x86_64-desktop-file-utils mingw-w64-ucrt-x86_64-ca-certificates mingw-w64-ucrt-x86_64-meson git
|
install: mingw-w64-ucrt-x86_64-gtk4 mingw-w64-ucrt-x86_64-libadwaita mingw-w64-ucrt-x86_64-python-gobject mingw-w64-ucrt-x86_64-python-yaml mingw-w64-ucrt-x86_64-python-requests mingw-w64-ucrt-x86_64-python-pillow mingw-w64-ucrt-x86_64-desktop-file-utils mingw-w64-ucrt-x86_64-ca-certificates mingw-w64-ucrt-x86_64-meson git
|
||||||
|
|
||||||
- name: Compile
|
- name: Compile
|
||||||
shell: msys2 {0}
|
shell: msys2 {0}
|
||||||
run: |
|
run: |
|
||||||
@@ -43,18 +23,10 @@ jobs:
|
|||||||
ninja -C _build install
|
ninja -C _build install
|
||||||
pacman --noconfirm -Rs mingw-w64-ucrt-x86_64-desktop-file-utils mingw-w64-ucrt-x86_64-meson git
|
pacman --noconfirm -Rs mingw-w64-ucrt-x86_64-desktop-file-utils mingw-w64-ucrt-x86_64-meson git
|
||||||
find /ucrt64/share/locale/ -type f ! -name "*cartridges.mo" -delete
|
find /ucrt64/share/locale/ -type f ! -name "*cartridges.mo" -delete
|
||||||
|
- name: "Inno Setup"
|
||||||
- name: Test
|
|
||||||
shell: msys2 {0}
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
timeout 2 cartridges; [ "$?" -eq "124" ]
|
|
||||||
|
|
||||||
- name: Inno Setup
|
|
||||||
run: iscc ".\_build\Cartridges.iss"
|
run: iscc ".\_build\Cartridges.iss"
|
||||||
|
- name: "Upload Artifact"
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Windows Installer
|
name: "Installer"
|
||||||
path: _build/Output/Cartridges Setup.exe
|
path: "_build/Output/Cartridges Setup.exe"
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
ignore=importers
|
ignore=importers
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
|
||||||
disable=raw-checker-failed,
|
disable=raw-checker-failed,
|
||||||
bad-inline-option,
|
bad-inline-option,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ The project can be translated on [Weblate](https://hosted.weblate.org/engage/car
|
|||||||
|
|
||||||
## For Windows
|
## For Windows
|
||||||
1. Install [MSYS2](https://www.msys2.org/).
|
1. Install [MSYS2](https://www.msys2.org/).
|
||||||
2. From the MSYS2 shell, install the required dependencies listed [here](https://github.com/kra-mo/cartridges/blob/main/.github/workflows/ci.yml).
|
2. From the MSYS2 shell, install the required dependencies listed [here](https://github.com/kra-mo/cartridges/blob/main/.github/workflows/windows.yml).
|
||||||
3. Build it via Meson.
|
3. Build it via Meson.
|
||||||
|
|
||||||
## Meson
|
## Meson
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -10,13 +10,16 @@
|
|||||||
[![Flathub][flathub-image]][flathub-url]
|
[![Flathub][flathub-image]][flathub-url]
|
||||||
[![Build status][github-actions-image]][github-actions-url]
|
[![Build status][github-actions-image]][github-actions-url]
|
||||||
[![Translation Status][weblate-image]][weblate-url]
|
[![Translation Status][weblate-image]][weblate-url]
|
||||||
|
[![License][license-image]][license-url]
|
||||||
[![Code style][code-style-image]][code-style-url]
|
[![Code style][code-style-image]][code-style-url]
|
||||||
[![Discord][discord-image]][discord-url]
|
[![Discord][discord-image]][discord-url]
|
||||||
|
|
||||||
[circle-url]: https://circle.gnome.org
|
[circle-url]: https://circle.gnome.org
|
||||||
[circle-image]: https://circle.gnome.org/assets/button/badge.svg
|
[circle-image]: https://circle.gnome.org/assets/button/badge.svg
|
||||||
[github-actions-url]: https://github.com/kra-mo/cartridges
|
[github-actions-url]: https://github.com/kra-mo/cartridges
|
||||||
[github-actions-image]: https://github.com/kra-mo/cartridges/actions/workflows/ci.yml/badge.svg
|
[github-actions-image]: https://github.com/kra-mo/cartridges/actions/workflows/flatpak-builder.yml/badge.svg
|
||||||
|
[license-url]: https://github.com/kra-mo/cartridges/blob/main/LICENSE
|
||||||
|
[license-image]: https://img.shields.io/github/license/kra-mo/cartridges
|
||||||
[code-style-url]: https://github.com/psf/black
|
[code-style-url]: https://github.com/psf/black
|
||||||
[code-style-image]: https://img.shields.io/badge/code%20style-black-000000?style=flat
|
[code-style-image]: https://img.shields.io/badge/code%20style-black-000000?style=flat
|
||||||
[weblate-url]: https://hosted.weblate.org/engage/cartridges/
|
[weblate-url]: https://hosted.weblate.org/engage/cartridges/
|
||||||
@@ -36,14 +39,8 @@ Cartridges is a simple game launcher written in Python using GTK4 and Libadwaita
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Manually adding and editing games
|
- Manually adding and editing games
|
||||||
- Importing games from various sources:
|
- Importing games from Steam, Lutris, Heroic, Bottles and itch
|
||||||
- Steam
|
- Support for multiple Steam install locations
|
||||||
- Lutris
|
|
||||||
- Heroic
|
|
||||||
- Bottles
|
|
||||||
- itch
|
|
||||||
- Legendary
|
|
||||||
- Flatpak
|
|
||||||
- Hiding games
|
- Hiding games
|
||||||
- Searching and sorting by title, date added and last played
|
- Searching and sorting by title, date added and last played
|
||||||
- Automatically downloading cover art from [SteamGridDB](https://www.steamgriddb.com/)
|
- Automatically downloading cover art from [SteamGridDB](https://www.steamgriddb.com/)
|
||||||
@@ -89,4 +86,4 @@ Thanks to [Weblate](https://weblate.org/) for hosting our translations!
|
|||||||
|
|
||||||
The project follows the [GNOME Code of Conduct](https://wiki.gnome.org/Foundation/CodeOfConduct).
|
The project follows the [GNOME Code of Conduct](https://wiki.gnome.org/Foundation/CodeOfConduct).
|
||||||
|
|
||||||
See [CODE_OF_CONDUCT.md](https://github.com/kra-mo/cartridges/blob/main/CODE_OF_CONDUCT.md).
|
See [CODE_OF_CONDUCT.md](https://github.com/kra-mo/cartridges/blob/main/CODE_OF_CONDUCT.md).
|
||||||
@@ -132,15 +132,6 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
valign: center;
|
valign: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: _("Import Flatpak Games");
|
|
||||||
activatable-widget: lutris_import_flatpak_switch;
|
|
||||||
|
|
||||||
Switch lutris_import_flatpak_switch {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Adw.ExpanderRow heroic_expander_row {
|
Adw.ExpanderRow heroic_expander_row {
|
||||||
@@ -225,29 +216,6 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Adw.ExpanderRow flatpak_expander_row {
|
|
||||||
title: _("Flatpak");
|
|
||||||
show-enable-switch: true;
|
|
||||||
|
|
||||||
Adw.ActionRow flatpak_data_action_row {
|
|
||||||
title: _("Install Location");
|
|
||||||
|
|
||||||
Button flatpak_data_file_chooser_button {
|
|
||||||
icon-name: "folder-symbolic";
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow flatpak_import_launchers_row {
|
|
||||||
title: _("Import Game Launchers");
|
|
||||||
activatable-widget: flatpak_import_launchers_switch;
|
|
||||||
|
|
||||||
Switch flatpak_import_launchers_switch {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,6 @@
|
|||||||
<key name="lutris-import-steam" type="b">
|
<key name="lutris-import-steam" type="b">
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</key>
|
</key>
|
||||||
<key name="lutris-import-flatpak" type="b">
|
|
||||||
<default>false</default>
|
|
||||||
</key>
|
|
||||||
<key name="heroic" type="b">
|
<key name="heroic" type="b">
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
</key>
|
</key>
|
||||||
@@ -64,15 +61,6 @@
|
|||||||
<key name="legendary-location" type="s">
|
<key name="legendary-location" type="s">
|
||||||
<default>"~/.config/legendary/"</default>
|
<default>"~/.config/legendary/"</default>
|
||||||
</key>
|
</key>
|
||||||
<key name="flatpak" type="b">
|
|
||||||
<default>true</default>
|
|
||||||
</key>
|
|
||||||
<key name="flatpak-location" type="s">
|
|
||||||
<default>"/var/lib/flatpak/"</default>
|
|
||||||
</key>
|
|
||||||
<key name="flatpak-import-launchers" type="b">
|
|
||||||
<default>false</default>
|
|
||||||
</key>
|
|
||||||
<key name="sgdb-key" type="s">
|
<key name="sgdb-key" type="s">
|
||||||
<default>""</default>
|
<default>""</default>
|
||||||
</key>
|
</key>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<component type="desktop-application">
|
<component type="desktop-application">
|
||||||
<id>@APP_ID@</id>
|
<id>@APP_ID@.desktop</id>
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0-or-later</project_license>
|
<project_license>GPL-3.0-or-later</project_license>
|
||||||
<name>Cartridges</name>
|
<name>Cartridges</name>
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
"--filesystem=~/.var/app/net.lutris.Lutris/:ro",
|
"--filesystem=~/.var/app/net.lutris.Lutris/:ro",
|
||||||
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
|
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
|
||||||
"--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro",
|
"--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro",
|
||||||
"--filesystem=~/.var/app/io.itch.itch/config/itch/:ro",
|
"--filesystem=~/.var/app/io.itch.itch/config/itch/:ro"
|
||||||
"--filesystem=/var/lib/flatpak:ro"
|
|
||||||
],
|
],
|
||||||
"cleanup" : [
|
"cleanup" : [
|
||||||
"/include",
|
"/include",
|
||||||
@@ -97,9 +96,75 @@
|
|||||||
"sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"
|
"sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name" : "python3-snegg",
|
||||||
|
"buildsystem" : "simple",
|
||||||
|
"build-commands": [
|
||||||
|
"cd snegg",
|
||||||
|
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} . --no-build-isolation"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type" : "git",
|
||||||
|
"url": "https://gitlab.freedesktop.org/libinput/snegg.git",
|
||||||
|
"tag": "main"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "python3-attrs",
|
||||||
|
"buildsystem": "simple",
|
||||||
|
"build-commands": [
|
||||||
|
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"attrs\" --no-build-isolation"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/f0/eb/fcb708c7bf5056045e9e98f62b93bd7467eb718b0202e7698eb11d66416c/attrs-23.1.0-py3-none-any.whl",
|
||||||
|
"sha256": "1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "python3-jinja2",
|
||||||
|
"buildsystem": "simple",
|
||||||
|
"build-commands": [
|
||||||
|
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"jinja2\" --no-build-isolation"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl",
|
||||||
|
"sha256": "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/6d/7c/59a3248f411813f8ccba92a55feaac4bf360d29e2ff05ee7d8e1ef2d7dbf/MarkupSafe-2.1.3.tar.gz",
|
||||||
|
"sha256": "af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name" : "libei",
|
||||||
|
"buildsystem" : "meson",
|
||||||
|
"config-opts": [
|
||||||
|
"-Ddocumentation=[]",
|
||||||
|
"-Dtests=disabled"
|
||||||
|
],
|
||||||
|
"sources" : [
|
||||||
|
{
|
||||||
|
"type" : "git",
|
||||||
|
"url" : "https://gitlab.freedesktop.org/libinput/libei.git",
|
||||||
|
"tag" : "1.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cleanup" : [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name" : "blueprint-compiler",
|
"name" : "blueprint-compiler",
|
||||||
"buildsystem" : "meson",
|
"buildsystem" : "meson",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Cartridges\n"
|
"Project-Id-Version: Cartridges\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-07-01 13:45+0200\n"
|
"POT-Creation-Date: 2023-06-26 22:22+0200\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -19,7 +19,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: data/hu.kramo.Cartridges.desktop.in:3
|
#: data/hu.kramo.Cartridges.desktop.in:3
|
||||||
#: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47
|
#: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47
|
||||||
#: src/main.py:162
|
#: src/main.py:153
|
||||||
msgid "Cartridges"
|
msgid "Cartridges"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ msgid "Game Details"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416
|
#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416
|
||||||
#: src/details_window.py:238
|
#: src/details_window.py:239
|
||||||
msgid "Preferences"
|
msgid "Preferences"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ msgstr ""
|
|||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/game.blp:107 src/window.py:171
|
#: data/gtk/game.blp:107 src/window.py:169
|
||||||
msgid "Hide"
|
msgid "Hide"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ msgstr ""
|
|||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/game.blp:126 src/window.py:173
|
#: data/gtk/game.blp:126 src/window.py:171
|
||||||
msgid "Unhide"
|
msgid "Unhide"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ msgstr ""
|
|||||||
msgid "Shortcuts"
|
msgid "Shortcuts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112
|
#: data/gtk/help-overlay.blp:34 src/game.py:105 src/preferences.py:103
|
||||||
msgid "Undo"
|
msgid "Undo"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ msgstr ""
|
|||||||
msgid "Remove game"
|
msgid "Remove game"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268
|
#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:236
|
||||||
msgid "Behavior"
|
msgid "Behavior"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -217,9 +217,8 @@ msgid "Steam"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110
|
#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110
|
||||||
#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192
|
#: data/gtk/preferences.blp:142 data/gtk/preferences.blp:183
|
||||||
#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220
|
#: data/gtk/preferences.blp:197 data/gtk/preferences.blp:211
|
||||||
#: data/gtk/preferences.blp:234
|
|
||||||
msgid "Install Location"
|
msgid "Install Location"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -235,71 +234,59 @@ msgstr ""
|
|||||||
msgid "Import Steam Games"
|
msgid "Import Steam Games"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:137
|
#: data/gtk/preferences.blp:138
|
||||||
msgid "Import Flatpak Games"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:147
|
|
||||||
msgid "Heroic"
|
msgid "Heroic"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:160
|
#: data/gtk/preferences.blp:151
|
||||||
msgid "Import Epic Games"
|
msgid "Import Epic Games"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:169
|
#: data/gtk/preferences.blp:160
|
||||||
msgid "Import GOG Games"
|
msgid "Import GOG Games"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:178
|
#: data/gtk/preferences.blp:169
|
||||||
msgid "Import Sideloaded Games"
|
msgid "Import Sideloaded Games"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:188
|
#: data/gtk/preferences.blp:179
|
||||||
msgid "Bottles"
|
msgid "Bottles"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:202
|
#: data/gtk/preferences.blp:193
|
||||||
msgid "itch"
|
msgid "itch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:216
|
#: data/gtk/preferences.blp:207
|
||||||
msgid "Legendary"
|
msgid "Legendary"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:230
|
#: data/gtk/preferences.blp:224
|
||||||
msgid "Flatpak"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:243
|
|
||||||
msgid "Import Game Launchers"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:256
|
|
||||||
msgid "SteamGridDB"
|
msgid "SteamGridDB"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:260
|
#: data/gtk/preferences.blp:228
|
||||||
msgid "Authentication"
|
msgid "Authentication"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:263
|
#: data/gtk/preferences.blp:231
|
||||||
msgid "API Key"
|
msgid "API Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:271
|
#: data/gtk/preferences.blp:239
|
||||||
msgid "Use SteamGridDB"
|
msgid "Use SteamGridDB"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:272
|
#: data/gtk/preferences.blp:240
|
||||||
msgid "Download images when adding or importing games"
|
msgid "Download images when adding or importing games"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:281
|
#: data/gtk/preferences.blp:249
|
||||||
msgid "Prefer Over Official Images"
|
msgid "Prefer Over Official Images"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: data/gtk/preferences.blp:290
|
#: data/gtk/preferences.blp:258
|
||||||
msgid "Prefer Animated Images"
|
msgid "Prefer Animated Images"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -388,21 +375,21 @@ msgid "About Cartridges"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Translators: Replace this with your name for it to show up in the about window
|
#. Translators: Replace this with your name for it to show up in the about window
|
||||||
#: src/main.py:180
|
#: src/main.py:171
|
||||||
msgid "translator_credits"
|
msgid "translator_credits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. The variable is the date when the game was added
|
#. The variable is the date when the game was added
|
||||||
#: src/window.py:194
|
#: src/window.py:192
|
||||||
msgid "Added: {}"
|
msgid "Added: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/window.py:197
|
#: src/window.py:195
|
||||||
msgid "Never"
|
msgid "Never"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. The variable is the date when the game was last played
|
#. The variable is the date when the game was last played
|
||||||
#: src/window.py:201
|
#: src/window.py:199
|
||||||
msgid "Last played: {}"
|
msgid "Last played: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -455,68 +442,46 @@ msgstr ""
|
|||||||
msgid "Couldn't Add Game"
|
msgid "Couldn't Add Game"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/details_window.py:146 src/details_window.py:180
|
#: src/details_window.py:146 src/details_window.py:181
|
||||||
msgid "Game title cannot be empty."
|
msgid "Game title cannot be empty."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/details_window.py:152 src/details_window.py:188
|
#: src/details_window.py:152 src/details_window.py:189
|
||||||
msgid "Executable cannot be empty."
|
msgid "Executable cannot be empty."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/details_window.py:179 src/details_window.py:187
|
#: src/details_window.py:180 src/details_window.py:188
|
||||||
msgid "Couldn't Apply Preferences"
|
msgid "Couldn't Apply Preferences"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. The variable is the title of the game
|
#. The variable is the title of the game
|
||||||
#: src/game.py:138
|
#: src/game.py:141
|
||||||
msgid "{} launched"
|
msgid "{} launched"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. The variable is the title of the game
|
#. The variable is the title of the game
|
||||||
#: src/game.py:151
|
#: src/game.py:154
|
||||||
msgid "{} hidden"
|
msgid "{} hidden"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/game.py:151
|
#: src/game.py:154
|
||||||
msgid "{} unhidden"
|
msgid "{} unhidden"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/game.py:168
|
#: src/game.py:171
|
||||||
msgid "{} removed"
|
msgid "{} removed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/preferences.py:111
|
#: src/preferences.py:102
|
||||||
msgid "All games removed"
|
msgid "All games removed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/preferences.py:159
|
#: src/preferences.py:149
|
||||||
msgid ""
|
msgid ""
|
||||||
"An API key is required to use SteamGridDB. You can generate one {}here{}."
|
"An API key is required to use SteamGridDB. You can generate one {}here{}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/preferences.py:284
|
#: src/preferences.py:289
|
||||||
msgid "Installation Not Found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/preferences.py:286
|
|
||||||
msgid "Select a valid directory."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/preferences.py:349 src/preferences.py:353
|
|
||||||
msgid "Invalid Directory"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. The variable is the name of the source
|
|
||||||
#: src/preferences.py:351
|
|
||||||
msgid "Select the {} cache directory."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. The variable is the name of the source
|
|
||||||
#: src/preferences.py:355
|
|
||||||
msgid "Select the {} installation directory."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/preferences.py:361
|
|
||||||
msgid "Set Location"
|
msgid "Set Location"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class DetailsWindow(Adw.Window):
|
|||||||
self.exec_info_label.set_label(exec_info_text)
|
self.exec_info_label.set_label(exec_info_text)
|
||||||
|
|
||||||
def clear_info_selection(*_args):
|
def clear_info_selection(*_args):
|
||||||
self.exec_info_label.select_region(-1, -1)
|
self.exec_info_label.select_region(0, 0)
|
||||||
|
|
||||||
self.exec_info_popover.connect("show", clear_info_selection)
|
self.exec_info_popover.connect("show", clear_info_selection)
|
||||||
|
|
||||||
@@ -169,7 +169,8 @@ class DetailsWindow(Adw.Window):
|
|||||||
"hidden": False,
|
"hidden": False,
|
||||||
"source": "imported",
|
"source": "imported",
|
||||||
"added": int(time()),
|
"added": int(time()),
|
||||||
}
|
},
|
||||||
|
allow_side_effects=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Game(Gtk.Box):
|
|||||||
game_cover = None
|
game_cover = None
|
||||||
version = 0
|
version = 0
|
||||||
|
|
||||||
def __init__(self, data, **kwargs):
|
def __init__(self, data, allow_side_effects=True, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.win = shared.win
|
self.win = shared.win
|
||||||
@@ -70,6 +70,9 @@ class Game(Gtk.Box):
|
|||||||
|
|
||||||
self.update_values(data)
|
self.update_values(data)
|
||||||
|
|
||||||
|
if allow_side_effects:
|
||||||
|
self.win.games[self.game_id] = self
|
||||||
|
|
||||||
self.set_play_icon()
|
self.set_play_icon()
|
||||||
|
|
||||||
self.event_contoller_motion = Gtk.EventControllerMotion.new()
|
self.event_contoller_motion = Gtk.EventControllerMotion.new()
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ class GameCover:
|
|||||||
stat = ImageStat.Stat(image.convert("L"))
|
stat = ImageStat.Stat(image.convert("L"))
|
||||||
|
|
||||||
# Luminance values for light and dark mode
|
# Luminance values for light and dark mode
|
||||||
self.luminance = [
|
self.luminance = (
|
||||||
min((stat.mean[0] + stat.extrema[0][0]) / 510, 0.7),
|
(stat.mean[0] + stat.extrema[0][0]) / 510,
|
||||||
max((stat.mean[0] + stat.extrema[0][1]) / 510, 0.3),
|
(stat.mean[0] + stat.extrema[0][1]) / 510,
|
||||||
]
|
)
|
||||||
else:
|
else:
|
||||||
self.blurred = self.placeholder_small
|
self.blurred = self.placeholder_small
|
||||||
self.luminance = (0.3, 0.5)
|
self.luminance = (0.3, 0.5)
|
||||||
|
|||||||
@@ -18,9 +18,7 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from shutil import copytree, rmtree
|
|
||||||
|
|
||||||
from gi.repository import Adw, GLib, Gtk
|
from gi.repository import Adw, GLib, Gtk
|
||||||
|
|
||||||
@@ -28,15 +26,7 @@ from src import shared
|
|||||||
from src.errors.error_producer import ErrorProducer
|
from src.errors.error_producer import ErrorProducer
|
||||||
from src.errors.friendly_error import FriendlyError
|
from src.errors.friendly_error import FriendlyError
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.importer.sources.bottles_source import BottlesSource
|
|
||||||
from src.importer.sources.flatpak_source import FlatpakSource
|
|
||||||
from src.importer.sources.heroic_source import HeroicSource
|
|
||||||
from src.importer.sources.itch_source import ItchSource
|
|
||||||
from src.importer.sources.legendary_source import LegendarySource
|
|
||||||
from src.importer.sources.location import UnresolvableLocationError
|
|
||||||
from src.importer.sources.lutris_source import LutrisSource
|
|
||||||
from src.importer.sources.source import Source
|
from src.importer.sources.source import Source
|
||||||
from src.importer.sources.steam_source import SteamSource
|
|
||||||
from src.store.managers.async_manager import AsyncManager
|
from src.store.managers.async_manager import AsyncManager
|
||||||
from src.store.pipeline import Pipeline
|
from src.store.pipeline import Pipeline
|
||||||
from src.utils.task import Task
|
from src.utils.task import Task
|
||||||
@@ -62,20 +52,6 @@ class Importer(ErrorProducer):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.game_pipelines = set()
|
self.game_pipelines = set()
|
||||||
self.sources = set()
|
self.sources = set()
|
||||||
if shared.schema.get_boolean("lutris"):
|
|
||||||
self.sources.add(LutrisSource())
|
|
||||||
if shared.schema.get_boolean("steam"):
|
|
||||||
self.sources.add(SteamSource())
|
|
||||||
if shared.schema.get_boolean("heroic"):
|
|
||||||
self.sources.add(HeroicSource())
|
|
||||||
if shared.schema.get_boolean("bottles"):
|
|
||||||
self.sources.add(BottlesSource())
|
|
||||||
if shared.schema.get_boolean("flatpak"):
|
|
||||||
self.sources.add(FlatpakSource())
|
|
||||||
if shared.schema.get_boolean("itch"):
|
|
||||||
self.sources.add(ItchSource())
|
|
||||||
if shared.schema.get_boolean("legendary"):
|
|
||||||
self.sources.add(LegendarySource())
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def n_games_added(self):
|
def n_games_added(self):
|
||||||
@@ -90,15 +66,7 @@ class Importer(ErrorProducer):
|
|||||||
try:
|
try:
|
||||||
progress = progress / len(self.game_pipelines)
|
progress = progress / len(self.game_pipelines)
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
progress = 0
|
progress = 1
|
||||||
return progress
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sources_progress(self):
|
|
||||||
try:
|
|
||||||
progress = self.n_source_tasks_done / self.n_source_tasks_created
|
|
||||||
except ZeroDivisionError:
|
|
||||||
progress = 0
|
|
||||||
return progress
|
return progress
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -108,47 +76,12 @@ class Importer(ErrorProducer):
|
|||||||
and len(self.game_pipelines) == self.n_pipelines_done
|
and len(self.game_pipelines) == self.n_pipelines_done
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_games_from_disk(self):
|
def add_source(self, source):
|
||||||
"""Load the games from disk"""
|
self.sources.add(source)
|
||||||
if shared.games_dir.is_dir():
|
|
||||||
for game_file in shared.games_dir.iterdir():
|
|
||||||
data = json.load(game_file.open())
|
|
||||||
game = Game(data)
|
|
||||||
shared.store.add_game(game, {"skip_save": True})
|
|
||||||
game.update()
|
|
||||||
|
|
||||||
def delete_backup(self):
|
|
||||||
"""Delete a a previously made backup"""
|
|
||||||
rmtree(shared.backup_games_dir, ignore_errors=True)
|
|
||||||
rmtree(shared.backup_covers_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def create_backup(self):
|
|
||||||
"""Make a games and covers backup"""
|
|
||||||
self.delete_backup()
|
|
||||||
copytree(shared.games_dir, shared.backup_games_dir)
|
|
||||||
copytree(shared.covers_dir, shared.backup_covers_dir)
|
|
||||||
|
|
||||||
def restore_backup(self):
|
|
||||||
"""Restore a previously made backup"""
|
|
||||||
|
|
||||||
# Remove games from the store and UI
|
|
||||||
for game in shared.store.games.values():
|
|
||||||
game.update_values({"removed": True})
|
|
||||||
game.update()
|
|
||||||
|
|
||||||
# Restore the disk backup
|
|
||||||
rmtree(shared.games_dir, ignore_errors=True)
|
|
||||||
rmtree(shared.covers_dir, ignore_errors=True)
|
|
||||||
shared.backup_games_dir.rename(shared.games_dir)
|
|
||||||
shared.backup_covers_dir.rename(shared.covers_dir)
|
|
||||||
|
|
||||||
# Reload games from disk
|
|
||||||
self.load_games_from_disk()
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Use several Gio.Task to import games from added sources"""
|
"""Use several Gio.Task to import games from added sources"""
|
||||||
|
|
||||||
self.create_backup()
|
|
||||||
self.create_dialog()
|
self.create_dialog()
|
||||||
|
|
||||||
# Collect all errors and reset the cancellables for the managers
|
# Collect all errors and reset the cancellables for the managers
|
||||||
@@ -192,18 +125,16 @@ class Importer(ErrorProducer):
|
|||||||
source: Source
|
source: Source
|
||||||
source, *_rest = data
|
source, *_rest = data
|
||||||
|
|
||||||
# Early exit if not available or not installed
|
# Early exit if not installed
|
||||||
if not source.is_available:
|
if not source.is_available:
|
||||||
logging.info("Source %s skipped, not available", source.id)
|
logging.info("Source %s skipped, not installed", source.id)
|
||||||
return
|
|
||||||
try:
|
|
||||||
iterator = iter(source)
|
|
||||||
except UnresolvableLocationError:
|
|
||||||
logging.info("Source %s skipped, bad location", source.id)
|
|
||||||
return
|
return
|
||||||
|
logging.info("Scanning source %s", source.id)
|
||||||
|
|
||||||
|
# Initialize source iteration
|
||||||
|
iterator = iter(source)
|
||||||
|
|
||||||
# Get games from source
|
# Get games from source
|
||||||
logging.info("Scanning source %s", source.id)
|
|
||||||
while True:
|
while True:
|
||||||
# Handle exceptions raised when iterating
|
# Handle exceptions raised when iterating
|
||||||
try:
|
try:
|
||||||
@@ -241,11 +172,8 @@ class Importer(ErrorProducer):
|
|||||||
self.game_pipelines.add(pipeline)
|
self.game_pipelines.add(pipeline)
|
||||||
|
|
||||||
def update_progressbar(self):
|
def update_progressbar(self):
|
||||||
"""Update the progressbar to show the overall import progress"""
|
"""Update the progressbar to show the percentage of game pipelines done"""
|
||||||
# Reserve 10% for the sources discovery, the rest is the pipelines
|
self.progressbar.set_fraction(self.pipelines_progress)
|
||||||
self.progressbar.set_fraction(
|
|
||||||
(0.1 * self.sources_progress) + (0.9 * self.pipelines_progress)
|
|
||||||
)
|
|
||||||
|
|
||||||
def source_callback(self, _obj, _result, data):
|
def source_callback(self, _obj, _result, data):
|
||||||
"""Callback executed when a source is fully scanned"""
|
"""Callback executed when a source is fully scanned"""
|
||||||
@@ -346,17 +274,13 @@ class Importer(ErrorProducer):
|
|||||||
"open_preferences",
|
"open_preferences",
|
||||||
"import",
|
"import",
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
toast.set_button_label(_("Undo"))
|
elif self.n_games_added == 1:
|
||||||
toast.connect(
|
toast.set_title(_("1 game imported"))
|
||||||
"button-clicked", self.dialog_response_callback, "undo_import"
|
|
||||||
)
|
elif self.n_games_added > 1:
|
||||||
toast.set_title(
|
# The variable is the number of games
|
||||||
_("1 game imported")
|
toast.set_title(_("{} games imported").format(self.n_games_added))
|
||||||
if self.n_games_added == 1
|
|
||||||
# The variable is the number of games
|
|
||||||
else _("{} games imported").format(self.n_games_added)
|
|
||||||
)
|
|
||||||
|
|
||||||
shared.win.toast_overlay.add_toast(toast)
|
shared.win.toast_overlay.add_toast(toast)
|
||||||
return toast
|
return toast
|
||||||
@@ -377,7 +301,5 @@ class Importer(ErrorProducer):
|
|||||||
self.open_preferences(*args)
|
self.open_preferences(*args)
|
||||||
elif response == "open_preferences_import":
|
elif response == "open_preferences_import":
|
||||||
self.open_preferences(*args).connect("close-request", self.timeout_toast)
|
self.open_preferences(*args).connect("close-request", self.timeout_toast)
|
||||||
elif response == "undo_import":
|
|
||||||
self.restore_backup()
|
|
||||||
else:
|
else:
|
||||||
self.timeout_toast()
|
self.timeout_toast()
|
||||||
|
|||||||
@@ -41,20 +41,20 @@ class BottlesSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
data = self.source.data_location["library.yml"].read_text("utf-8")
|
data = self.source.data_location["library.yml"].read_text("utf-8")
|
||||||
library: dict = yaml.safe_load(data)
|
library: dict = yaml.safe_load(data)
|
||||||
added_time = int(time())
|
|
||||||
|
|
||||||
for entry in library.values():
|
for entry in library.values():
|
||||||
# Build game
|
# Build game
|
||||||
values = {
|
values = {
|
||||||
|
"version": shared.SPEC_VERSION,
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"added": added_time,
|
"added": int(time()),
|
||||||
"name": entry["name"],
|
"name": entry["name"],
|
||||||
"game_id": self.source.game_id_format.format(game_id=entry["id"]),
|
"game_id": self.source.game_id_format.format(game_id=entry["id"]),
|
||||||
"executable": self.source.executable_format.format(
|
"executable": self.source.executable_format.format(
|
||||||
bottle_name=entry["bottle"]["name"], game_name=entry["name"]
|
bottle_name=entry["bottle"]["name"], game_name=entry["name"]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
game = Game(values)
|
game = Game(values, allow_side_effects=False)
|
||||||
|
|
||||||
# Get official cover path
|
# Get official cover path
|
||||||
try:
|
try:
|
||||||
@@ -86,14 +86,13 @@ class BottlesSource(URLExecutableSource):
|
|||||||
name = "Bottles"
|
name = "Bottles"
|
||||||
iterator_class = BottlesSourceIterator
|
iterator_class = BottlesSourceIterator
|
||||||
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
|
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
|
||||||
available_on = {"linux"}
|
available_on = set(("linux",))
|
||||||
|
|
||||||
data_location = Location(
|
data_location = Location(
|
||||||
schema_key="bottles-location",
|
schema_key="bottles-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles",
|
"~/.var/app/com.usebottles.bottles/data/bottles/",
|
||||||
shared.data_dir / "bottles/",
|
shared.data_dir / "bottles/",
|
||||||
shared.home / ".local" / "share" / "bottles",
|
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
"library.yml": (False, "library.yml"),
|
"library.yml": (False, "library.yml"),
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
# flatpak_source.py
|
|
||||||
#
|
|
||||||
# Copyright 2022-2023 kramo
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from time import time
|
|
||||||
|
|
||||||
from gi.repository import GLib, Gtk
|
|
||||||
|
|
||||||
from src import shared
|
|
||||||
from src.game import Game
|
|
||||||
from src.importer.sources.location import Location
|
|
||||||
from src.importer.sources.source import Source, SourceIterationResult, SourceIterator
|
|
||||||
|
|
||||||
|
|
||||||
class FlatpakSourceIterator(SourceIterator):
|
|
||||||
source: "FlatpakSource"
|
|
||||||
|
|
||||||
def generator_builder(self) -> SourceIterationResult:
|
|
||||||
"""Generator method producing games"""
|
|
||||||
|
|
||||||
added_time = int(time())
|
|
||||||
|
|
||||||
icon_theme = Gtk.IconTheme.new()
|
|
||||||
icon_theme.add_search_path(str(self.source.data_location["icons"]))
|
|
||||||
|
|
||||||
blacklist = (
|
|
||||||
{"hu.kramo.Cartridges"}
|
|
||||||
if shared.schema.get_boolean("flatpak-import-launchers")
|
|
||||||
else {
|
|
||||||
"hu.kramo.Cartridges",
|
|
||||||
"com.valvesoftware.Steam",
|
|
||||||
"net.lutris.Lutris",
|
|
||||||
"com.heroicgameslauncher.hgl",
|
|
||||||
"com.usebottles.Bottles",
|
|
||||||
"io.itch.itch",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
for entry in (self.source.data_location["applications"]).iterdir():
|
|
||||||
if entry.suffix != ".desktop":
|
|
||||||
continue
|
|
||||||
|
|
||||||
keyfile = GLib.KeyFile.new()
|
|
||||||
|
|
||||||
try:
|
|
||||||
keyfile.load_from_file(str(entry), 0)
|
|
||||||
|
|
||||||
if "Game" not in keyfile.get_string_list("Desktop Entry", "Categories"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (
|
|
||||||
flatpak_id := keyfile.get_string("Desktop Entry", "X-Flatpak")
|
|
||||||
) in blacklist or flatpak_id != entry.stem:
|
|
||||||
continue
|
|
||||||
|
|
||||||
name = keyfile.get_string("Desktop Entry", "Name")
|
|
||||||
|
|
||||||
except GLib.GError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
values = {
|
|
||||||
"source": self.source.id,
|
|
||||||
"added": added_time,
|
|
||||||
"name": name,
|
|
||||||
"game_id": self.source.game_id_format.format(game_id=flatpak_id),
|
|
||||||
"executable": self.source.executable_format.format(
|
|
||||||
flatpak_id=flatpak_id
|
|
||||||
),
|
|
||||||
}
|
|
||||||
game = Game(values)
|
|
||||||
|
|
||||||
additional_data = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
if (
|
|
||||||
icon_path := icon_theme.lookup_icon(
|
|
||||||
keyfile.get_string("Desktop Entry", "Icon"),
|
|
||||||
None,
|
|
||||||
512,
|
|
||||||
1,
|
|
||||||
shared.win.get_direction(),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
.get_file()
|
|
||||||
.get_path()
|
|
||||||
):
|
|
||||||
additional_data = {"local_icon_path": Path(icon_path)}
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
except GLib.GError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Produce game
|
|
||||||
yield (game, additional_data)
|
|
||||||
|
|
||||||
|
|
||||||
class FlatpakSource(Source):
|
|
||||||
"""Generic Flatpak source"""
|
|
||||||
|
|
||||||
name = "Flatpak"
|
|
||||||
iterator_class = FlatpakSourceIterator
|
|
||||||
executable_format = "flatpak run {flatpak_id}"
|
|
||||||
available_on = {"linux"}
|
|
||||||
|
|
||||||
data_location = Location(
|
|
||||||
schema_key="flatpak-location",
|
|
||||||
candidates=(
|
|
||||||
"/var/lib/flatpak/",
|
|
||||||
shared.data_dir / "flatpak",
|
|
||||||
),
|
|
||||||
paths={
|
|
||||||
"applications": (True, "exports/share/applications"),
|
|
||||||
"icons": (True, "exports/share/icons"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -68,7 +68,7 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def game_from_library_entry(
|
def game_from_library_entry(
|
||||||
self, entry: HeroicLibraryEntry, added_time: int
|
self, entry: HeroicLibraryEntry
|
||||||
) -> SourceIterationResult:
|
) -> SourceIterationResult:
|
||||||
"""Helper method used to build a Game from a Heroic library entry"""
|
"""Helper method used to build a Game from a Heroic library entry"""
|
||||||
|
|
||||||
@@ -81,8 +81,9 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
runner = entry["runner"]
|
runner = entry["runner"]
|
||||||
service = self.sub_sources[runner]["service"]
|
service = self.sub_sources[runner]["service"]
|
||||||
values = {
|
values = {
|
||||||
|
"version": shared.SPEC_VERSION,
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"added": added_time,
|
"added": int(time()),
|
||||||
"name": entry["title"],
|
"name": entry["title"],
|
||||||
"developer": entry["developer"],
|
"developer": entry["developer"],
|
||||||
"game_id": self.source.game_id_format.format(
|
"game_id": self.source.game_id_format.format(
|
||||||
@@ -90,7 +91,7 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
),
|
),
|
||||||
"executable": self.source.executable_format.format(app_name=app_name),
|
"executable": self.source.executable_format.format(app_name=app_name),
|
||||||
}
|
}
|
||||||
game = Game(values)
|
game = Game(values, allow_side_effects=False)
|
||||||
|
|
||||||
# Get the image path from the heroic cache
|
# Get the image path from the heroic cache
|
||||||
# Filenames are derived from the URL that heroic used to get the file
|
# Filenames are derived from the URL that heroic used to get the file
|
||||||
@@ -118,12 +119,9 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
# Invalid library.json file, skip it
|
# Invalid library.json file, skip it
|
||||||
logging.warning("Couldn't open Heroic file: %s", str(file))
|
logging.warning("Couldn't open Heroic file: %s", str(file))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
added_time = int(time())
|
|
||||||
|
|
||||||
for entry in library:
|
for entry in library:
|
||||||
try:
|
try:
|
||||||
result = self.game_from_library_entry(entry, added_time)
|
result = self.game_from_library_entry(entry)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# Skip invalid games
|
# Skip invalid games
|
||||||
logging.warning("Invalid Heroic game skipped in %s", str(file))
|
logging.warning("Invalid Heroic game skipped in %s", str(file))
|
||||||
@@ -132,20 +130,20 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
|
|
||||||
class HeroicSource(URLExecutableSource):
|
class HeroicSource(URLExecutableSource):
|
||||||
"""Generic Heroic Games Launcher source"""
|
"""Generic heroic games launcher source"""
|
||||||
|
|
||||||
name = "Heroic"
|
name = "Heroic"
|
||||||
iterator_class = HeroicSourceIterator
|
iterator_class = HeroicSourceIterator
|
||||||
url_format = "heroic://launch/{app_name}"
|
url_format = "heroic://launch/{app_name}"
|
||||||
available_on = {"linux", "win32"}
|
available_on = set(("linux", "win32"))
|
||||||
|
|
||||||
config_location = Location(
|
config_location = Location(
|
||||||
schema_key="heroic-location",
|
schema_key="heroic-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic",
|
"~/.var/app/com.heroicgameslauncher.hgl/config/heroic/",
|
||||||
shared.config_dir / "heroic",
|
shared.config_dir / "heroic/",
|
||||||
shared.home / ".config" / "heroic",
|
"~/.config/heroic/",
|
||||||
shared.appdata_dir / "heroic",
|
shared.appdata_dir / "heroic/",
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
"config.json": (False, "config.json"),
|
"config.json": (False, "config.json"),
|
||||||
|
|||||||
@@ -59,19 +59,18 @@ class ItchSourceIterator(SourceIterator):
|
|||||||
connection = connect(db_path)
|
connection = connect(db_path)
|
||||||
cursor = connection.execute(db_request)
|
cursor = connection.execute(db_request)
|
||||||
|
|
||||||
added_time = int(time())
|
|
||||||
|
|
||||||
# Create games from the db results
|
# Create games from the db results
|
||||||
for row in cursor:
|
for row in cursor:
|
||||||
values = {
|
values = {
|
||||||
"added": added_time,
|
"version": shared.SPEC_VERSION,
|
||||||
|
"added": int(time()),
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"name": row[1],
|
"name": row[1],
|
||||||
"game_id": self.source.game_id_format.format(game_id=row[0]),
|
"game_id": self.source.game_id_format.format(game_id=row[0]),
|
||||||
"executable": self.source.executable_format.format(cave_id=row[4]),
|
"executable": self.source.executable_format.format(cave_id=row[4]),
|
||||||
}
|
}
|
||||||
additional_data = {"online_cover_url": row[3] or row[2]}
|
additional_data = {"online_cover_url": row[3] or row[2]}
|
||||||
game = Game(values)
|
game = Game(values, allow_side_effects=False)
|
||||||
yield (game, additional_data)
|
yield (game, additional_data)
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
@@ -82,15 +81,15 @@ class ItchSource(URLExecutableSource):
|
|||||||
name = "Itch"
|
name = "Itch"
|
||||||
iterator_class = ItchSourceIterator
|
iterator_class = ItchSourceIterator
|
||||||
url_format = "itch://caves/{cave_id}/launch"
|
url_format = "itch://caves/{cave_id}/launch"
|
||||||
available_on = {"linux", "win32"}
|
available_on = set(("linux", "win32"))
|
||||||
|
|
||||||
config_location = Location(
|
config_location = Location(
|
||||||
schema_key="itch-location",
|
schema_key="itch-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
shared.flatpak_dir / "io.itch.itch" / "config" / "itch",
|
"~/.var/app/io.itch.itch/config/itch/",
|
||||||
shared.config_dir / "itch",
|
shared.config_dir / "itch/",
|
||||||
shared.home / ".config" / "itch",
|
"~/.config/itch/",
|
||||||
shared.appdata_dir / "itch",
|
shared.appdata_dir / "itch/",
|
||||||
),
|
),
|
||||||
paths={"butler.db": (False, "db/butler.db")},
|
paths={"butler.db": (False, "db/butler.db")},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ from src.importer.sources.source import Source, SourceIterationResult, SourceIte
|
|||||||
class LegendarySourceIterator(SourceIterator):
|
class LegendarySourceIterator(SourceIterator):
|
||||||
source: "LegendarySource"
|
source: "LegendarySource"
|
||||||
|
|
||||||
def game_from_library_entry(
|
def game_from_library_entry(self, entry: dict) -> SourceIterationResult:
|
||||||
self, entry: dict, added_time: int
|
|
||||||
) -> SourceIterationResult:
|
|
||||||
# Skip non-games
|
# Skip non-games
|
||||||
if entry["is_dlc"]:
|
if entry["is_dlc"]:
|
||||||
return None
|
return None
|
||||||
@@ -42,7 +40,8 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
# Build game
|
# Build game
|
||||||
app_name = entry["app_name"]
|
app_name = entry["app_name"]
|
||||||
values = {
|
values = {
|
||||||
"added": added_time,
|
"version": shared.SPEC_VERSION,
|
||||||
|
"added": int(time()),
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"name": entry["title"],
|
"name": entry["title"],
|
||||||
"game_id": self.source.game_id_format.format(game_id=app_name),
|
"game_id": self.source.game_id_format.format(game_id=app_name),
|
||||||
@@ -62,7 +61,7 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
except (JSONDecodeError, OSError, KeyError):
|
except (JSONDecodeError, OSError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
game = Game(values)
|
game = Game(values, allow_side_effects=False)
|
||||||
return (game, data)
|
return (game, data)
|
||||||
|
|
||||||
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
||||||
@@ -73,13 +72,10 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
except (JSONDecodeError, OSError):
|
except (JSONDecodeError, OSError):
|
||||||
logging.warning("Couldn't open Legendary file: %s", str(file))
|
logging.warning("Couldn't open Legendary file: %s", str(file))
|
||||||
return
|
return
|
||||||
|
|
||||||
added_time = int(time())
|
|
||||||
|
|
||||||
# Generate games from library
|
# Generate games from library
|
||||||
for entry in library.values():
|
for entry in library.values():
|
||||||
try:
|
try:
|
||||||
result = self.game_from_library_entry(entry, added_time)
|
result = self.game_from_library_entry(entry)
|
||||||
except KeyError as error:
|
except KeyError as error:
|
||||||
# Skip invalid games
|
# Skip invalid games
|
||||||
logging.warning(
|
logging.warning(
|
||||||
@@ -92,14 +88,14 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
class LegendarySource(Source):
|
class LegendarySource(Source):
|
||||||
name = "Legendary"
|
name = "Legendary"
|
||||||
executable_format = "legendary launch {app_name}"
|
executable_format = "legendary launch {app_name}"
|
||||||
available_on = {"linux", "win32"}
|
available_on = set(("linux", "win32"))
|
||||||
|
|
||||||
iterator_class = LegendarySourceIterator
|
iterator_class = LegendarySourceIterator
|
||||||
config_location: Location = Location(
|
data_location: Location = Location(
|
||||||
schema_key="legendary-location",
|
schema_key="legendary-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
shared.config_dir / "legendary",
|
shared.config_dir / "legendary/",
|
||||||
shared.home / ".config" / "legendary",
|
"~/.config/legendary",
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
"installed.json": (False, "installed.json"),
|
"installed.json": (False, "installed.json"),
|
||||||
|
|||||||
@@ -48,24 +48,19 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
AND configPath IS NOT NULL
|
AND configPath IS NOT NULL
|
||||||
AND installed
|
AND installed
|
||||||
AND (runner IS NOT "steam" OR :import_steam)
|
AND (runner IS NOT "steam" OR :import_steam)
|
||||||
AND (runner IS NOT "flatpak" OR :import_flatpak)
|
|
||||||
;
|
;
|
||||||
"""
|
"""
|
||||||
params = {
|
params = {"import_steam": shared.schema.get_boolean("lutris-import-steam")}
|
||||||
"import_steam": shared.schema.get_boolean("lutris-import-steam"),
|
|
||||||
"import_flatpak": shared.schema.get_boolean("lutris-import-flatpak"),
|
|
||||||
}
|
|
||||||
db_path = copy_db(self.source.data_location["pga.db"])
|
db_path = copy_db(self.source.data_location["pga.db"])
|
||||||
connection = connect(db_path)
|
connection = connect(db_path)
|
||||||
cursor = connection.execute(request, params)
|
cursor = connection.execute(request, params)
|
||||||
|
|
||||||
added_time = int(time())
|
|
||||||
|
|
||||||
# Create games from the DB results
|
# Create games from the DB results
|
||||||
for row in cursor:
|
for row in cursor:
|
||||||
# Create game
|
# Create game
|
||||||
values = {
|
values = {
|
||||||
"added": added_time,
|
"version": shared.SPEC_VERSION,
|
||||||
|
"added": int(time()),
|
||||||
"hidden": row[4],
|
"hidden": row[4],
|
||||||
"name": row[1],
|
"name": row[1],
|
||||||
"source": f"{self.source.id}_{row[3]}",
|
"source": f"{self.source.id}_{row[3]}",
|
||||||
@@ -74,7 +69,7 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
),
|
),
|
||||||
"executable": self.source.executable_format.format(game_id=row[2]),
|
"executable": self.source.executable_format.format(game_id=row[2]),
|
||||||
}
|
}
|
||||||
game = Game(values)
|
game = Game(values, allow_side_effects=False)
|
||||||
|
|
||||||
# Get official image path
|
# Get official image path
|
||||||
image_path = self.source.cache_location["coverart"] / f"{row[2]}.jpg"
|
image_path = self.source.cache_location["coverart"] / f"{row[2]}.jpg"
|
||||||
@@ -88,21 +83,21 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
|
|
||||||
class LutrisSource(URLExecutableSource):
|
class LutrisSource(URLExecutableSource):
|
||||||
"""Generic Lutris source"""
|
"""Generic lutris source"""
|
||||||
|
|
||||||
name = "Lutris"
|
name = "Lutris"
|
||||||
iterator_class = LutrisSourceIterator
|
iterator_class = LutrisSourceIterator
|
||||||
url_format = "lutris:rungameid/{game_id}"
|
url_format = "lutris:rungameid/{game_id}"
|
||||||
available_on = {"linux"}
|
available_on = set(("linux",))
|
||||||
|
|
||||||
# FIXME possible bug: location picks ~/.var... and cache_lcoation picks ~/.local...
|
# FIXME possible bug: location picks ~/.var... and cache_lcoation picks ~/.local...
|
||||||
|
|
||||||
data_location = Location(
|
data_location = Location(
|
||||||
schema_key="lutris-location",
|
schema_key="lutris-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris",
|
"~/.var/app/net.lutris.Lutris/data/lutris/",
|
||||||
shared.data_dir / "lutris",
|
shared.data_dir / "lutris/",
|
||||||
shared.home / ".local" / "share" / "lutris",
|
"~/.local/share/lutris/",
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
"pga.db": (False, "pga.db"),
|
"pga.db": (False, "pga.db"),
|
||||||
@@ -112,9 +107,9 @@ class LutrisSource(URLExecutableSource):
|
|||||||
cache_location = Location(
|
cache_location = Location(
|
||||||
schema_key="lutris-cache-location",
|
schema_key="lutris-cache-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris",
|
"~/.var/app/net.lutris.Lutris/cache/lutris/",
|
||||||
shared.cache_dir / "lutris",
|
shared.cache_dir / "lutris/",
|
||||||
shared.home / ".cache" / "lutris",
|
"~/.cache/lutris",
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
"coverart": (True, "coverart"),
|
"coverart": (True, "coverart"),
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ from abc import abstractmethod
|
|||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from typing import Any, Generator, Optional
|
from typing import Any, Generator, Optional
|
||||||
|
|
||||||
|
from src.errors.friendly_error import FriendlyError
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.importer.sources.location import Location
|
from src.importer.sources.location import Location, UnresolvableLocationError
|
||||||
|
|
||||||
# Type of the data returned by iterating on a Source
|
# Type of the data returned by iterating on a Source
|
||||||
SourceIterationResult = None | Game | tuple[Game, tuple[Any]]
|
SourceIterationResult = None | Game | tuple[Game, tuple[Any]]
|
||||||
@@ -87,7 +88,7 @@ class Source(Iterable):
|
|||||||
@property
|
@property
|
||||||
def game_id_format(self) -> str:
|
def game_id_format(self) -> str:
|
||||||
"""The string format used to construct game IDs"""
|
"""The string format used to construct game IDs"""
|
||||||
return self.id + "_{game_id}"
|
return self.name.lower() + "_{game_id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_available(self):
|
def is_available(self):
|
||||||
@@ -99,15 +100,27 @@ class Source(Iterable):
|
|||||||
"""The executable format used to construct game executables"""
|
"""The executable format used to construct game executables"""
|
||||||
|
|
||||||
def __iter__(self) -> SourceIterator:
|
def __iter__(self) -> SourceIterator:
|
||||||
"""
|
"""Get an iterator for the source"""
|
||||||
Get an iterator for the source
|
for location_name in (
|
||||||
:raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable
|
locations := {
|
||||||
"""
|
"data": "Data",
|
||||||
for location_name in ("data", "cache", "config"):
|
"cache": "Cache",
|
||||||
|
"config": "Configuration",
|
||||||
|
}.keys()
|
||||||
|
):
|
||||||
location = getattr(self, f"{location_name}_location", None)
|
location = getattr(self, f"{location_name}_location", None)
|
||||||
if location is None:
|
if location is None:
|
||||||
continue
|
continue
|
||||||
location.resolve()
|
try:
|
||||||
|
location.resolve()
|
||||||
|
except UnresolvableLocationError as error:
|
||||||
|
raise FriendlyError(
|
||||||
|
# The variables are the type of location (eg. cache) and the source's name (eg. Steam)
|
||||||
|
"Invalid {} Location for {{}}".format(locations[location_name]),
|
||||||
|
"Pick a new one or disable the source in preferences",
|
||||||
|
(self.name,),
|
||||||
|
(self.name,),
|
||||||
|
) from error
|
||||||
return self.iterator_class(self)
|
return self.iterator_class(self)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,9 +66,6 @@ class SteamSourceIterator(SourceIterator):
|
|||||||
"""Generator method producing games"""
|
"""Generator method producing games"""
|
||||||
appid_cache = set()
|
appid_cache = set()
|
||||||
manifests = self.get_manifests()
|
manifests = self.get_manifests()
|
||||||
|
|
||||||
added_time = int(time())
|
|
||||||
|
|
||||||
for manifest in manifests:
|
for manifest in manifests:
|
||||||
# Get metadata from manifest
|
# Get metadata from manifest
|
||||||
steam = SteamFileHelper()
|
steam = SteamFileHelper()
|
||||||
@@ -90,13 +87,14 @@ class SteamSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
# Build game from local data
|
# Build game from local data
|
||||||
values = {
|
values = {
|
||||||
"added": added_time,
|
"version": shared.SPEC_VERSION,
|
||||||
|
"added": int(time()),
|
||||||
"name": local_data["name"],
|
"name": local_data["name"],
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"game_id": self.source.game_id_format.format(game_id=appid),
|
"game_id": self.source.game_id_format.format(game_id=appid),
|
||||||
"executable": self.source.executable_format.format(game_id=appid),
|
"executable": self.source.executable_format.format(game_id=appid),
|
||||||
}
|
}
|
||||||
game = Game(values)
|
game = Game(values, allow_side_effects=False)
|
||||||
|
|
||||||
# Add official cover image
|
# Add official cover image
|
||||||
image_path = (
|
image_path = (
|
||||||
@@ -111,16 +109,16 @@ class SteamSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
class SteamSource(URLExecutableSource):
|
class SteamSource(URLExecutableSource):
|
||||||
name = "Steam"
|
name = "Steam"
|
||||||
available_on = {"linux", "win32"}
|
available_on = set(("linux", "win32"))
|
||||||
iterator_class = SteamSourceIterator
|
iterator_class = SteamSourceIterator
|
||||||
url_format = "steam://rungameid/{game_id}"
|
url_format = "steam://rungameid/{game_id}"
|
||||||
|
|
||||||
data_location = Location(
|
data_location = Location(
|
||||||
schema_key="steam-location",
|
schema_key="steam-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
|
"~/.var/app/com.valvesoftware.Steam/data/Steam/",
|
||||||
shared.data_dir / "Steam",
|
shared.data_dir / "Steam/",
|
||||||
shared.home / ".steam",
|
"~/.steam/",
|
||||||
shared.programfiles32_dir / "Steam",
|
shared.programfiles32_dir / "Steam",
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
|
|||||||
99
src/keyboard_emulator.py
Normal file
99
src/keyboard_emulator.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import logging
|
||||||
|
import select
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from gi.repository import GLib
|
||||||
|
from snegg.oeffis import DisconnectedError, Oeffis, SessionClosedError
|
||||||
|
from snegg.ei import Sender, DeviceCapability, EventType, Seat, Device, Event
|
||||||
|
|
||||||
|
|
||||||
|
class PortalError(Exception):
|
||||||
|
"""Error raised when a oeffis portal can't be acquired"""
|
||||||
|
|
||||||
|
|
||||||
|
class KeyboardEmulator:
|
||||||
|
"""
|
||||||
|
A class that triggers keypresses with libei
|
||||||
|
|
||||||
|
Libei docs: https://libinput.pages.freedesktop.org/libei/
|
||||||
|
Snegg docs: https://libinput.pages.freedesktop.org/snegg/snegg.ei.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
app = None
|
||||||
|
queue: deque = None
|
||||||
|
|
||||||
|
sender: Sender = None
|
||||||
|
seat: Seat = None
|
||||||
|
keyboard: Device = None
|
||||||
|
|
||||||
|
def __init__(self, app) -> None:
|
||||||
|
self.app = app
|
||||||
|
self.queue = deque()
|
||||||
|
|
||||||
|
self.app.connect("emulate-key", self.on_emulate_key)
|
||||||
|
GLib.Thread.new(None, self.thread_func)
|
||||||
|
|
||||||
|
def on_emulate_key(self, keyval):
|
||||||
|
self.queue.append(keyval)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_eis_portal() -> Oeffis:
|
||||||
|
"""Get a portal to the eis server"""
|
||||||
|
portal = Oeffis.create()
|
||||||
|
if portal is None:
|
||||||
|
raise PortalError()
|
||||||
|
poll = select.poll()
|
||||||
|
poll.register(portal.fd)
|
||||||
|
while poll.poll():
|
||||||
|
try:
|
||||||
|
if portal.dispatch():
|
||||||
|
# We need to keep the portal object alive so we don't get disconnected
|
||||||
|
return portal
|
||||||
|
except (SessionClosedError, DisconnectedError) as error:
|
||||||
|
raise PortalError() from error
|
||||||
|
|
||||||
|
def thread_func(self):
|
||||||
|
"""Daemon thread entry point"""
|
||||||
|
|
||||||
|
# Connect to the EIS server
|
||||||
|
try:
|
||||||
|
portal = self.get_eis_portal()
|
||||||
|
except PortalError as error:
|
||||||
|
logging.error("Can't get EIS portal", exc_info=error)
|
||||||
|
raise
|
||||||
|
self.sender = Sender.create_for_fd(fd=portal.eis_fd, name="ei-debug-events")
|
||||||
|
|
||||||
|
# Handle sender events
|
||||||
|
poll = select.poll()
|
||||||
|
poll.register(self.sender.fd)
|
||||||
|
while poll.poll():
|
||||||
|
self.sender.dispatch()
|
||||||
|
for event in self.sender.events:
|
||||||
|
self.handle_sender_event(event)
|
||||||
|
|
||||||
|
def handle_sender_event(self, event: Event):
|
||||||
|
"""Handle libei sender (input producer) events"""
|
||||||
|
|
||||||
|
match event.event_type:
|
||||||
|
# The emulated seat is created, we need to specify its capabilities
|
||||||
|
case EventType.SEAT_ADDED:
|
||||||
|
if not event.seat:
|
||||||
|
return
|
||||||
|
self.seat = event.seat
|
||||||
|
self.seat.bind(DeviceCapability.KEYBOARD)
|
||||||
|
|
||||||
|
# A device was added to the seat (here, we're only doing a keyboard)
|
||||||
|
case EventType.DEVICE_ADDED:
|
||||||
|
if not event.device:
|
||||||
|
return
|
||||||
|
self.keyboard = event.device
|
||||||
|
|
||||||
|
# Input can be processed, send keys
|
||||||
|
case EventType.DEVICE_RESUMED:
|
||||||
|
self.keyboard.start_emulating()
|
||||||
|
keyval = self.queue.popleft()
|
||||||
|
self.keyboard.keyboard_key(keyval, True)
|
||||||
|
self.keyboard.frame()
|
||||||
|
self.keyboard.keyboard_key(keyval, False)
|
||||||
|
self.keyboard.frame()
|
||||||
|
self.keyboard.stop_emulating()
|
||||||
@@ -33,8 +33,6 @@ class SessionFileHandler(StreamHandler):
|
|||||||
The files are compressed and older sessions logs are kept up to a small limit.
|
The files are compressed and older sessions logs are kept up to a small limit.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NUMBER_SUFFIX_POSITION = 1
|
|
||||||
|
|
||||||
backup_count: int
|
backup_count: int
|
||||||
filename: Path
|
filename: Path
|
||||||
log_file: StringIO = None
|
log_file: StringIO = None
|
||||||
@@ -43,79 +41,50 @@ class SessionFileHandler(StreamHandler):
|
|||||||
"""Create the log dir if needed"""
|
"""Create the log dir if needed"""
|
||||||
self.filename.parent.mkdir(exist_ok=True, parents=True)
|
self.filename.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
def path_is_logfile(self, path: Path) -> bool:
|
|
||||||
return path.is_file() and path.name.startswith(self.filename.stem)
|
|
||||||
|
|
||||||
def path_has_number(self, path: Path) -> bool:
|
|
||||||
try:
|
|
||||||
int(path.suffixes[self.NUMBER_SUFFIX_POSITION][1:])
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_path_number(self, path: Path) -> int:
|
|
||||||
"""Get the number extension in the filename as an int"""
|
|
||||||
suffixes = path.suffixes
|
|
||||||
number = (
|
|
||||||
0
|
|
||||||
if not self.path_has_number(path)
|
|
||||||
else int(suffixes[self.NUMBER_SUFFIX_POSITION][1:])
|
|
||||||
)
|
|
||||||
return number
|
|
||||||
|
|
||||||
def set_path_number(self, path: Path, number: int) -> str:
|
|
||||||
"""Set or add the number extension in the filename"""
|
|
||||||
suffixes = path.suffixes
|
|
||||||
if self.path_has_number(path):
|
|
||||||
suffixes.pop(self.NUMBER_SUFFIX_POSITION)
|
|
||||||
suffixes.insert(self.NUMBER_SUFFIX_POSITION, f".{number}")
|
|
||||||
stem = path.name.split(".", maxsplit=1)[0]
|
|
||||||
new_name = stem + "".join(suffixes)
|
|
||||||
return new_name
|
|
||||||
|
|
||||||
def file_sort_key(self, path: Path) -> int:
|
|
||||||
"""Key function used to sort files"""
|
|
||||||
return self.get_path_number(path) if self.path_has_number(path) else 0
|
|
||||||
|
|
||||||
def get_logfiles(self) -> list[Path]:
|
|
||||||
"""Get the log files"""
|
|
||||||
logfiles = list(filter(self.path_is_logfile, self.filename.parent.iterdir()))
|
|
||||||
logfiles.sort(key=self.file_sort_key, reverse=True)
|
|
||||||
return logfiles
|
|
||||||
|
|
||||||
def rotate_file(self, path: Path):
|
def rotate_file(self, path: Path):
|
||||||
"""Rotate a file's number suffix and remove it if it's too old"""
|
"""Rotate a file's number suffix and remove it if it's too old"""
|
||||||
|
|
||||||
# If uncompressed, compress
|
# Skip non interesting dir entries
|
||||||
if not path.name.endswith(".xz"):
|
if not (path.is_file() and path.name.startswith(self.filename.name)):
|
||||||
compressed_path = path.with_suffix(path.suffix + ".xz")
|
return
|
||||||
with (
|
|
||||||
lzma.open(
|
# Compute the new number suffix
|
||||||
compressed_path,
|
suffixes = path.suffixes
|
||||||
"wt",
|
has_number = len(suffixes) != len(self.filename.suffixes)
|
||||||
format=FORMAT_XZ,
|
current_number = 0 if not has_number else int(suffixes[-1][1:])
|
||||||
preset=PRESET_DEFAULT,
|
new_number = current_number + 1
|
||||||
encoding="utf-8",
|
|
||||||
) as lzma_file,
|
|
||||||
open(path, "r", encoding="utf-8") as original_file,
|
|
||||||
):
|
|
||||||
lzma_file.write(original_file.read())
|
|
||||||
path.unlink()
|
|
||||||
path = compressed_path
|
|
||||||
|
|
||||||
# Rename with new number suffix
|
# Rename with new number suffix
|
||||||
new_number = self.get_path_number(path) + 1
|
if has_number:
|
||||||
new_path_name = self.set_path_number(path, new_number)
|
suffixes.pop()
|
||||||
path = path.rename(path.with_name(new_path_name))
|
suffixes.append(f".{new_number}")
|
||||||
|
stem = path.name.split(".", maxsplit=1)[0]
|
||||||
|
new_name = stem + "".join(suffixes)
|
||||||
|
path = path.rename(path.with_name(new_name))
|
||||||
|
|
||||||
# Remove older files
|
# Remove older files
|
||||||
if new_number > self.backup_count:
|
if new_number > self.backup_count:
|
||||||
path.unlink()
|
path.unlink()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def file_sort_key(self, path: Path) -> int:
|
||||||
|
"""Key function used to sort files"""
|
||||||
|
if not path.name.startswith(self.filename.name):
|
||||||
|
# First all files that aren't logs
|
||||||
|
return -1
|
||||||
|
if path.name == self.filename.name:
|
||||||
|
# Then the latest log file
|
||||||
|
return 0
|
||||||
|
# Then in order the other log files
|
||||||
|
return int(path.suffixes[-1][1:])
|
||||||
|
|
||||||
|
def get_files(self) -> list[Path]:
|
||||||
|
return list(self.filename.parent.iterdir())
|
||||||
|
|
||||||
def rotate(self) -> None:
|
def rotate(self) -> None:
|
||||||
"""Rotate the numbered suffix on the log files and remove old ones"""
|
"""Rotate the numbered suffix on the log files and remove old ones"""
|
||||||
for path in self.get_logfiles():
|
(files := self.get_files()).sort(key=self.file_sort_key, reverse=True)
|
||||||
|
for path in files:
|
||||||
self.rotate_file(path)
|
self.rotate_file(path)
|
||||||
|
|
||||||
def __init__(self, filename: PathLike, backup_count: int = 2) -> None:
|
def __init__(self, filename: PathLike, backup_count: int = 2) -> None:
|
||||||
@@ -123,8 +92,10 @@ class SessionFileHandler(StreamHandler):
|
|||||||
self.backup_count = backup_count
|
self.backup_count = backup_count
|
||||||
self.create_dir()
|
self.create_dir()
|
||||||
self.rotate()
|
self.rotate()
|
||||||
self.log_file = open(self.filename, "w", encoding="utf-8")
|
shared.log_files = self.get_files()
|
||||||
shared.log_files = self.get_logfiles()
|
self.log_file = lzma.open(
|
||||||
|
self.filename, "at", format=FORMAT_XZ, preset=PRESET_DEFAULT
|
||||||
|
)
|
||||||
super().__init__(self.log_file)
|
super().__init__(self.log_file)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import logging.config as logging_dot_config
|
import logging.config as logging_dot_config
|
||||||
import os
|
import os
|
||||||
import platform
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -36,14 +35,13 @@ def setup_logging():
|
|||||||
app_log_level = os.environ.get("LOGLEVEL", profile_app_log_level).upper()
|
app_log_level = os.environ.get("LOGLEVEL", profile_app_log_level).upper()
|
||||||
lib_log_level = os.environ.get("LIBLOGLEVEL", profile_lib_log_level).upper()
|
lib_log_level = os.environ.get("LIBLOGLEVEL", profile_lib_log_level).upper()
|
||||||
|
|
||||||
log_filename = shared.cache_dir / "cartridges" / "logs" / "cartridges.log"
|
log_filename = shared.cache_dir / "cartridges" / "logs" / "cartridges.log.xz"
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"formatters": {
|
"formatters": {
|
||||||
"file_formatter": {
|
"file_formatter": {
|
||||||
"format": "%(asctime)s - %(levelname)s: %(message)s",
|
"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
|
||||||
"datefmt": "%M:%S",
|
|
||||||
},
|
},
|
||||||
"console_formatter": {
|
"console_formatter": {
|
||||||
"format": "%(name)s %(levelname)s - %(message)s",
|
"format": "%(name)s %(levelname)s - %(message)s",
|
||||||
@@ -93,8 +91,9 @@ def log_system_info():
|
|||||||
"""Log system debug information"""
|
"""Log system debug information"""
|
||||||
|
|
||||||
logging.debug("Starting %s v%s (%s)", shared.APP_ID, shared.VERSION, shared.PROFILE)
|
logging.debug("Starting %s v%s (%s)", shared.APP_ID, shared.VERSION, shared.PROFILE)
|
||||||
|
logging.debug("System: %s", sys.platform)
|
||||||
logging.debug("Python version: %s", sys.version)
|
logging.debug("Python version: %s", sys.version)
|
||||||
if os.getenv("FLATPAK_ID") == shared.APP_ID:
|
if os.getenv("FLATPAK_ID"):
|
||||||
process = subprocess.run(
|
process = subprocess.run(
|
||||||
("flatpak-spawn", "--host", "flatpak", "--version"),
|
("flatpak-spawn", "--host", "flatpak", "--version"),
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -102,8 +101,11 @@ def log_system_info():
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
logging.debug("Flatpak version: %s", process.stdout.rstrip())
|
logging.debug("Flatpak version: %s", process.stdout.rstrip())
|
||||||
logging.debug("Platform: %s", platform.platform())
|
|
||||||
if os.name == "posix":
|
if os.name == "posix":
|
||||||
for key, value in platform.uname()._asdict().items():
|
uname = os.uname()
|
||||||
logging.debug("\t%s: %s", key.title(), value)
|
logging.debug("Uname info:")
|
||||||
logging.debug("─" * 37)
|
logging.debug("\tsysname: %s", uname.sysname)
|
||||||
|
logging.debug("\trelease: %s", uname.release)
|
||||||
|
logging.debug("\tversion: %s", uname.version)
|
||||||
|
logging.debug("\tmachine: %s", uname.machine)
|
||||||
|
logging.debug("-" * 80)
|
||||||
|
|||||||
149
src/main.py
149
src/main.py
@@ -17,6 +17,8 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
import lzma
|
import lzma
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -24,13 +26,22 @@ import gi
|
|||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
gi.require_version("Adw", "1")
|
gi.require_version("Adw", "1")
|
||||||
|
gi.require_version("Manette", "0.2")
|
||||||
|
|
||||||
# pylint: disable=wrong-import-position
|
# pylint: disable=wrong-import-position
|
||||||
from gi.repository import Adw, Gio, GLib, Gtk
|
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk, Manette
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.details_window import DetailsWindow
|
from src.details_window import DetailsWindow
|
||||||
|
from src.game import Game
|
||||||
from src.importer.importer import Importer
|
from src.importer.importer import Importer
|
||||||
|
from src.importer.sources.bottles_source import BottlesSource
|
||||||
|
from src.importer.sources.heroic_source import HeroicSource
|
||||||
|
from src.importer.sources.itch_source import ItchSource
|
||||||
|
from src.importer.sources.legendary_source import LegendarySource
|
||||||
|
from src.importer.sources.lutris_source import LutrisSource
|
||||||
|
from src.importer.sources.steam_source import SteamSource
|
||||||
|
from src.keyboard_emulator import KeyboardEmulator
|
||||||
from src.logging.setup import log_system_info, setup_logging
|
from src.logging.setup import log_system_info, setup_logging
|
||||||
from src.preferences import PreferencesWindow
|
from src.preferences import PreferencesWindow
|
||||||
from src.store.managers.display_manager import DisplayManager
|
from src.store.managers.display_manager import DisplayManager
|
||||||
@@ -45,16 +56,81 @@ from src.window import CartridgesWindow
|
|||||||
|
|
||||||
class CartridgesApplication(Adw.Application):
|
class CartridgesApplication(Adw.Application):
|
||||||
win = None
|
win = None
|
||||||
|
window_controller = None
|
||||||
|
keyboard_emulator = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
shared.store = Store()
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
application_id=shared.APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE
|
application_id=shared.APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@GObject.Signal(name="emulate-key", arg_types=[int])
|
||||||
|
def emulate_key(self, keyval) -> None:
|
||||||
|
"""Signal emitted when the app wants to emulate a keypress"""
|
||||||
|
|
||||||
|
def gamepad_abs_axis(self, _device, event):
|
||||||
|
logging.debug(event.get_absolute())
|
||||||
|
|
||||||
|
def gamepad_hat_axis(self, device, event):
|
||||||
|
device.rumble(1000, 1000, 50)
|
||||||
|
logging.debug(
|
||||||
|
"Gamepad: hat axis: %s, value: %s", *(hat := event.get_hat())[1:3]
|
||||||
|
)
|
||||||
|
|
||||||
|
if hat[2] != 0:
|
||||||
|
self.navigate(hat[1] + hat[2])
|
||||||
|
|
||||||
|
def navigate(self, direction: int):
|
||||||
|
match direction:
|
||||||
|
case 16:
|
||||||
|
self.emit("emulate-key", Gdk.KEY_Up)
|
||||||
|
print("up")
|
||||||
|
case 18:
|
||||||
|
print("down")
|
||||||
|
case 15:
|
||||||
|
print("left")
|
||||||
|
case 17:
|
||||||
|
print("right")
|
||||||
|
|
||||||
|
def print_args(self, *args):
|
||||||
|
print(*args)
|
||||||
|
|
||||||
|
def gamepad_button_pressed(self, _device, event):
|
||||||
|
logging.debug("Gamepad: %s pressed", (button := event.get_button()[1]))
|
||||||
|
|
||||||
|
match button:
|
||||||
|
case 304:
|
||||||
|
print("A button pressed")
|
||||||
|
case 305:
|
||||||
|
print("B button pressed")
|
||||||
|
case 307:
|
||||||
|
print("X button pressed")
|
||||||
|
case 308:
|
||||||
|
print("Y button pressed")
|
||||||
|
case 314:
|
||||||
|
self.win.on_show_hidden_action()
|
||||||
|
|
||||||
|
def gamepad_listen(self, device, *_args):
|
||||||
|
device.connect("button-press-event", self.gamepad_button_pressed)
|
||||||
|
device.connect("hat-axis-event", self.gamepad_hat_axis)
|
||||||
|
|
||||||
|
def log_connected():
|
||||||
|
logging.debug("%s connected", device.get_name())
|
||||||
|
GLib.timeout_add_seconds(60, log_connected)
|
||||||
|
|
||||||
|
log_connected()
|
||||||
|
|
||||||
def do_activate(self): # pylint: disable=arguments-differ
|
def do_activate(self): # pylint: disable=arguments-differ
|
||||||
"""Called on app creation"""
|
"""Called on app creation"""
|
||||||
|
|
||||||
|
# Setup gamepads
|
||||||
|
manette_monitor = Manette.Monitor.new()
|
||||||
|
manette_iter = manette_monitor.iterate()
|
||||||
|
|
||||||
|
while (device := manette_iter.next())[0]:
|
||||||
|
self.gamepad_listen(device[1])
|
||||||
|
self.keyboard_emulator = KeyboardEmulator(self)
|
||||||
|
|
||||||
# Set fallback icon-name
|
# Set fallback icon-name
|
||||||
Gtk.Window.set_default_icon_name(shared.APP_ID)
|
Gtk.Window.set_default_icon_name(shared.APP_ID)
|
||||||
|
|
||||||
@@ -63,6 +139,16 @@ class CartridgesApplication(Adw.Application):
|
|||||||
if not self.win:
|
if not self.win:
|
||||||
shared.win = self.win = CartridgesWindow(application=self)
|
shared.win = self.win = CartridgesWindow(application=self)
|
||||||
|
|
||||||
|
for index in range(
|
||||||
|
(list_model := self.win.observe_controllers()).get_n_items()
|
||||||
|
):
|
||||||
|
if isinstance((item := list_model.get_item(index)), Gtk.EventControllerKey):
|
||||||
|
self.window_controller = item
|
||||||
|
break
|
||||||
|
|
||||||
|
self.window_controller.connect("key-pressed", self.print_args)
|
||||||
|
self.window_controller.connect("key-released", self.print_args)
|
||||||
|
|
||||||
# Save window geometry
|
# Save window geometry
|
||||||
shared.state_schema.bind(
|
shared.state_schema.bind(
|
||||||
"width", self.win, "default-width", Gio.SettingsBindFlags.DEFAULT
|
"width", self.win, "default-width", Gio.SettingsBindFlags.DEFAULT
|
||||||
@@ -74,16 +160,20 @@ class CartridgesApplication(Adw.Application):
|
|||||||
"is-maximized", self.win, "maximized", Gio.SettingsBindFlags.DEFAULT
|
"is-maximized", self.win, "maximized", Gio.SettingsBindFlags.DEFAULT
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add managers to the store for game imports
|
# Create the games store ready to load games from disk
|
||||||
shared.store.add_manager(DisplayManager())
|
if not shared.store:
|
||||||
shared.store.add_manager(FileManager())
|
shared.store = Store()
|
||||||
|
shared.store.add_manager(FileManager(), False)
|
||||||
|
shared.store.add_manager(DisplayManager())
|
||||||
|
|
||||||
|
self.load_games_from_disk()
|
||||||
|
|
||||||
|
# Add rest of the managers for game imports
|
||||||
shared.store.add_manager(LocalCoverManager())
|
shared.store.add_manager(LocalCoverManager())
|
||||||
shared.store.add_manager(SteamAPIManager())
|
shared.store.add_manager(SteamAPIManager())
|
||||||
shared.store.add_manager(OnlineCoverManager())
|
shared.store.add_manager(OnlineCoverManager())
|
||||||
shared.store.add_manager(SGDBManager())
|
shared.store.add_manager(SGDBManager())
|
||||||
|
shared.store.enable_manager_in_pipelines(FileManager)
|
||||||
# Load games from disk
|
|
||||||
Importer().load_games_from_disk()
|
|
||||||
|
|
||||||
# Create actions
|
# Create actions
|
||||||
self.create_actions(
|
self.create_actions(
|
||||||
@@ -124,21 +214,18 @@ class CartridgesApplication(Adw.Application):
|
|||||||
|
|
||||||
self.win.present()
|
self.win.present()
|
||||||
|
|
||||||
|
def load_games_from_disk(self):
|
||||||
|
if shared.games_dir.is_dir():
|
||||||
|
for game_file in shared.games_dir.iterdir():
|
||||||
|
data = json.load(game_file.open())
|
||||||
|
game = Game(data, allow_side_effects=False)
|
||||||
|
shared.store.add_game(game, {"skip_save": True})
|
||||||
|
|
||||||
def on_about_action(self, *_args):
|
def on_about_action(self, *_args):
|
||||||
# Get the debug info from the log files
|
|
||||||
debug_str = ""
|
debug_str = ""
|
||||||
for i, path in enumerate(shared.log_files):
|
for path in shared.log_files:
|
||||||
# Add a horizontal line between runs
|
log_file = lzma.open(path, "rt", encoding="utf-8")
|
||||||
if i > 0:
|
|
||||||
debug_str += "─" * 37 + "\n"
|
|
||||||
# Add the run's logs
|
|
||||||
log_file = (
|
|
||||||
lzma.open(path, "rt", encoding="utf-8")
|
|
||||||
if path.name.endswith(".xz")
|
|
||||||
else open(path, "r", encoding="utf-8")
|
|
||||||
)
|
|
||||||
debug_str += log_file.read()
|
debug_str += log_file.read()
|
||||||
log_file.close()
|
|
||||||
|
|
||||||
about = Adw.AboutWindow(
|
about = Adw.AboutWindow(
|
||||||
transient_for=self.win,
|
transient_for=self.win,
|
||||||
@@ -190,7 +277,27 @@ class CartridgesApplication(Adw.Application):
|
|||||||
DetailsWindow()
|
DetailsWindow()
|
||||||
|
|
||||||
def on_import_action(self, *_args):
|
def on_import_action(self, *_args):
|
||||||
Importer().run()
|
importer = Importer()
|
||||||
|
|
||||||
|
if shared.schema.get_boolean("lutris"):
|
||||||
|
importer.add_source(LutrisSource())
|
||||||
|
|
||||||
|
if shared.schema.get_boolean("steam"):
|
||||||
|
importer.add_source(SteamSource())
|
||||||
|
|
||||||
|
if shared.schema.get_boolean("heroic"):
|
||||||
|
importer.add_source(HeroicSource())
|
||||||
|
|
||||||
|
if shared.schema.get_boolean("bottles"):
|
||||||
|
importer.add_source(BottlesSource())
|
||||||
|
|
||||||
|
if shared.schema.get_boolean("itch"):
|
||||||
|
importer.add_source(ItchSource())
|
||||||
|
|
||||||
|
if shared.schema.get_boolean("legendary"):
|
||||||
|
importer.add_source(LegendarySource())
|
||||||
|
|
||||||
|
importer.run()
|
||||||
|
|
||||||
def on_remove_game_action(self, *_args):
|
def on_remove_game_action(self, *_args):
|
||||||
self.win.active_game.remove_game()
|
self.win.active_game.remove_game()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ install_data(
|
|||||||
'details_window.py',
|
'details_window.py',
|
||||||
'game.py',
|
'game.py',
|
||||||
'game_cover.py',
|
'game_cover.py',
|
||||||
|
'keyboard_emulator.py',
|
||||||
configure_file(
|
configure_file(
|
||||||
input: 'shared.py.in',
|
input: 'shared.py.in',
|
||||||
output: 'shared.py',
|
output: 'shared.py',
|
||||||
|
|||||||
@@ -26,11 +26,9 @@ from gi.repository import Adw, Gio, GLib, Gtk
|
|||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.importer.sources.bottles_source import BottlesSource
|
from src.importer.sources.bottles_source import BottlesSource
|
||||||
from src.importer.sources.flatpak_source import FlatpakSource
|
|
||||||
from src.importer.sources.heroic_source import HeroicSource
|
from src.importer.sources.heroic_source import HeroicSource
|
||||||
from src.importer.sources.itch_source import ItchSource
|
from src.importer.sources.itch_source import ItchSource
|
||||||
from src.importer.sources.legendary_source import LegendarySource
|
from src.importer.sources.legendary_source import LegendarySource
|
||||||
from src.importer.sources.location import UnresolvableLocationError
|
|
||||||
from src.importer.sources.lutris_source import LutrisSource
|
from src.importer.sources.lutris_source import LutrisSource
|
||||||
from src.importer.sources.source import Source
|
from src.importer.sources.source import Source
|
||||||
from src.importer.sources.steam_source import SteamSource
|
from src.importer.sources.steam_source import SteamSource
|
||||||
@@ -61,7 +59,6 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
lutris_cache_action_row = Gtk.Template.Child()
|
lutris_cache_action_row = Gtk.Template.Child()
|
||||||
lutris_cache_file_chooser_button = Gtk.Template.Child()
|
lutris_cache_file_chooser_button = Gtk.Template.Child()
|
||||||
lutris_import_steam_switch = Gtk.Template.Child()
|
lutris_import_steam_switch = Gtk.Template.Child()
|
||||||
lutris_import_flatpak_switch = Gtk.Template.Child()
|
|
||||||
|
|
||||||
heroic_expander_row = Gtk.Template.Child()
|
heroic_expander_row = Gtk.Template.Child()
|
||||||
heroic_config_action_row = Gtk.Template.Child()
|
heroic_config_action_row = Gtk.Template.Child()
|
||||||
@@ -82,11 +79,6 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
legendary_config_action_row = Gtk.Template.Child()
|
legendary_config_action_row = Gtk.Template.Child()
|
||||||
legendary_config_file_chooser_button = Gtk.Template.Child()
|
legendary_config_file_chooser_button = Gtk.Template.Child()
|
||||||
|
|
||||||
flatpak_expander_row = Gtk.Template.Child()
|
|
||||||
flatpak_data_action_row = Gtk.Template.Child()
|
|
||||||
flatpak_data_file_chooser_button = Gtk.Template.Child()
|
|
||||||
flatpak_import_launchers_switch = Gtk.Template.Child()
|
|
||||||
|
|
||||||
sgdb_key_group = Gtk.Template.Child()
|
sgdb_key_group = Gtk.Template.Child()
|
||||||
sgdb_key_entry_row = Gtk.Template.Child()
|
sgdb_key_entry_row = Gtk.Template.Child()
|
||||||
sgdb_switch = Gtk.Template.Child()
|
sgdb_switch = Gtk.Template.Child()
|
||||||
@@ -100,7 +92,6 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
remove_all_games_button = Gtk.Template.Child()
|
remove_all_games_button = Gtk.Template.Child()
|
||||||
|
|
||||||
removed_games = set()
|
removed_games = set()
|
||||||
warning_menu_buttons = {}
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@@ -133,7 +124,6 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
# Sources settings
|
# Sources settings
|
||||||
for source_class in (
|
for source_class in (
|
||||||
BottlesSource,
|
BottlesSource,
|
||||||
FlatpakSource,
|
|
||||||
HeroicSource,
|
HeroicSource,
|
||||||
ItchSource,
|
ItchSource,
|
||||||
LegendarySource,
|
LegendarySource,
|
||||||
@@ -143,7 +133,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
source = source_class()
|
source = source_class()
|
||||||
if not source.is_available:
|
if not source.is_available:
|
||||||
expander_row = getattr(self, f"{source.id}_expander_row")
|
expander_row = getattr(self, f"{source.id}_expander_row")
|
||||||
expander_row.set_visible(False)
|
expander_row.remove()
|
||||||
else:
|
else:
|
||||||
self.init_source_row(source)
|
self.init_source_row(source)
|
||||||
|
|
||||||
@@ -178,11 +168,9 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
"cover-launches-game",
|
"cover-launches-game",
|
||||||
"high-quality-images",
|
"high-quality-images",
|
||||||
"lutris-import-steam",
|
"lutris-import-steam",
|
||||||
"lutris-import-flatpak",
|
|
||||||
"heroic-import-epic",
|
"heroic-import-epic",
|
||||||
"heroic-import-gog",
|
"heroic-import-gog",
|
||||||
"heroic-import-sideload",
|
"heroic-import-sideload",
|
||||||
"flatpak-import-launchers",
|
|
||||||
"sgdb",
|
"sgdb",
|
||||||
"sgdb-prefer",
|
"sgdb-prefer",
|
||||||
"sgdb-animated",
|
"sgdb-animated",
|
||||||
@@ -214,7 +202,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
self.toast.dismiss()
|
self.toast.dismiss()
|
||||||
|
|
||||||
def remove_all_games(self, *_args):
|
def remove_all_games(self, *_args):
|
||||||
for game in shared.store.games.values():
|
for game in self.win.games.values():
|
||||||
if not game.removed:
|
if not game.removed:
|
||||||
self.removed_games.add(game)
|
self.removed_games.add(game)
|
||||||
|
|
||||||
@@ -227,7 +215,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
|
|
||||||
self.add_toast(self.toast)
|
self.add_toast(self.toast)
|
||||||
|
|
||||||
def reset_app(self, *_args):
|
def reset_app(*_args):
|
||||||
rmtree(shared.data_dir / "cartridges", True)
|
rmtree(shared.data_dir / "cartridges", True)
|
||||||
rmtree(shared.config_dir / "cartridges", True)
|
rmtree(shared.config_dir / "cartridges", True)
|
||||||
rmtree(shared.cache_dir / "cartridges", True)
|
rmtree(shared.cache_dir / "cartridges", True)
|
||||||
@@ -253,65 +241,15 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
if not action_row:
|
if not action_row:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Historically "location" meant data or config, so the key stays shared
|
||||||
infix = "-cache" if location == "cache" else ""
|
infix = "-cache" if location == "cache" else ""
|
||||||
key = f"{source.id}{infix}-location"
|
key = f"{source.id}{infix}-location"
|
||||||
path = Path(shared.schema.get_string(key)).expanduser()
|
path = Path(shared.schema.get_string(key)).expanduser()
|
||||||
|
|
||||||
# Remove the path prefix if picked via Flatpak portal
|
# Remove the path if the dir is picked via the Flatpak portal
|
||||||
subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path))
|
subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path))
|
||||||
action_row.set_subtitle(subtitle)
|
action_row.set_subtitle(subtitle)
|
||||||
|
|
||||||
def resolve_locations(self, source):
|
|
||||||
"""Resolve locations and add a warning if location cannot be found"""
|
|
||||||
|
|
||||||
def clear_warning_selection(_widget, label):
|
|
||||||
label.select_region(-1, -1)
|
|
||||||
|
|
||||||
for location_name in ("data", "config", "cache"):
|
|
||||||
action_row = getattr(self, f"{source.id}_{location_name}_action_row", None)
|
|
||||||
if not action_row:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
getattr(source, f"{location_name}_location", None).resolve()
|
|
||||||
|
|
||||||
except UnresolvableLocationError:
|
|
||||||
popover = Gtk.Popover(
|
|
||||||
child=(
|
|
||||||
label := Gtk.Label(
|
|
||||||
label=(
|
|
||||||
'<span rise="12pt"><b><big>'
|
|
||||||
+ _("Installation Not Found")
|
|
||||||
+ "</big></b></span>\n"
|
|
||||||
+ _("Select a valid directory.")
|
|
||||||
),
|
|
||||||
use_markup=True,
|
|
||||||
wrap=True,
|
|
||||||
max_width_chars=50,
|
|
||||||
halign=Gtk.Align.CENTER,
|
|
||||||
valign=Gtk.Align.CENTER,
|
|
||||||
justify=Gtk.Justification.CENTER,
|
|
||||||
margin_top=9,
|
|
||||||
margin_bottom=9,
|
|
||||||
margin_start=12,
|
|
||||||
margin_end=12,
|
|
||||||
selectable=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
popover.connect("show", clear_warning_selection, label)
|
|
||||||
|
|
||||||
menu_button = Gtk.MenuButton(
|
|
||||||
icon_name="dialog-warning-symbolic",
|
|
||||||
valign=Gtk.Align.CENTER,
|
|
||||||
popover=popover,
|
|
||||||
)
|
|
||||||
menu_button.add_css_class("warning")
|
|
||||||
|
|
||||||
action_row.add_prefix(menu_button)
|
|
||||||
self.warning_menu_buttons[source.id] = menu_button
|
|
||||||
|
|
||||||
def init_source_row(self, source: Source):
|
def init_source_row(self, source: Source):
|
||||||
"""Initialize a preference row for a source class"""
|
"""Initialize a preference row for a source class"""
|
||||||
|
|
||||||
@@ -333,30 +271,20 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
shared.schema.set_string(key, value)
|
shared.schema.set_string(key, value)
|
||||||
# Update the row
|
# Update the row
|
||||||
self.update_source_action_row_paths(source)
|
self.update_source_action_row_paths(source)
|
||||||
|
|
||||||
if self.warning_menu_buttons.get(source.id):
|
|
||||||
action_row = getattr(
|
|
||||||
self, f"{source.id}_{location_name}_action_row", None
|
|
||||||
)
|
|
||||||
action_row.remove(self.warning_menu_buttons[source.id])
|
|
||||||
self.warning_menu_buttons.pop(source.id)
|
|
||||||
|
|
||||||
logging.debug("User-set value for schema key %s: %s", key, value)
|
logging.debug("User-set value for schema key %s: %s", key, value)
|
||||||
|
|
||||||
# Bad picked location, inform user
|
# Bad picked location, inform user
|
||||||
else:
|
else:
|
||||||
if location_name == "cache":
|
if location_name == "cache":
|
||||||
title = _("Invalid Directory")
|
title = "Cache directory not found"
|
||||||
# The variable is the name of the source
|
subtitle_format = "Select the {} cache directory."
|
||||||
subtitle_format = _("Select the {} cache directory.")
|
|
||||||
else:
|
else:
|
||||||
title = _("Invalid Directory")
|
title = "Installation directory not found"
|
||||||
# The variable is the name of the source
|
subtitle_format = "Select the {} installation directory."
|
||||||
subtitle_format = _("Select the {} installation directory.")
|
|
||||||
dialog = create_dialog(
|
dialog = create_dialog(
|
||||||
self,
|
self,
|
||||||
title,
|
_(title),
|
||||||
subtitle_format.format(source.name),
|
_(subtitle_format).format(source.name),
|
||||||
"choose_folder",
|
"choose_folder",
|
||||||
_("Set Location"),
|
_("Set Location"),
|
||||||
)
|
)
|
||||||
@@ -383,5 +311,4 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
button.connect("clicked", self.choose_folder, set_dir, location)
|
button.connect("clicked", self.choose_folder, set_dir, location)
|
||||||
|
|
||||||
# Set the source row subtitles
|
# Set the source row subtitles
|
||||||
self.resolve_locations(source)
|
|
||||||
self.update_source_action_row_paths(source)
|
self.update_source_action_row_paths(source)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from gi.repository import Gdk, Gio, GLib
|
from gi.repository import Gdk, Gio
|
||||||
|
|
||||||
APP_ID = "@APP_ID@"
|
APP_ID = "@APP_ID@"
|
||||||
VERSION = "@VERSION@"
|
VERSION = "@VERSION@"
|
||||||
@@ -31,19 +31,25 @@ SPEC_VERSION = 1.5 # The version of the game_id.json spec
|
|||||||
schema = Gio.Settings.new(APP_ID)
|
schema = Gio.Settings.new(APP_ID)
|
||||||
state_schema = Gio.Settings.new(APP_ID + ".State")
|
state_schema = Gio.Settings.new(APP_ID + ".State")
|
||||||
|
|
||||||
home = Path.home()
|
data_dir = (
|
||||||
data_dir = Path(GLib.get_user_data_dir())
|
Path(os.getenv("XDG_DATA_HOME"))
|
||||||
config_dir = Path(GLib.get_user_config_dir())
|
if "XDG_DATA_HOME" in os.environ
|
||||||
cache_dir = Path(GLib.get_user_cache_dir())
|
else Path.home() / ".local" / "share"
|
||||||
flatpak_dir = home / ".var" / "app"
|
)
|
||||||
|
config_dir = (
|
||||||
|
Path(os.getenv("XDG_CONFIG_HOME"))
|
||||||
|
if "XDG_CONFIG_HOME" in os.environ
|
||||||
|
else Path.home() / ".config"
|
||||||
|
)
|
||||||
|
cache_dir = (
|
||||||
|
Path(os.getenv("XDG_CACHE_HOME"))
|
||||||
|
if "XDG_CACHE_HOME" in os.environ
|
||||||
|
else Path.home() / ".cache"
|
||||||
|
)
|
||||||
|
|
||||||
games_dir = data_dir / "cartridges" / "games"
|
games_dir = data_dir / "cartridges" / "games"
|
||||||
covers_dir = data_dir / "cartridges" / "covers"
|
covers_dir = data_dir / "cartridges" / "covers"
|
||||||
|
|
||||||
backup_dir = cache_dir / "cartridges" / "backup"
|
|
||||||
backup_games_dir = backup_dir / games_dir.name
|
|
||||||
backup_covers_dir = backup_dir / covers_dir.name
|
|
||||||
|
|
||||||
appdata_dir = Path(os.getenv("appdata") or "C:\\Users\\Default\\AppData\\Roaming")
|
appdata_dir = Path(os.getenv("appdata") or "C:\\Users\\Default\\AppData\\Roaming")
|
||||||
programfiles32_dir = Path(os.getenv("programfiles(x86)") or "C:\\Program Files (x86)")
|
programfiles32_dir = Path(os.getenv("programfiles(x86)") or "C:\\Program Files (x86)")
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.game_cover import GameCover
|
from src.game_cover import GameCover
|
||||||
@@ -34,6 +32,7 @@ class DisplayManager(Manager):
|
|||||||
signals = {"update-ready"}
|
signals = {"update-ready"}
|
||||||
|
|
||||||
def manager_logic(self, game: Game, _additional_data: dict) -> None:
|
def manager_logic(self, game: Game, _additional_data: dict) -> None:
|
||||||
|
shared.win.games[game.game_id] = game
|
||||||
if game.get_parent():
|
if game.get_parent():
|
||||||
game.get_parent().get_parent().remove(game)
|
game.get_parent().get_parent().remove(game)
|
||||||
if game.get_parent():
|
if game.get_parent():
|
||||||
@@ -49,28 +48,27 @@ class DisplayManager(Manager):
|
|||||||
"notify::visible", game.toggle_play, None
|
"notify::visible", game.toggle_play, None
|
||||||
)
|
)
|
||||||
game.menu_button.get_popover().connect(
|
game.menu_button.get_popover().connect(
|
||||||
"notify::visible", shared.win.set_active_game, game
|
"notify::visible", game.win.set_active_game, game
|
||||||
)
|
)
|
||||||
|
|
||||||
if game.game_id in shared.win.game_covers:
|
if game.game_id in game.win.game_covers:
|
||||||
game.game_cover = shared.win.game_covers[game.game_id]
|
game.game_cover = game.win.game_covers[game.game_id]
|
||||||
game.game_cover.add_picture(game.cover)
|
game.game_cover.add_picture(game.cover)
|
||||||
else:
|
else:
|
||||||
game.game_cover = GameCover({game.cover}, game.get_cover_path())
|
game.game_cover = GameCover({game.cover}, game.get_cover_path())
|
||||||
shared.win.game_covers[game.game_id] = game.game_cover
|
game.win.game_covers[game.game_id] = game.game_cover
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shared.win.stack.get_visible_child() == shared.win.details_view
|
game.win.stack.get_visible_child() == game.win.details_view
|
||||||
and shared.win.active_game == game
|
and game.win.active_game == game
|
||||||
):
|
):
|
||||||
shared.win.show_details_view(game)
|
game.win.show_details_view(game)
|
||||||
|
|
||||||
if not game.removed and not game.blacklisted:
|
if not game.removed and not game.blacklisted:
|
||||||
logging.debug("Adding %s (%s) to the UI", game.name, game.game_id)
|
|
||||||
if game.hidden:
|
if game.hidden:
|
||||||
shared.win.hidden_library.append(game)
|
game.win.hidden_library.append(game)
|
||||||
else:
|
else:
|
||||||
shared.win.library.append(game)
|
game.win.library.append(game)
|
||||||
game.get_parent().set_focusable(False)
|
game.get_parent().set_focusable(False)
|
||||||
|
|
||||||
shared.win.set_library_child()
|
game.win.set_library_child()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# local_cover_manager.py
|
# local_cover_manager.py
|
||||||
#
|
#
|
||||||
# Copyright 2023 Geoffrey Coulaud
|
# Copyright 2023 Geoffrey Coulaud
|
||||||
# Copyright 2023 kramo
|
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -18,13 +17,12 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from gi.repository import GdkPixbuf
|
from pathlib import Path
|
||||||
|
|
||||||
from src import shared
|
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.store.managers.manager import Manager
|
from src.store.managers.manager import Manager
|
||||||
from src.store.managers.steam_api_manager import SteamAPIManager
|
from src.store.managers.steam_api_manager import SteamAPIManager
|
||||||
from src.utils.save_cover import resize_cover, save_cover
|
from src.utils.save_cover import save_cover, resize_cover
|
||||||
|
|
||||||
|
|
||||||
class LocalCoverManager(Manager):
|
class LocalCoverManager(Manager):
|
||||||
@@ -33,39 +31,12 @@ class LocalCoverManager(Manager):
|
|||||||
run_after = (SteamAPIManager,)
|
run_after = (SteamAPIManager,)
|
||||||
|
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
||||||
if image_path := additional_data.get("local_image_path"):
|
# Ensure that the cover path is in the additional data
|
||||||
if not image_path.is_file():
|
try:
|
||||||
return
|
image_path: Path = additional_data["local_image_path"]
|
||||||
save_cover(game.game_id, resize_cover(image_path))
|
except KeyError:
|
||||||
elif icon_path := additional_data.get("local_icon_path"):
|
return
|
||||||
cover_width, cover_height = shared.image_size
|
if not image_path.is_file():
|
||||||
|
return
|
||||||
dest_width = cover_width * 0.7
|
# Save the image
|
||||||
dest_height = cover_width * 0.7
|
save_cover(game.game_id, resize_cover(image_path))
|
||||||
|
|
||||||
dest_x = cover_width * 0.15
|
|
||||||
dest_y = (cover_height - dest_height) / 2
|
|
||||||
|
|
||||||
image = GdkPixbuf.Pixbuf.new_from_file(str(icon_path)).scale_simple(
|
|
||||||
dest_width, dest_height, GdkPixbuf.InterpType.BILINEAR
|
|
||||||
)
|
|
||||||
|
|
||||||
cover = image.scale_simple(
|
|
||||||
1, 2, GdkPixbuf.InterpType.BILINEAR
|
|
||||||
).scale_simple(cover_width, cover_height, GdkPixbuf.InterpType.BILINEAR)
|
|
||||||
|
|
||||||
image.composite(
|
|
||||||
cover,
|
|
||||||
dest_x,
|
|
||||||
dest_y,
|
|
||||||
dest_width,
|
|
||||||
dest_height,
|
|
||||||
dest_x,
|
|
||||||
dest_y,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
GdkPixbuf.InterpType.BILINEAR,
|
|
||||||
255,
|
|
||||||
)
|
|
||||||
|
|
||||||
save_cover(game.game_id, resize_cover(pixbuf=cover))
|
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ class Manager(ErrorProducer):
|
|||||||
except Exception as error: # pylint: disable=broad-exception-caught
|
except Exception as error: # pylint: disable=broad-exception-caught
|
||||||
handle_error(error)
|
handle_error(error)
|
||||||
|
|
||||||
logging.debug("Running %s for %s (%s)", self.name, game.name, game.game_id)
|
|
||||||
try_manager_logic()
|
try_manager_logic()
|
||||||
|
|
||||||
def process_game(
|
def process_game(
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class OnlineCoverManager(Manager):
|
|||||||
"""Manager that downloads game covers from URLs"""
|
"""Manager that downloads game covers from URLs"""
|
||||||
|
|
||||||
run_after = (LocalCoverManager,)
|
run_after = (LocalCoverManager,)
|
||||||
retryable_on = (HTTPError, SSLError, ConnectionError)
|
retryable_on = (HTTPError, SSLError)
|
||||||
|
|
||||||
def save_composited_cover(
|
def save_composited_cover(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from src.utils.steam import (
|
|||||||
class SteamAPIManager(AsyncManager):
|
class SteamAPIManager(AsyncManager):
|
||||||
"""Manager in charge of completing a game's data from the Steam API"""
|
"""Manager in charge of completing a game's data from the Steam API"""
|
||||||
|
|
||||||
retryable_on = (HTTPError, SSLError, ConnectionError)
|
retryable_on = (HTTPError, SSLError)
|
||||||
|
|
||||||
steam_api_helper: SteamAPIHelper = None
|
steam_api_helper: SteamAPIHelper = None
|
||||||
steam_rate_limiter: SteamRateLimiter = None
|
steam_rate_limiter: SteamRateLimiter = None
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
from gi.repository import Gdk, Gio, GLib
|
from gi.repository import Gio
|
||||||
from PIL import Image, ImageSequence, UnidentifiedImageError
|
from PIL import Image, ImageSequence, UnidentifiedImageError
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
@@ -63,13 +63,7 @@ def resize_cover(cover_path=None, pixbuf=None):
|
|||||||
else "webp",
|
else "webp",
|
||||||
)
|
)
|
||||||
except UnidentifiedImageError:
|
except UnidentifiedImageError:
|
||||||
try:
|
return None
|
||||||
Gdk.Texture.new_from_filename(str(cover_path)).save_to_tiff(
|
|
||||||
tmp_path := Gio.File.new_tmp("XXXXXX.tiff")[0].get_path()
|
|
||||||
)
|
|
||||||
return resize_cover(tmp_path)
|
|
||||||
except GLib.GError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return tmp_path
|
return tmp_path
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class SteamManifestData(TypedDict):
|
|||||||
class SteamAPIData(TypedDict):
|
class SteamAPIData(TypedDict):
|
||||||
"""Dict returned by SteamAPIHelper.get_api_data"""
|
"""Dict returned by SteamAPIHelper.get_api_data"""
|
||||||
|
|
||||||
developer: str
|
developers: str
|
||||||
|
|
||||||
|
|
||||||
class SteamRateLimiter(RateLimiter):
|
class SteamRateLimiter(RateLimiter):
|
||||||
@@ -148,5 +148,5 @@ class SteamAPIHelper:
|
|||||||
raise SteamNotAGameError()
|
raise SteamNotAGameError()
|
||||||
|
|
||||||
# Return API values we're interested in
|
# Return API values we're interested in
|
||||||
values = SteamAPIData(developer=", ".join(data["data"]["developers"]))
|
values = SteamAPIData(developers=", ".join(data["data"]["developers"]))
|
||||||
return values
|
return values
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
|||||||
hidden_search_entry = Gtk.Template.Child()
|
hidden_search_entry = Gtk.Template.Child()
|
||||||
hidden_search_button = Gtk.Template.Child()
|
hidden_search_button = Gtk.Template.Child()
|
||||||
|
|
||||||
|
games = {}
|
||||||
game_covers = {}
|
game_covers = {}
|
||||||
toasts = {}
|
toasts = {}
|
||||||
active_game = None
|
active_game = None
|
||||||
@@ -99,9 +100,6 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
|||||||
self.search_entry.connect("search-changed", self.search_changed, False)
|
self.search_entry.connect("search-changed", self.search_changed, False)
|
||||||
self.hidden_search_entry.connect("search-changed", self.search_changed, True)
|
self.hidden_search_entry.connect("search-changed", self.search_changed, True)
|
||||||
|
|
||||||
self.search_entry.connect("activate", self.show_details_view_search)
|
|
||||||
self.hidden_search_entry.connect("activate", self.show_details_view_search)
|
|
||||||
|
|
||||||
back_mouse_button = Gtk.GestureClick(button=8)
|
back_mouse_button = Gtk.GestureClick(button=8)
|
||||||
(back_mouse_button).connect("pressed", self.on_go_back_action)
|
(back_mouse_button).connect("pressed", self.on_go_back_action)
|
||||||
self.add_controller(back_mouse_button)
|
self.add_controller(back_mouse_button)
|
||||||
@@ -117,7 +115,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
|||||||
def set_library_child(self):
|
def set_library_child(self):
|
||||||
child, hidden_child = self.notice_empty, self.hidden_notice_empty
|
child, hidden_child = self.notice_empty, self.hidden_notice_empty
|
||||||
|
|
||||||
for game in shared.store.games.values():
|
for game in self.games.values():
|
||||||
if game.removed or game.blacklisted:
|
if game.removed or game.blacklisted:
|
||||||
continue
|
continue
|
||||||
if game.hidden:
|
if game.hidden:
|
||||||
@@ -305,29 +303,13 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
|||||||
search_entry.set_text("")
|
search_entry.set_text("")
|
||||||
|
|
||||||
def on_escape_action(self, *_args):
|
def on_escape_action(self, *_args):
|
||||||
if (
|
if self.stack.get_visible_child() == self.details_view:
|
||||||
|
self.on_go_back_action()
|
||||||
|
elif (
|
||||||
self.get_focus() == self.search_entry.get_focus_child()
|
self.get_focus() == self.search_entry.get_focus_child()
|
||||||
or self.hidden_search_entry.get_focus_child()
|
or self.hidden_search_entry.get_focus_child()
|
||||||
):
|
):
|
||||||
self.on_toggle_search_action()
|
self.on_toggle_search_action()
|
||||||
else:
|
|
||||||
self.on_go_back_action()
|
|
||||||
|
|
||||||
def show_details_view_search(self, widget):
|
|
||||||
library = (
|
|
||||||
self.hidden_library if widget == self.hidden_search_entry else self.library
|
|
||||||
)
|
|
||||||
index = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if not (child := library.get_child_at_index(index)):
|
|
||||||
break
|
|
||||||
|
|
||||||
if self.filter_func(child):
|
|
||||||
self.show_details_view(child.get_child())
|
|
||||||
break
|
|
||||||
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
def on_undo_action(self, _widget, game=None, undo=None):
|
def on_undo_action(self, _widget, game=None, undo=None):
|
||||||
if not game: # If the action was activated via Ctrl + Z
|
if not game: # If the action was activated via Ctrl + Z
|
||||||
|
|||||||
Reference in New Issue
Block a user