Compare commits

..

1 Commits

Author SHA1 Message Date
GeoffreyCoulaud
755b733023 🚧 Very unfinished initial work on yuzu source 2023-07-01 03:52:52 +02:00
29 changed files with 370 additions and 428 deletions

19
.github/workflows/flatpak-builder.yml vendored Normal file
View 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 }}

View File

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

View File

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

View File

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

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/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.
## Meson

View File

@@ -10,13 +10,16 @@
[![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/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-image]: https://img.shields.io/badge/code%20style-black-000000?style=flat
[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
- Manually adding and editing games
- Importing games from various sources:
- Steam
- Lutris
- Heroic
- Bottles
- itch
- Legendary
- Flatpak
- Importing games from Steam, Lutris, Heroic, Bottles and itch
- Support for multiple Steam install locations
- Hiding games
- Searching and sorting by title, date added and last played
- 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).
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).

View File

@@ -68,7 +68,7 @@
<default>true</default>
</key>
<key name="flatpak-location" type="s">
<default>"/var/lib/flatpak/"</default>
<default>"/var/lib/flatpak/exports/"</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@</id>
<id>@APP_ID@.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<name>Cartridges</name>

View File

@@ -97,6 +97,20 @@
"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-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"
"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:162
#: src/main.py:153
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:238
#: src/details_window.py:239
msgid "Preferences"
msgstr ""
@@ -106,7 +106,7 @@ msgstr ""
msgid "Edit"
msgstr ""
#: data/gtk/game.blp:107 src/window.py:171
#: data/gtk/game.blp:107 src/window.py:169
msgid "Hide"
msgstr ""
@@ -115,7 +115,7 @@ msgstr ""
msgid "Remove"
msgstr ""
#: data/gtk/game.blp:126 src/window.py:173
#: data/gtk/game.blp:126 src/window.py:171
msgid "Unhide"
msgstr ""
@@ -140,7 +140,7 @@ msgstr ""
msgid "Shortcuts"
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"
msgstr ""
@@ -168,7 +168,7 @@ msgstr ""
msgid "Remove game"
msgstr ""
#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268
#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:236
msgid "Behavior"
msgstr ""
@@ -217,9 +217,8 @@ msgid "Steam"
msgstr ""
#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110
#: 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
#: data/gtk/preferences.blp:142 data/gtk/preferences.blp:183
#: data/gtk/preferences.blp:197 data/gtk/preferences.blp:211
msgid "Install Location"
msgstr ""
@@ -235,71 +234,59 @@ msgstr ""
msgid "Import Steam Games"
msgstr ""
#: data/gtk/preferences.blp:137
msgid "Import Flatpak Games"
msgstr ""
#: data/gtk/preferences.blp:147
#: data/gtk/preferences.blp:138
msgid "Heroic"
msgstr ""
#: data/gtk/preferences.blp:160
#: data/gtk/preferences.blp:151
msgid "Import Epic Games"
msgstr ""
#: data/gtk/preferences.blp:169
#: data/gtk/preferences.blp:160
msgid "Import GOG Games"
msgstr ""
#: data/gtk/preferences.blp:178
#: data/gtk/preferences.blp:169
msgid "Import Sideloaded Games"
msgstr ""
#: data/gtk/preferences.blp:188
#: data/gtk/preferences.blp:179
msgid "Bottles"
msgstr ""
#: data/gtk/preferences.blp:202
#: data/gtk/preferences.blp:193
msgid "itch"
msgstr ""
#: data/gtk/preferences.blp:216
#: data/gtk/preferences.blp:207
msgid "Legendary"
msgstr ""
#: data/gtk/preferences.blp:230
msgid "Flatpak"
msgstr ""
#: data/gtk/preferences.blp:243
msgid "Import Game Launchers"
msgstr ""
#: data/gtk/preferences.blp:256
#: data/gtk/preferences.blp:224
msgid "SteamGridDB"
msgstr ""
#: data/gtk/preferences.blp:260
#: data/gtk/preferences.blp:228
msgid "Authentication"
msgstr ""
#: data/gtk/preferences.blp:263
#: data/gtk/preferences.blp:231
msgid "API Key"
msgstr ""
#: data/gtk/preferences.blp:271
#: data/gtk/preferences.blp:239
msgid "Use SteamGridDB"
msgstr ""
#: data/gtk/preferences.blp:272
#: data/gtk/preferences.blp:240
msgid "Download images when adding or importing games"
msgstr ""
#: data/gtk/preferences.blp:281
#: data/gtk/preferences.blp:249
msgid "Prefer Over Official Images"
msgstr ""
#: data/gtk/preferences.blp:290
#: data/gtk/preferences.blp:258
msgid "Prefer Animated Images"
msgstr ""
@@ -388,21 +375,21 @@ msgid "About Cartridges"
msgstr ""
#. 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"
msgstr ""
#. The variable is the date when the game was added
#: src/window.py:194
#: src/window.py:192
msgid "Added: {}"
msgstr ""
#: src/window.py:197
#: src/window.py:195
msgid "Never"
msgstr ""
#. The variable is the date when the game was last played
#: src/window.py:201
#: src/window.py:199
msgid "Last played: {}"
msgstr ""
@@ -455,68 +442,46 @@ msgstr ""
msgid "Couldn't Add Game"
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."
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."
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"
msgstr ""
#. The variable is the title of the game
#: src/game.py:138
#: src/game.py:141
msgid "{} launched"
msgstr ""
#. The variable is the title of the game
#: src/game.py:151
#: src/game.py:154
msgid "{} hidden"
msgstr ""
#: src/game.py:151
#: src/game.py:154
msgid "{} unhidden"
msgstr ""
#: src/game.py:168
#: src/game.py:171
msgid "{} removed"
msgstr ""
#: src/preferences.py:111
#: src/preferences.py:102
msgid "All games removed"
msgstr ""
#: src/preferences.py:159
#: src/preferences.py:149
msgid ""
"An API key is required to use SteamGridDB. You can generate one {}here{}."
msgstr ""
#: 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
#: src/preferences.py:289
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(-1, -1)
self.exec_info_label.select_region(0, 0)
self.exec_info_popover.connect("show", clear_info_selection)

View File

@@ -18,9 +18,7 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
import json
import logging
from shutil import copytree, rmtree
from gi.repository import Adw, GLib, Gtk
@@ -28,15 +26,8 @@ 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
@@ -62,20 +53,6 @@ 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):
@@ -108,47 +85,12 @@ class Importer(ErrorProducer):
and len(self.game_pipelines) == self.n_pipelines_done
)
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 add_source(self, source):
self.sources.add(source)
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
@@ -346,17 +288,13 @@ class Importer(ErrorProducer):
"open_preferences",
"import",
)
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)
)
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))
shared.win.toast_overlay.add_toast(toast)
return toast
@@ -377,7 +315,5 @@ 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,14 +86,13 @@ class BottlesSource(URLExecutableSource):
name = "Bottles"
iterator_class = BottlesSourceIterator
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
available_on = {"linux"}
available_on = set(("linux",))
data_location = Location(
schema_key="bottles-location",
candidates=(
shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles",
"~/.var/app/com.usebottles.bottles/data/bottles/",
shared.data_dir / "bottles/",
shared.home / ".local" / "share" / "bottles",
),
paths={
"library.yml": (False, "library.yml"),

View File

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

View File

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

View File

@@ -82,14 +82,13 @@ class ItchSource(URLExecutableSource):
name = "Itch"
iterator_class = ItchSourceIterator
url_format = "itch://caves/{cave_id}/launch"
available_on = {"linux", "win32"}
available_on = set(("linux", "win32"))
config_location = Location(
schema_key="itch-location",
candidates=(
shared.flatpak_dir / "io.itch.itch" / "config" / "itch",
"~/.var/app/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,15 +92,12 @@ class LegendarySourceIterator(SourceIterator):
class LegendarySource(Source):
name = "Legendary"
executable_format = "legendary launch {app_name}"
available_on = {"linux", "win32"}
available_on = set(("linux", "win32"))
iterator_class = LegendarySourceIterator
config_location: Location = Location(
data_location: Location = Location(
schema_key="legendary-location",
candidates=(
shared.config_dir / "legendary",
shared.home / ".config" / "legendary",
),
candidates=(shared.config_dir / "legendary",),
paths={
"installed.json": (False, "installed.json"),
"metadata": (True, "metadata"),

View File

@@ -93,16 +93,15 @@ class LutrisSource(URLExecutableSource):
name = "Lutris"
iterator_class = LutrisSourceIterator
url_format = "lutris:rungameid/{game_id}"
available_on = {"linux"}
available_on = set(("linux",))
# FIXME possible bug: location picks ~/.var... and cache_lcoation picks ~/.local...
data_location = Location(
schema_key="lutris-location",
candidates=(
shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris",
"~/.var/app/net.lutris.Lutris/data/lutris/",
shared.data_dir / "lutris",
shared.home / ".local" / "share" / "lutris",
),
paths={
"pga.db": (False, "pga.db"),
@@ -112,9 +111,8 @@ class LutrisSource(URLExecutableSource):
cache_location = Location(
schema_key="lutris-cache-location",
candidates=(
shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris",
"~/.var/app/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 = {"linux", "win32"}
available_on = set(("linux", "win32"))
iterator_class = SteamSourceIterator
url_format = "steam://rungameid/{game_id}"
data_location = Location(
schema_key="steam-location",
candidates=(
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
"~/.var/app/com.valvesoftware.Steam/data/Steam/",
shared.data_dir / "Steam",
shared.home / ".steam",
"~/.steam",
shared.programfiles32_dir / "Steam",
),
paths={

View File

@@ -0,0 +1,115 @@
# lutris_source.py
#
# Copyright 2022-2023 kramo
# Copyright 2023 Geoffrey Coulaud
#
# 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 time import time
from typing import Generator, Iterable
from configparser import ConfigParser
from pathlib import Path
import os
from src import shared
from src.game import Game
from src.importer.sources.location import Location
from src.importer.sources.source import (
SourceIterationResult,
SourceIterator,
Source,
)
class YuzuSourceIterator(SourceIterator):
source: "YuzuSource"
extensions = (".xci", ".nsp", ".nso", ".nro")
def iter_game_dirs(self) -> Iterable[tuple[bool, Path]]:
"""
Get the rom directories from the parsed config
The returned tuple indicates if the dir should be scanned recursively,
then its path.
"""
# Get the config data
config = ConfigParser()
if not config.read(
self.source.data_location["qt-config.ini"], encoding="utf-8"
):
return
# Iterate through the dirs
n_dirs = config.getint("UI", r"Paths\gamedirs\size", fallback=0)
for i in range(1, n_dirs + 1):
deep = config.getboolean(
"UI", f"Paths\\gamedirs\\{i}\\deep_scan", fallback=False
)
path = Path(config.get("UI", f"Paths\\gamedirs\\{i}\\path", fallback=None))
if path is None:
continue
yield deep, path
def iter_rom_files(
self, root: Path, recursive: bool = False
) -> Generator[Path, None, None]:
"""Generator method to iterate through rom files"""
if not recursive:
for path in root.iterdir():
if not path.is_file():
continue
if not path.suffix in self.extensions:
continue
yield path
else:
for dir_path, _dirs, file_names in os.walk(root):
for filename in file_names:
path = Path(dir_path) / filename
if path.suffix in self.extensions:
continue
yield path
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
"""Generator method producing games"""
added_time = int(time())
# Get the games
for recursive_search, game_dir in self.iter_game_dirs():
for path in self.iter_rom_files(game_dir, recursive_search):
values = {
# TODO add game_id
"added": added_time,
"source": self.source.id,
"executable": f"yuzu {str(path)}", # HACK change depending on the variant
}
game = Game(values)
additional_data = {}
yield game, additional_data
class YuzuSource(Source):
config_location = Location(
"yuzu-location",
(
"~/.var/app/org.yuzu_emu.yuzu/config/yuzu",
shared.config_dir / "yuzu",
"~/.config/yuzu",
# TODO windows path
),
{"qt-config.ini": (False, "qt-config.ini")},
)

View File

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

View File

@@ -17,6 +17,7 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
import json
import lzma
import sys
@@ -30,7 +31,15 @@ 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
@@ -74,16 +83,17 @@ class CartridgesApplication(Adw.Application):
"is-maximized", self.win, "maximized", Gio.SettingsBindFlags.DEFAULT
)
# Add managers to the store for game imports
# Load games from disk
shared.store.add_manager(FileManager(), False)
shared.store.add_manager(DisplayManager())
shared.store.add_manager(FileManager())
self.load_games_from_disk()
# Add rest of the managers for game imports
shared.store.add_manager(LocalCoverManager())
shared.store.add_manager(SteamAPIManager())
shared.store.add_manager(OnlineCoverManager())
shared.store.add_manager(SGDBManager())
# Load games from disk
Importer().load_games_from_disk()
shared.store.enable_manager_in_pipelines(FileManager)
# Create actions
self.create_actions(
@@ -124,6 +134,13 @@ 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 = ""
@@ -190,7 +207,30 @@ class CartridgesApplication(Adw.Application):
DetailsWindow()
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("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()
def on_remove_game_action(self, *_args):
self.win.active_game.remove_game()

View File

@@ -30,7 +30,6 @@ 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
@@ -100,7 +99,6 @@ class PreferencesWindow(Adw.PreferencesWindow):
remove_all_games_button = Gtk.Template.Child()
removed_games = set()
warning_menu_buttons = {}
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -143,7 +141,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
source = source_class()
if not source.is_available:
expander_row = getattr(self, f"{source.id}_expander_row")
expander_row.set_visible(False)
expander_row.remove()
else:
self.init_source_row(source)
@@ -253,65 +251,15 @@ 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 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))
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"""
@@ -333,30 +281,20 @@ 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 = _("Invalid Directory")
# The variable is the name of the source
subtitle_format = _("Select the {} cache directory.")
title = "Cache directory not found"
subtitle_format = "Select the {} cache directory."
else:
title = _("Invalid Directory")
# The variable is the name of the source
subtitle_format = _("Select the {} installation directory.")
title = "Installation directory not found"
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"),
)
@@ -383,5 +321,4 @@ 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, GLib
from gi.repository import Gdk, Gio
APP_ID = "@APP_ID@"
VERSION = "@VERSION@"
@@ -31,19 +31,25 @@ 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")
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"
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"
)
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,9 +17,6 @@
#
# 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
@@ -49,28 +46,27 @@ class DisplayManager(Manager):
"notify::visible", game.toggle_play, None
)
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:
game.game_cover = shared.win.game_covers[game.game_id]
if game.game_id in game.win.game_covers:
game.game_cover = game.win.game_covers[game.game_id]
game.game_cover.add_picture(game.cover)
else:
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 (
shared.win.stack.get_visible_child() == shared.win.details_view
and shared.win.active_game == game
game.win.stack.get_visible_child() == game.win.details_view
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:
logging.debug("Adding %s (%s) to the UI", game.name, game.game_id)
if game.hidden:
shared.win.hidden_library.append(game)
game.win.hidden_library.append(game)
else:
shared.win.library.append(game)
game.win.library.append(game)
game.get_parent().set_focusable(False)
shared.win.set_library_child()
game.win.set_library_child()

View File

@@ -110,7 +110,6 @@ 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, ConnectionError)
retryable_on = (HTTPError, SSLError)
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, ConnectionError)
retryable_on = (HTTPError, SSLError)
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"""
developer: str
developers: str
class SteamRateLimiter(RateLimiter):
@@ -148,5 +148,5 @@ class SteamAPIHelper:
raise SteamNotAGameError()
# Return API values we're interested in
values = SteamAPIData(developer=", ".join(data["data"]["developers"]))
values = SteamAPIData(developers=", ".join(data["data"]["developers"]))
return values