Compare commits

...

40 Commits

Author SHA1 Message Date
GeoffreyCoulaud
927275cda9 🚧 Prototype import undo v2
bad:
- Not optimised at all
- copies a lot of unnecessary things
- not reliable

good:
- works, a little

---

For it to work more than "a little", we need to fix that bug:
- Import
- Remove a game
- Import (game comes back)
- Undo Import (game gone)
- Import
- Undo (everything gone)
2023-07-03 17:48:32 +02:00
GeoffreyCoulaud
df9f7dbca8 Revert "🚧 Very unfinished initial work on import undo"
This reverts commit d252b6b97d.
2023-07-03 14:00:23 +02:00
GeoffreyCoulaud
6c6fd29faa Merge branch 'main' into add-import-undo 2023-07-03 13:58:22 +02:00
kramo
e320e58ffc Fix Appstream ID 2023-07-03 12:43:33 +02:00
kramo
c9a1104b44 Revert test 2023-07-02 14:46:54 +02:00
kramo
ee5740c21c Test Actions 2023-07-02 14:46:37 +02:00
kramo
1498326f45 Exit differently 2023-07-02 13:03:34 +02:00
kramo
69fc7e1b03 Remove test module 2023-07-02 12:55:19 +02:00
kramo
8a3397bef5 This should work 2023-07-02 12:54:49 +02:00
kramo
d87048ee64 Test worked 2023-07-02 12:26:56 +02:00
kramo
3d6601238d Actions Test 2023-07-02 12:21:55 +02:00
kramo
a7393ba9b9 Rename Windows Action 2023-07-02 01:25:08 +02:00
kramo
c2f429a29c Fix Flatpak action caching 2023-07-02 00:57:54 +02:00
kramo
8fadcc8524 oops 2023-07-02 00:53:20 +02:00
kramo
c544901519 I'm one with the cloud ☁️ (#130) 2023-07-02 00:51:28 +02:00
GeoffreyCoulaud
d252b6b97d 🚧 Very unfinished initial work on import undo 2023-07-02 00:08:00 +02:00
kramo
7756f75bb9 Test Release Action 2023-07-01 20:18:26 +02:00
Geoffrey Coulaud
fabd9828f6 Merge pull request #129 from kra-mo/online-managers-improvements
Retry all online managers on ConnectionError
2023-07-01 18:52:26 +02:00
GeoffreyCoulaud
29e022327b Retry all online managers on ConnectionError 2023-07-01 18:51:50 +02:00
Geoffrey Coulaud
49fb3705e4 Merge pull request #128 from kra-mo/windows-logging-fix
Windows fixes
2023-07-01 18:29:47 +02:00
GeoffreyCoulaud
0c6cfc14f0 Fix hiding unavailable source rows 2023-07-01 18:27:11 +02:00
GeoffreyCoulaud
75a5255806 Sepcified utf-8 for lzma log backups 2023-07-01 18:14:10 +02:00
Geoffrey Coulaud
dd1dd2b7e5 Merge pull request #127 from kra-mo/sources-paths-fix
Sources paths fix
2023-07-01 17:10:47 +02:00
GeoffreyCoulaud
4a204442b5 Using new shared paths in sources 2023-07-01 15:53:52 +02:00
GeoffreyCoulaud
fb0c47c1f1 Re added hardcoded candidates for linux
- Real fix would be to get the XDG_*_DIR
2023-07-01 15:17:12 +02:00
GeoffreyCoulaud
3af968fee7 Simplified source available on 2023-07-01 15:10:40 +02:00
kramo
9fbb45cfa5 Fix Steam API developers 2023-07-01 14:30:26 +02:00
kramo
bbfe478ac3 Fix Legendary location labels 2023-07-01 13:58:47 +02:00
kramo
339ec1c20a Update translations 2023-07-01 13:48:30 +02:00
kramo
97e40e0a80 Fix Legendary, warning for missing locations 2023-07-01 13:44:14 +02:00
kramo
95d47815ab Add translation comments 2023-07-01 11:36:45 +02:00
kramo
772bf30468 Fix translations 2023-07-01 11:35:17 +02:00
kramo
7311015549 Fix translations 2023-07-01 11:30:44 +02:00
kramo
e7d2f58416 Merge pull request #126 from kra-mo/kra-mo-patch-1
Update README.md
2023-07-01 11:23:46 +02:00
kramo
a040b058d2 Update README.md 2023-07-01 11:23:38 +02:00
kramo
7be20c64bd Update translations 2023-07-01 11:15:55 +02:00
kramo
036e5814f3 Fix Flatpak custom paths 2023-07-01 11:11:16 +02:00
kramo
e46c9b6a30 Fix Flatpak desktop entry search 2023-07-01 10:54:48 +02:00
kramo
495755f278 More reliance on GLib 2023-07-01 10:44:15 +02:00
kramo
1f25bed842 Move away from PyXDG 2023-07-01 10:05:16 +02:00
28 changed files with 428 additions and 255 deletions

View File

@@ -2,20 +2,40 @@ on:
push:
branches: [main]
pull_request:
name: "Build for Windows"
name: CI
concurrency:
group: release-${{ github.sha }}
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:
name: "Build"
name: Windows
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: MSYS2
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
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
- name: Compile
shell: msys2 {0}
run: |
@@ -23,10 +43,18 @@ jobs:
ninja -C _build install
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
- name: "Inno Setup"
- name: Test
shell: msys2 {0}
run: |
set +e
timeout 2 cartridges; [ "$?" -eq "124" ]
- name: Inno Setup
run: iscc ".\_build\Cartridges.iss"
- name: "Upload Artifact"
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: "Installer"
path: "_build/Output/Cartridges Setup.exe"
name: Windows Installer
path: _build/Output/Cartridges Setup.exe

View File

@@ -1,19 +0,0 @@
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 Normal file
View File

@@ -0,0 +1,45 @@
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

View File

@@ -2,9 +2,9 @@
ignore=importers
[MESSAGES CONTROL]
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,

View File

@@ -23,7 +23,7 @@ The project can be translated on [Weblate](https://hosted.weblate.org/engage/car
## For Windows
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/windows.yml).
2. From the MSYS2 shell, install the required dependencies listed [here](https://github.com/kra-mo/cartridges/blob/main/.github/workflows/ci.yml).
3. Build it via Meson.
## Meson

View File

@@ -10,16 +10,13 @@
[![Flathub][flathub-image]][flathub-url]
[![Build status][github-actions-image]][github-actions-url]
[![Translation Status][weblate-image]][weblate-url]
[![License][license-image]][license-url]
[![Code style][code-style-image]][code-style-url]
[![Discord][discord-image]][discord-url]
[circle-url]: https://circle.gnome.org
[circle-image]: https://circle.gnome.org/assets/button/badge.svg
[github-actions-url]: https://github.com/kra-mo/cartridges
[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
[github-actions-image]: https://github.com/kra-mo/cartridges/actions/workflows/ci.yml/badge.svg
[code-style-url]: https://github.com/psf/black
[code-style-image]: https://img.shields.io/badge/code%20style-black-000000?style=flat
[weblate-url]: https://hosted.weblate.org/engage/cartridges/
@@ -39,8 +36,14 @@ Cartridges is a simple game launcher written in Python using GTK4 and Libadwaita
## Features
- Manually adding and editing games
- Importing games from Steam, Lutris, Heroic, Bottles and itch
- Support for multiple Steam install locations
- Importing games from various sources:
- Steam
- Lutris
- Heroic
- Bottles
- itch
- Legendary
- Flatpak
- Hiding games
- Searching and sorting by title, date added and last played
- Automatically downloading cover art from [SteamGridDB](https://www.steamgriddb.com/)

View File

@@ -68,7 +68,7 @@
<default>true</default>
</key>
<key name="flatpak-location" type="s">
<default>"/var/lib/flatpak/exports/"</default>
<default>"/var/lib/flatpak/"</default>
</key>
<key name="flatpak-import-launchers" type="b">
<default>false</default>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>@APP_ID@.desktop</id>
<id>@APP_ID@</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<name>Cartridges</name>

View File

@@ -97,20 +97,6 @@
"sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"
}
]
},
{
"name": "python3-pyxdg",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pyxdg\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/e5/8d/cf41b66a8110670e3ad03dab9b759704eeed07fa96e90fdc0357b2ba70e2/pyxdg-0.28-py2.py3-none-any.whl",
"sha256": "bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"
}
]
}
]
},

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Cartridges\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-26 22:22+0200\n"
"POT-Creation-Date: 2023-07-01 13:45+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -19,7 +19,7 @@ msgstr ""
#: data/hu.kramo.Cartridges.desktop.in:3
#: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47
#: src/main.py:153
#: src/main.py:162
msgid "Cartridges"
msgstr ""
@@ -57,7 +57,7 @@ msgid "Game Details"
msgstr ""
#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416
#: src/details_window.py:239
#: src/details_window.py:238
msgid "Preferences"
msgstr ""
@@ -106,7 +106,7 @@ msgstr ""
msgid "Edit"
msgstr ""
#: data/gtk/game.blp:107 src/window.py:169
#: data/gtk/game.blp:107 src/window.py:171
msgid "Hide"
msgstr ""
@@ -115,7 +115,7 @@ msgstr ""
msgid "Remove"
msgstr ""
#: data/gtk/game.blp:126 src/window.py:171
#: data/gtk/game.blp:126 src/window.py:173
msgid "Unhide"
msgstr ""
@@ -140,7 +140,7 @@ msgstr ""
msgid "Shortcuts"
msgstr ""
#: data/gtk/help-overlay.blp:34 src/game.py:105 src/preferences.py:103
#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112
msgid "Undo"
msgstr ""
@@ -168,7 +168,7 @@ msgstr ""
msgid "Remove game"
msgstr ""
#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:236
#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268
msgid "Behavior"
msgstr ""
@@ -217,8 +217,9 @@ msgid "Steam"
msgstr ""
#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110
#: data/gtk/preferences.blp:142 data/gtk/preferences.blp:183
#: data/gtk/preferences.blp:197 data/gtk/preferences.blp:211
#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192
#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220
#: data/gtk/preferences.blp:234
msgid "Install Location"
msgstr ""
@@ -234,59 +235,71 @@ msgstr ""
msgid "Import Steam Games"
msgstr ""
#: data/gtk/preferences.blp:138
#: data/gtk/preferences.blp:137
msgid "Import Flatpak Games"
msgstr ""
#: data/gtk/preferences.blp:147
msgid "Heroic"
msgstr ""
#: data/gtk/preferences.blp:151
#: data/gtk/preferences.blp:160
msgid "Import Epic Games"
msgstr ""
#: data/gtk/preferences.blp:160
#: data/gtk/preferences.blp:169
msgid "Import GOG Games"
msgstr ""
#: data/gtk/preferences.blp:169
#: data/gtk/preferences.blp:178
msgid "Import Sideloaded Games"
msgstr ""
#: data/gtk/preferences.blp:179
#: data/gtk/preferences.blp:188
msgid "Bottles"
msgstr ""
#: data/gtk/preferences.blp:193
#: data/gtk/preferences.blp:202
msgid "itch"
msgstr ""
#: data/gtk/preferences.blp:207
#: data/gtk/preferences.blp:216
msgid "Legendary"
msgstr ""
#: data/gtk/preferences.blp:224
#: data/gtk/preferences.blp:230
msgid "Flatpak"
msgstr ""
#: data/gtk/preferences.blp:243
msgid "Import Game Launchers"
msgstr ""
#: data/gtk/preferences.blp:256
msgid "SteamGridDB"
msgstr ""
#: data/gtk/preferences.blp:228
#: data/gtk/preferences.blp:260
msgid "Authentication"
msgstr ""
#: data/gtk/preferences.blp:231
#: data/gtk/preferences.blp:263
msgid "API Key"
msgstr ""
#: data/gtk/preferences.blp:239
#: data/gtk/preferences.blp:271
msgid "Use SteamGridDB"
msgstr ""
#: data/gtk/preferences.blp:240
#: data/gtk/preferences.blp:272
msgid "Download images when adding or importing games"
msgstr ""
#: data/gtk/preferences.blp:249
#: data/gtk/preferences.blp:281
msgid "Prefer Over Official Images"
msgstr ""
#: data/gtk/preferences.blp:258
#: data/gtk/preferences.blp:290
msgid "Prefer Animated Images"
msgstr ""
@@ -375,21 +388,21 @@ msgid "About Cartridges"
msgstr ""
#. Translators: Replace this with your name for it to show up in the about window
#: src/main.py:171
#: src/main.py:180
msgid "translator_credits"
msgstr ""
#. The variable is the date when the game was added
#: src/window.py:192
#: src/window.py:194
msgid "Added: {}"
msgstr ""
#: src/window.py:195
#: src/window.py:197
msgid "Never"
msgstr ""
#. The variable is the date when the game was last played
#: src/window.py:199
#: src/window.py:201
msgid "Last played: {}"
msgstr ""
@@ -442,46 +455,68 @@ msgstr ""
msgid "Couldn't Add Game"
msgstr ""
#: src/details_window.py:146 src/details_window.py:181
#: src/details_window.py:146 src/details_window.py:180
msgid "Game title cannot be empty."
msgstr ""
#: src/details_window.py:152 src/details_window.py:189
#: src/details_window.py:152 src/details_window.py:188
msgid "Executable cannot be empty."
msgstr ""
#: src/details_window.py:180 src/details_window.py:188
#: src/details_window.py:179 src/details_window.py:187
msgid "Couldn't Apply Preferences"
msgstr ""
#. The variable is the title of the game
#: src/game.py:141
#: src/game.py:138
msgid "{} launched"
msgstr ""
#. The variable is the title of the game
#: src/game.py:154
#: src/game.py:151
msgid "{} hidden"
msgstr ""
#: src/game.py:154
#: src/game.py:151
msgid "{} unhidden"
msgstr ""
#: src/game.py:171
#: src/game.py:168
msgid "{} removed"
msgstr ""
#: src/preferences.py:102
#: src/preferences.py:111
msgid "All games removed"
msgstr ""
#: src/preferences.py:149
#: src/preferences.py:159
msgid ""
"An API key is required to use SteamGridDB. You can generate one {}here{}."
msgstr ""
#: src/preferences.py:289
#: src/preferences.py:284
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"
msgstr ""

View File

@@ -114,7 +114,7 @@ class DetailsWindow(Adw.Window):
self.exec_info_label.set_label(exec_info_text)
def clear_info_selection(*_args):
self.exec_info_label.select_region(0, 0)
self.exec_info_label.select_region(-1, -1)
self.exec_info_popover.connect("show", clear_info_selection)

View File

@@ -18,7 +18,9 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
import json
import logging
from shutil import copytree, rmtree
from gi.repository import Adw, GLib, Gtk
@@ -26,8 +28,15 @@ from src import shared
from src.errors.error_producer import ErrorProducer
from src.errors.friendly_error import FriendlyError
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.steam_source import SteamSource
from src.store.managers.async_manager import AsyncManager
from src.store.pipeline import Pipeline
from src.utils.task import Task
@@ -53,6 +62,20 @@ class Importer(ErrorProducer):
super().__init__()
self.game_pipelines = 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
def n_games_added(self):
@@ -85,12 +108,47 @@ class Importer(ErrorProducer):
and len(self.game_pipelines) == self.n_pipelines_done
)
def add_source(self, source):
self.sources.add(source)
def load_games_from_disk(self):
"""Load the games from disk"""
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):
"""Use several Gio.Task to import games from added sources"""
self.create_backup()
self.create_dialog()
# Collect all errors and reset the cancellables for the managers
@@ -288,13 +346,17 @@ class Importer(ErrorProducer):
"open_preferences",
"import",
)
elif self.n_games_added == 1:
toast.set_title(_("1 game imported"))
elif self.n_games_added > 1:
# The variable is the number of games
toast.set_title(_("{} games imported").format(self.n_games_added))
else:
toast.set_button_label(_("Undo"))
toast.connect(
"button-clicked", self.dialog_response_callback, "undo_import"
)
toast.set_title(
_("1 game imported")
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)
return toast
@@ -315,5 +377,7 @@ class Importer(ErrorProducer):
self.open_preferences(*args)
elif response == "open_preferences_import":
self.open_preferences(*args).connect("close-request", self.timeout_toast)
elif response == "undo_import":
self.restore_backup()
else:
self.timeout_toast()

View File

@@ -86,13 +86,14 @@ class BottlesSource(URLExecutableSource):
name = "Bottles"
iterator_class = BottlesSourceIterator
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
available_on = set(("linux",))
available_on = {"linux"}
data_location = Location(
schema_key="bottles-location",
candidates=(
"~/.var/app/com.usebottles.bottles/data/bottles/",
shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles",
shared.data_dir / "bottles/",
shared.home / ".local" / "share" / "bottles",
),
paths={
"library.yml": (False, "library.yml"),

View File

@@ -17,11 +17,10 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
import re
from pathlib import Path
from time import time
import subprocess
from xdg import IconTheme
from gi.repository import GLib, Gtk
from src import shared
from src.game import Game
@@ -37,64 +36,48 @@ class FlatpakSourceIterator(SourceIterator):
added_time = int(time())
IconTheme.icondirs.append(self.source.data_location["icons"])
icon_theme = Gtk.IconTheme.new()
icon_theme.add_search_path(str(self.source.data_location["icons"]))
try:
process = subprocess.run(
("flatpak-spawn", "--host", "flatpak", "list", "--columns=application"),
capture_output=True,
encoding="utf-8",
check=True,
)
flatpak_ids = process.stdout.split("\n")
to_remove = (
{"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 item in to_remove:
if item in flatpak_ids:
flatpak_ids.remove(item)
except subprocess.CalledProcessError:
return
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():
flatpak_id = entry.stem
if flatpak_id not in flatpak_ids:
if entry.suffix != ".desktop":
continue
with entry.open("r", encoding="utf-8") as open_file:
string = open_file.read()
keyfile = GLib.KeyFile.new()
desktop_values = {"Name": None, "Icon": None, "Categories": None}
for key in desktop_values:
if regex := re.findall(f"{key}=(.*)\n", string):
desktop_values[key] = regex[0]
try:
keyfile.load_from_file(str(entry), 0)
if not desktop_values["Name"]:
continue
if "Game" not in keyfile.get_string_list("Desktop Entry", "Categories"):
continue
if not desktop_values["Categories"]:
continue
if (
flatpak_id := keyfile.get_string("Desktop Entry", "X-Flatpak")
) in blacklist or flatpak_id != entry.stem:
continue
if not "Game" in desktop_values["Categories"].split(";"):
name = keyfile.get_string("Desktop Entry", "Name")
except GLib.GError:
continue
values = {
"source": self.source.id,
"added": added_time,
"name": desktop_values["Name"],
"name": name,
"game_id": self.source.game_id_format.format(game_id=flatpak_id),
"executable": self.source.executable_format.format(
flatpak_id=flatpak_id
@@ -103,11 +86,25 @@ class FlatpakSourceIterator(SourceIterator):
game = Game(values)
additional_data = {}
if icon_name := desktop_values["Icon"]:
if icon_path := IconTheme.getIconPath(icon_name, 512):
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)
@@ -119,16 +116,16 @@ class FlatpakSource(Source):
name = "Flatpak"
iterator_class = FlatpakSourceIterator
executable_format = "flatpak run {flatpak_id}"
available_on = set(("linux",))
available_on = {"linux"}
data_location = Location(
schema_key="flatpak-location",
candidates=(
"/var/lib/flatpak/exports/",
shared.data_dir / "flatpak" / "exports",
"/var/lib/flatpak/",
shared.data_dir / "flatpak",
),
paths={
"applications": (True, "share/applications"),
"icons": (True, "share/icons"),
"applications": (True, "exports/share/applications"),
"icons": (True, "exports/share/icons"),
},
)

View File

@@ -137,13 +137,14 @@ class HeroicSource(URLExecutableSource):
name = "Heroic"
iterator_class = HeroicSourceIterator
url_format = "heroic://launch/{app_name}"
available_on = set(("linux", "win32"))
available_on = {"linux", "win32"}
config_location = Location(
schema_key="heroic-location",
candidates=(
"~/.var/app/com.heroicgameslauncher.hgl/config/heroic/",
shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic",
shared.config_dir / "heroic",
shared.home / ".config" / "heroic",
shared.appdata_dir / "heroic",
),
paths={

View File

@@ -82,13 +82,14 @@ class ItchSource(URLExecutableSource):
name = "Itch"
iterator_class = ItchSourceIterator
url_format = "itch://caves/{cave_id}/launch"
available_on = set(("linux", "win32"))
available_on = {"linux", "win32"}
config_location = Location(
schema_key="itch-location",
candidates=(
"~/.var/app/io.itch.itch/config/itch/",
shared.flatpak_dir / "io.itch.itch" / "config" / "itch",
shared.config_dir / "itch",
shared.home / ".config" / "itch",
shared.appdata_dir / "itch",
),
paths={"butler.db": (False, "db/butler.db")},

View File

@@ -92,12 +92,15 @@ class LegendarySourceIterator(SourceIterator):
class LegendarySource(Source):
name = "Legendary"
executable_format = "legendary launch {app_name}"
available_on = set(("linux", "win32"))
available_on = {"linux", "win32"}
iterator_class = LegendarySourceIterator
data_location: Location = Location(
config_location: Location = Location(
schema_key="legendary-location",
candidates=(shared.config_dir / "legendary",),
candidates=(
shared.config_dir / "legendary",
shared.home / ".config" / "legendary",
),
paths={
"installed.json": (False, "installed.json"),
"metadata": (True, "metadata"),

View File

@@ -93,15 +93,16 @@ class LutrisSource(URLExecutableSource):
name = "Lutris"
iterator_class = LutrisSourceIterator
url_format = "lutris:rungameid/{game_id}"
available_on = set(("linux",))
available_on = {"linux"}
# FIXME possible bug: location picks ~/.var... and cache_lcoation picks ~/.local...
data_location = Location(
schema_key="lutris-location",
candidates=(
"~/.var/app/net.lutris.Lutris/data/lutris/",
shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris",
shared.data_dir / "lutris",
shared.home / ".local" / "share" / "lutris",
),
paths={
"pga.db": (False, "pga.db"),
@@ -111,8 +112,9 @@ class LutrisSource(URLExecutableSource):
cache_location = Location(
schema_key="lutris-cache-location",
candidates=(
"~/.var/app/net.lutris.Lutris/cache/lutris/",
shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris",
shared.cache_dir / "lutris",
shared.home / ".cache" / "lutris",
),
paths={
"coverart": (True, "coverart"),

View File

@@ -111,16 +111,16 @@ class SteamSourceIterator(SourceIterator):
class SteamSource(URLExecutableSource):
name = "Steam"
available_on = set(("linux", "win32"))
available_on = {"linux", "win32"}
iterator_class = SteamSourceIterator
url_format = "steam://rungameid/{game_id}"
data_location = Location(
schema_key="steam-location",
candidates=(
"~/.var/app/com.valvesoftware.Steam/data/Steam/",
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
shared.data_dir / "Steam",
"~/.steam",
shared.home / ".steam",
shared.programfiles32_dir / "Steam",
),
paths={

View File

@@ -88,16 +88,20 @@ class SessionFileHandler(StreamHandler):
# If uncompressed, compress
if not path.name.endswith(".xz"):
new_path = path.with_suffix(path.suffix + ".xz")
compressed_path = path.with_suffix(path.suffix + ".xz")
with (
lzma.open(
new_path, "wt", format=FORMAT_XZ, preset=PRESET_DEFAULT
compressed_path,
"wt",
format=FORMAT_XZ,
preset=PRESET_DEFAULT,
encoding="utf-8",
) as lzma_file,
open(path, "r", encoding="utf-8") as original_file,
):
lzma_file.write(original_file.read())
path.unlink()
path = new_path
path = compressed_path
# Rename with new number suffix
new_number = self.get_path_number(path) + 1

View File

@@ -17,7 +17,6 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
import json
import lzma
import sys
@@ -31,15 +30,7 @@ from gi.repository import Adw, Gio, GLib, Gtk
from src import shared
from src.details_window import DetailsWindow
from src.game import Game
from src.importer.importer import Importer
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.lutris_source import LutrisSource
from src.importer.sources.steam_source import SteamSource
from src.logging.setup import log_system_info, setup_logging
from src.preferences import PreferencesWindow
from src.store.managers.display_manager import DisplayManager
@@ -83,17 +74,16 @@ class CartridgesApplication(Adw.Application):
"is-maximized", self.win, "maximized", Gio.SettingsBindFlags.DEFAULT
)
# Load games from disk
shared.store.add_manager(FileManager(), False)
# Add managers to the store for game imports
shared.store.add_manager(DisplayManager())
self.load_games_from_disk()
# Add rest of the managers for game imports
shared.store.add_manager(FileManager())
shared.store.add_manager(LocalCoverManager())
shared.store.add_manager(SteamAPIManager())
shared.store.add_manager(OnlineCoverManager())
shared.store.add_manager(SGDBManager())
shared.store.enable_manager_in_pipelines(FileManager)
# Load games from disk
Importer().load_games_from_disk()
# Create actions
self.create_actions(
@@ -134,13 +124,6 @@ class CartridgesApplication(Adw.Application):
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)
shared.store.add_game(game, {"skip_save": True})
def on_about_action(self, *_args):
# Get the debug info from the log files
debug_str = ""
@@ -207,30 +190,7 @@ class CartridgesApplication(Adw.Application):
DetailsWindow()
def on_import_action(self, *_args):
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("flatpak"):
importer.add_source(FlatpakSource())
if shared.schema.get_boolean("itch"):
importer.add_source(ItchSource())
if shared.schema.get_boolean("legendary"):
importer.add_source(LegendarySource())
importer.run()
Importer().run()
def on_remove_game_action(self, *_args):
self.win.active_game.remove_game()

View File

@@ -30,6 +30,7 @@ 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.steam_source import SteamSource
@@ -99,6 +100,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
remove_all_games_button = Gtk.Template.Child()
removed_games = set()
warning_menu_buttons = {}
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -141,7 +143,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
source = source_class()
if not source.is_available:
expander_row = getattr(self, f"{source.id}_expander_row")
expander_row.remove()
expander_row.set_visible(False)
else:
self.init_source_row(source)
@@ -251,15 +253,65 @@ class PreferencesWindow(Adw.PreferencesWindow):
if not action_row:
continue
# Historically "location" meant data or config, so the key stays shared
infix = "-cache" if location == "cache" else ""
key = f"{source.id}{infix}-location"
path = Path(shared.schema.get_string(key)).expanduser()
# Remove the path if the dir is picked via the Flatpak portal
# Remove the path prefix if picked via Flatpak portal
subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path))
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):
"""Initialize a preference row for a source class"""
@@ -281,20 +333,30 @@ class PreferencesWindow(Adw.PreferencesWindow):
shared.schema.set_string(key, value)
# Update the row
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)
# Bad picked location, inform user
else:
if location_name == "cache":
title = "Cache directory not found"
subtitle_format = "Select the {} cache directory."
title = _("Invalid Directory")
# The variable is the name of the source
subtitle_format = _("Select the {} cache directory.")
else:
title = "Installation directory not found"
subtitle_format = "Select the {} installation directory."
title = _("Invalid Directory")
# The variable is the name of the source
subtitle_format = _("Select the {} installation directory.")
dialog = create_dialog(
self,
_(title),
_(subtitle_format).format(source.name),
title,
subtitle_format.format(source.name),
"choose_folder",
_("Set Location"),
)
@@ -321,4 +383,5 @@ class PreferencesWindow(Adw.PreferencesWindow):
button.connect("clicked", self.choose_folder, set_dir, location)
# Set the source row subtitles
self.resolve_locations(source)
self.update_source_action_row_paths(source)

View File

@@ -20,7 +20,7 @@
import os
from pathlib import Path
from gi.repository import Gdk, Gio
from gi.repository import Gdk, Gio, GLib
APP_ID = "@APP_ID@"
VERSION = "@VERSION@"
@@ -31,25 +31,19 @@ SPEC_VERSION = 1.5 # The version of the game_id.json spec
schema = Gio.Settings.new(APP_ID)
state_schema = Gio.Settings.new(APP_ID + ".State")
data_dir = (
Path(os.getenv("XDG_DATA_HOME"))
if "XDG_DATA_HOME" in os.environ
else Path.home() / ".local" / "share"
)
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"
)
home = Path.home()
data_dir = Path(GLib.get_user_data_dir())
config_dir = Path(GLib.get_user_config_dir())
cache_dir = Path(GLib.get_user_cache_dir())
flatpak_dir = home / ".var" / "app"
games_dir = data_dir / "cartridges" / "games"
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")
programfiles32_dir = Path(os.getenv("programfiles(x86)") or "C:\\Program Files (x86)")

View File

@@ -17,6 +17,9 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
from src import shared
from src.game import Game
from src.game_cover import GameCover
from src.store.managers.manager import Manager
@@ -46,27 +49,28 @@ class DisplayManager(Manager):
"notify::visible", game.toggle_play, None
)
game.menu_button.get_popover().connect(
"notify::visible", game.win.set_active_game, game
"notify::visible", shared.win.set_active_game, game
)
if game.game_id in game.win.game_covers:
game.game_cover = game.win.game_covers[game.game_id]
if game.game_id in shared.win.game_covers:
game.game_cover = shared.win.game_covers[game.game_id]
game.game_cover.add_picture(game.cover)
else:
game.game_cover = GameCover({game.cover}, game.get_cover_path())
game.win.game_covers[game.game_id] = game.game_cover
shared.win.game_covers[game.game_id] = game.game_cover
if (
game.win.stack.get_visible_child() == game.win.details_view
and game.win.active_game == game
shared.win.stack.get_visible_child() == shared.win.details_view
and shared.win.active_game == game
):
game.win.show_details_view(game)
shared.win.show_details_view(game)
if not game.removed and not game.blacklisted:
logging.debug("Adding %s (%s) to the UI", game.name, game.game_id)
if game.hidden:
game.win.hidden_library.append(game)
shared.win.hidden_library.append(game)
else:
game.win.library.append(game)
shared.win.library.append(game)
game.get_parent().set_focusable(False)
game.win.set_library_child()
shared.win.set_library_child()

View File

@@ -110,6 +110,7 @@ class Manager(ErrorProducer):
except Exception as error: # pylint: disable=broad-exception-caught
handle_error(error)
logging.debug("Running %s for %s (%s)", self.name, game.name, game.game_id)
try_manager_logic()
def process_game(

View File

@@ -36,7 +36,7 @@ class OnlineCoverManager(Manager):
"""Manager that downloads game covers from URLs"""
run_after = (LocalCoverManager,)
retryable_on = (HTTPError, SSLError)
retryable_on = (HTTPError, SSLError, ConnectionError)
def save_composited_cover(
self,

View File

@@ -32,7 +32,7 @@ from src.utils.steam import (
class SteamAPIManager(AsyncManager):
"""Manager in charge of completing a game's data from the Steam API"""
retryable_on = (HTTPError, SSLError)
retryable_on = (HTTPError, SSLError, ConnectionError)
steam_api_helper: SteamAPIHelper = None
steam_rate_limiter: SteamRateLimiter = None

View File

@@ -57,7 +57,7 @@ class SteamManifestData(TypedDict):
class SteamAPIData(TypedDict):
"""Dict returned by SteamAPIHelper.get_api_data"""
developers: str
developer: str
class SteamRateLimiter(RateLimiter):
@@ -148,5 +148,5 @@ class SteamAPIHelper:
raise SteamNotAGameError()
# Return API values we're interested in
values = SteamAPIData(developers=", ".join(data["data"]["developers"]))
values = SteamAPIData(developer=", ".join(data["data"]["developers"]))
return values