From 69928a8b4f6c883da76056a0bb0c579e06b7fa85 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Tue, 10 Oct 2023 22:47:32 +0200 Subject: [PATCH] Implement search provider (#201) * Begin work on search provider * Initial search provider work, organize meson * Initial work on icons * Implement LaunchSearch * Don't hold arbitrary reference to service I don't know why Lollypop does this * Send notification, pad images * Update translations * Fix init_search_term typing --- {src => cartridges}/__builtins__.pyi | 0 {src => cartridges}/cartridges.in | 30 +- {src => cartridges}/details_window.py | 16 +- {src => cartridges}/errors/error_producer.py | 0 {src => cartridges}/errors/friendly_error.py | 0 {src => cartridges}/game.py | 24 +- {src => cartridges}/game_cover.py | 2 +- .../importer/bottles_source.py | 8 +- .../importer/desktop_source.py | 6 +- .../importer/flatpak_source.py | 8 +- {src => cartridges}/importer/heroic_source.py | 8 +- {src => cartridges}/importer/importer.py | 16 +- {src => cartridges}/importer/itch_source.py | 10 +- .../importer/legendary_source.py | 8 +- {src => cartridges}/importer/location.py | 2 +- {src => cartridges}/importer/lutris_source.py | 10 +- .../importer/retroarch_source.py | 16 +- {src => cartridges}/importer/source.py | 4 +- {src => cartridges}/importer/steam_source.py | 10 +- .../logging/color_log_formatter.py | 0 .../logging/session_file_handler.py | 2 +- {src => cartridges}/logging/setup.py | 6 +- {src => cartridges}/main.py | 91 ++++-- {src => cartridges}/meson.build | 2 +- {src => cartridges}/preferences.py | 30 +- {src => cartridges}/shared.py.in | 17 +- .../store/managers/async_manager.py | 4 +- .../store/managers/cover_manager.py | 10 +- .../store/managers/display_manager.py | 12 +- .../store/managers/file_manager.py | 8 +- {src => cartridges}/store/managers/manager.py | 6 +- .../store/managers/sgdb_manager.py | 12 +- .../store/managers/steam_api_manager.py | 6 +- {src => cartridges}/store/pipeline.py | 4 +- {src => cartridges}/store/store.py | 8 +- {src => cartridges}/utils/create_dialog.py | 0 .../utils/migrate_files_v1_to_v2.py | 2 +- {src => cartridges}/utils/rate_limiter.py | 0 {src => cartridges}/utils/relative_date.py | 0 cartridges/utils/run_executable.py | 24 ++ {src => cartridges}/utils/save_cover.py | 2 +- {src => cartridges}/utils/sqlite.py | 0 {src => cartridges}/utils/steam.py | 4 +- {src => cartridges}/utils/steamgriddb.py | 6 +- {src => cartridges}/window.py | 8 +- meson.build | 14 +- po/POTFILES | 41 +-- po/cartridges.pot | 158 +++++----- search-provider/cartridges-search-provider.in | 292 ++++++++++++++++++ .../hu.kramo.Cartridges.SearchProvider.ini | 5 + ...kramo.Cartridges.SearchProvider.service.in | 3 + search-provider/meson.build | 25 ++ 52 files changed, 687 insertions(+), 293 deletions(-) rename {src => cartridges}/__builtins__.pyi (100%) rename {src => cartridges}/cartridges.in (72%) rename {src => cartridges}/details_window.py (96%) rename {src => cartridges}/errors/error_producer.py (100%) rename {src => cartridges}/errors/friendly_error.py (100%) rename {src => cartridges}/game.py (90%) rename {src => cartridges}/game_cover.py (99%) rename {src => cartridges}/importer/bottles_source.py (94%) rename {src => cartridges}/importer/desktop_source.py (98%) rename {src => cartridges}/importer/flatpak_source.py (95%) rename {src => cartridges}/importer/heroic_source.py (98%) rename {src => cartridges}/importer/importer.py (97%) rename {src => cartridges}/importer/itch_source.py (92%) rename {src => cartridges}/importer/legendary_source.py (95%) rename {src => cartridges}/importer/location.py (99%) rename {src => cartridges}/importer/lutris_source.py (94%) rename {src => cartridges}/importer/retroarch_source.py (96%) rename {src => cartridges}/importer/source.py (97%) rename {src => cartridges}/importer/steam_source.py (94%) rename {src => cartridges}/logging/color_log_formatter.py (100%) rename {src => cartridges}/logging/session_file_handler.py (99%) rename {src => cartridges}/logging/setup.py (94%) rename {src => cartridges}/main.py (77%) rename {src => cartridges}/meson.build (93%) rename {src => cartridges}/preferences.py (95%) rename {src => cartridges}/shared.py.in (82%) rename {src => cartridges}/store/managers/async_manager.py (95%) rename {src => cartridges}/store/managers/cover_manager.py (96%) rename {src => cartridges}/store/managers/display_manager.py (89%) rename {src => cartridges}/store/managers/file_manager.py (89%) rename {src => cartridges}/store/managers/manager.py (96%) rename {src => cartridges}/store/managers/sgdb_manager.py (80%) rename {src => cartridges}/store/managers/steam_api_manager.py (93%) rename {src => cartridges}/store/pipeline.py (97%) rename {src => cartridges}/store/store.py (97%) rename {src => cartridges}/utils/create_dialog.py (100%) rename {src => cartridges}/utils/migrate_files_v1_to_v2.py (99%) rename {src => cartridges}/utils/rate_limiter.py (100%) rename {src => cartridges}/utils/relative_date.py (100%) create mode 100644 cartridges/utils/run_executable.py rename {src => cartridges}/utils/save_cover.py (99%) rename {src => cartridges}/utils/sqlite.py (100%) rename {src => cartridges}/utils/steam.py (98%) rename {src => cartridges}/utils/steamgriddb.py (97%) rename {src => cartridges}/window.py (99%) create mode 100755 search-provider/cartridges-search-provider.in create mode 100644 search-provider/hu.kramo.Cartridges.SearchProvider.ini create mode 100644 search-provider/hu.kramo.Cartridges.SearchProvider.service.in create mode 100644 search-provider/meson.build diff --git a/src/__builtins__.pyi b/cartridges/__builtins__.pyi similarity index 100% rename from src/__builtins__.pyi rename to cartridges/__builtins__.pyi diff --git a/src/cartridges.in b/cartridges/cartridges.in similarity index 72% rename from src/cartridges.in rename to cartridges/cartridges.in index 67e82e7..282a9fd 100755 --- a/src/cartridges.in +++ b/cartridges/cartridges.in @@ -19,11 +19,11 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import os -import sys -import signal -import locale import gettext +import locale +import os +import signal +import sys VERSION = "@VERSION@" if os.name == "nt": @@ -32,29 +32,27 @@ if os.name == "nt": os.environ["LANGUAGE"] = locale.windows_locale[ windll.kernel32.GetUserDefaultUILanguage() ] - pkgdatadir = os.path.join(os.path.dirname(__file__), "..", "share", "cartridges") - localedir = os.path.join(os.path.dirname(__file__), "..", "share", "locale") + PKGDATADIR = os.path.join(os.path.dirname(__file__), "..", "share", "cartridges") + LOCALEDIR = os.path.join(os.path.dirname(__file__), "..", "share", "locale") else: - pkgdatadir = "@pkgdatadir@" - localedir = "@localedir@" + PKGDATADIR = "@pkgdatadir@" + LOCALEDIR = "@localedir@" -sys.path.insert(1, pkgdatadir) +sys.path.insert(1, PKGDATADIR) signal.signal(signal.SIGINT, signal.SIG_DFL) if os.name != "nt": - locale.bindtextdomain("cartridges", localedir) + locale.bindtextdomain("cartridges", LOCALEDIR) locale.textdomain("cartridges") -gettext.install("cartridges", localedir) +gettext.install("cartridges", LOCALEDIR) if __name__ == "__main__": - import gi - from gi.repository import Gio - resource = Gio.Resource.load(os.path.join(pkgdatadir, "cartridges.gresource")) - resource._register() + resource = Gio.Resource.load(os.path.join(PKGDATADIR, "cartridges.gresource")) + resource._register() # pylint: disable=protected-access - from src import main + from cartridges import main sys.exit(main.main(VERSION)) diff --git a/src/details_window.py b/cartridges/details_window.py similarity index 96% rename from src/details_window.py rename to cartridges/details_window.py index ed8973c..fb8d26c 100644 --- a/src/details_window.py +++ b/cartridges/details_window.py @@ -26,14 +26,14 @@ from typing import Any, Optional from gi.repository import Adw, Gio, GLib, Gtk from PIL import Image, UnidentifiedImageError -from src import shared -from src.errors.friendly_error import FriendlyError -from src.game import Game -from src.game_cover import GameCover -from src.store.managers.cover_manager import CoverManager -from src.store.managers.sgdb_manager import SgdbManager -from src.utils.create_dialog import create_dialog -from src.utils.save_cover import convert_cover, save_cover +from cartridges import shared +from cartridges.errors.friendly_error import FriendlyError +from cartridges.game import Game +from cartridges.game_cover import GameCover +from cartridges.store.managers.cover_manager import CoverManager +from cartridges.store.managers.sgdb_manager import SgdbManager +from cartridges.utils.create_dialog import create_dialog +from cartridges.utils.save_cover import convert_cover, save_cover @Gtk.Template(resource_path=shared.PREFIX + "/gtk/details-window.ui") diff --git a/src/errors/error_producer.py b/cartridges/errors/error_producer.py similarity index 100% rename from src/errors/error_producer.py rename to cartridges/errors/error_producer.py diff --git a/src/errors/friendly_error.py b/cartridges/errors/friendly_error.py similarity index 100% rename from src/errors/friendly_error.py rename to cartridges/errors/friendly_error.py diff --git a/src/game.py b/cartridges/game.py similarity index 90% rename from src/game.py rename to cartridges/game.py index e1e3141..dc48d32 100644 --- a/src/game.py +++ b/cartridges/game.py @@ -17,18 +17,16 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import logging -import os import shlex -import subprocess from pathlib import Path from time import time from typing import Any, Optional from gi.repository import Adw, GObject, Gtk -from src import shared -from src.game_cover import GameCover +from cartridges import shared +from cartridges.game_cover import GameCover +from cartridges.utils.run_executable import run_executable # pylint: disable=too-many-instance-attributes @@ -118,21 +116,7 @@ class Game(Gtk.Box): self.save() self.update() - args = ( - "flatpak-spawn --host /bin/sh -c " + shlex.quote(self.executable) # Flatpak - if os.getenv("FLATPAK_ID") == shared.APP_ID - else self.executable # Others - ) - - logging.info("Starting %s: %s", self.name, str(args)) - # pylint: disable=consider-using-with - subprocess.Popen( - args, - cwd=shared.home, - shell=True, - start_new_session=True, - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0, # type: ignore - ) + run_executable(self.executable) if shared.schema.get_boolean("exit-after-launch"): self.app.quit() diff --git a/src/game_cover.py b/cartridges/game_cover.py similarity index 99% rename from src/game_cover.py rename to cartridges/game_cover.py index 40d48cb..46dd674 100644 --- a/src/game_cover.py +++ b/cartridges/game_cover.py @@ -23,7 +23,7 @@ from typing import Optional from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk from PIL import Image, ImageFilter, ImageStat -from src import shared +from cartridges import shared class GameCover: diff --git a/src/importer/bottles_source.py b/cartridges/importer/bottles_source.py similarity index 94% rename from src/importer/bottles_source.py rename to cartridges/importer/bottles_source.py index 2c9c618..fe6d09c 100644 --- a/src/importer/bottles_source.py +++ b/cartridges/importer/bottles_source.py @@ -23,10 +23,10 @@ from typing import NamedTuple import yaml -from src import shared -from src.game import Game -from src.importer.location import Location, LocationSubPath -from src.importer.source import SourceIterable, URLExecutableSource +from cartridges import shared +from cartridges.game import Game +from cartridges.importer.location import Location, LocationSubPath +from cartridges.importer.source import SourceIterable, URLExecutableSource class BottlesSourceIterable(SourceIterable): diff --git a/src/importer/desktop_source.py b/cartridges/importer/desktop_source.py similarity index 98% rename from src/importer/desktop_source.py rename to cartridges/importer/desktop_source.py index cf160e4..1866b27 100644 --- a/src/importer/desktop_source.py +++ b/cartridges/importer/desktop_source.py @@ -25,9 +25,9 @@ from typing import NamedTuple from gi.repository import GLib, Gtk -from src import shared -from src.game import Game -from src.importer.source import Source, SourceIterable +from cartridges import shared +from cartridges.game import Game +from cartridges.importer.source import Source, SourceIterable class DesktopSourceIterable(SourceIterable): diff --git a/src/importer/flatpak_source.py b/cartridges/importer/flatpak_source.py similarity index 95% rename from src/importer/flatpak_source.py rename to cartridges/importer/flatpak_source.py index aea3505..97e2fe5 100644 --- a/src/importer/flatpak_source.py +++ b/cartridges/importer/flatpak_source.py @@ -22,10 +22,10 @@ from typing import NamedTuple from gi.repository import GLib, Gtk -from src import shared -from src.game import Game -from src.importer.location import Location, LocationSubPath -from src.importer.source import ExecutableFormatSource, SourceIterable +from cartridges import shared +from cartridges.game import Game +from cartridges.importer.location import Location, LocationSubPath +from cartridges.importer.source import ExecutableFormatSource, SourceIterable class FlatpakSourceIterable(SourceIterable): diff --git a/src/importer/heroic_source.py b/cartridges/importer/heroic_source.py similarity index 98% rename from src/importer/heroic_source.py rename to cartridges/importer/heroic_source.py index 6792bc8..1533de8 100644 --- a/src/importer/heroic_source.py +++ b/cartridges/importer/heroic_source.py @@ -27,10 +27,10 @@ from json import JSONDecodeError from pathlib import Path from typing import Iterable, NamedTuple, Optional, TypedDict -from src import shared -from src.game import Game -from src.importer.location import Location, LocationSubPath -from src.importer.source import ( +from cartridges import shared +from cartridges.game import Game +from cartridges.importer.location import Location, LocationSubPath +from cartridges.importer.source import ( SourceIterable, SourceIterationResult, URLExecutableSource, diff --git a/src/importer/importer.py b/cartridges/importer/importer.py similarity index 97% rename from src/importer/importer.py rename to cartridges/importer/importer.py index 4bb6e63..f80a0da 100644 --- a/src/importer/importer.py +++ b/cartridges/importer/importer.py @@ -24,14 +24,14 @@ from typing import Any, Optional from gi.repository import Adw, Gio, GLib, Gtk -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.location import UnresolvableLocationError -from src.importer.source import Source -from src.store.managers.async_manager import AsyncManager -from src.store.pipeline import Pipeline +from cartridges import shared +from cartridges.errors.error_producer import ErrorProducer +from cartridges.errors.friendly_error import FriendlyError +from cartridges.game import Game +from cartridges.importer.location import UnresolvableLocationError +from cartridges.importer.source import Source +from cartridges.store.managers.async_manager import AsyncManager +from cartridges.store.pipeline import Pipeline # pylint: disable=too-many-instance-attributes diff --git a/src/importer/itch_source.py b/cartridges/importer/itch_source.py similarity index 92% rename from src/importer/itch_source.py rename to cartridges/importer/itch_source.py index d2de56a..c5bf786 100644 --- a/src/importer/itch_source.py +++ b/cartridges/importer/itch_source.py @@ -22,11 +22,11 @@ from shutil import rmtree from sqlite3 import connect from typing import NamedTuple -from src import shared -from src.game import Game -from src.importer.location import Location, LocationSubPath -from src.importer.source import SourceIterable, URLExecutableSource -from src.utils.sqlite import copy_db +from cartridges import shared +from cartridges.game import Game +from cartridges.importer.location import Location, LocationSubPath +from cartridges.importer.source import SourceIterable, URLExecutableSource +from cartridges.utils.sqlite import copy_db class ItchSourceIterable(SourceIterable): diff --git a/src/importer/legendary_source.py b/cartridges/importer/legendary_source.py similarity index 95% rename from src/importer/legendary_source.py rename to cartridges/importer/legendary_source.py index a4bc222..25221dd 100644 --- a/src/importer/legendary_source.py +++ b/cartridges/importer/legendary_source.py @@ -22,10 +22,10 @@ import logging from json import JSONDecodeError from typing import NamedTuple -from src import shared -from src.game import Game -from src.importer.location import Location, LocationSubPath -from src.importer.source import ( +from cartridges import shared +from cartridges.game import Game +from cartridges.importer.location import Location, LocationSubPath +from cartridges.importer.source import ( ExecutableFormatSource, SourceIterable, SourceIterationResult, diff --git a/src/importer/location.py b/cartridges/importer/location.py similarity index 99% rename from src/importer/location.py rename to cartridges/importer/location.py index 36f592b..ff92c96 100644 --- a/src/importer/location.py +++ b/cartridges/importer/location.py @@ -3,7 +3,7 @@ from os import PathLike from pathlib import Path from typing import Iterable, Mapping, NamedTuple, Optional -from src import shared +from cartridges import shared PathSegment = str | PathLike | Path PathSegments = Iterable[PathSegment] diff --git a/src/importer/lutris_source.py b/cartridges/importer/lutris_source.py similarity index 94% rename from src/importer/lutris_source.py rename to cartridges/importer/lutris_source.py index 3197af2..892b7f6 100644 --- a/src/importer/lutris_source.py +++ b/cartridges/importer/lutris_source.py @@ -21,11 +21,11 @@ from shutil import rmtree from sqlite3 import connect from typing import NamedTuple -from src import shared -from src.game import Game -from src.importer.location import Location, LocationSubPath -from src.importer.source import SourceIterable, URLExecutableSource -from src.utils.sqlite import copy_db +from cartridges import shared +from cartridges.game import Game +from cartridges.importer.location import Location, LocationSubPath +from cartridges.importer.source import SourceIterable, URLExecutableSource +from cartridges.utils.sqlite import copy_db class LutrisSourceIterable(SourceIterable): diff --git a/src/importer/retroarch_source.py b/cartridges/importer/retroarch_source.py similarity index 96% rename from src/importer/retroarch_source.py rename to cartridges/importer/retroarch_source.py index 955a1dc..4502ac3 100644 --- a/src/importer/retroarch_source.py +++ b/cartridges/importer/retroarch_source.py @@ -26,12 +26,16 @@ from pathlib import Path from shlex import quote as shell_quote from typing import NamedTuple -from src import shared -from src.errors.friendly_error import FriendlyError -from src.game import Game -from src.importer.location import Location, LocationSubPath, UnresolvableLocationError -from src.importer.source import Source, SourceIterable -from src.importer.steam_source import SteamSource +from cartridges import shared +from cartridges.errors.friendly_error import FriendlyError +from cartridges.game import Game +from cartridges.importer.location import ( + Location, + LocationSubPath, + UnresolvableLocationError, +) +from cartridges.importer.source import Source, SourceIterable +from cartridges.importer.steam_source import SteamSource class RetroarchSourceIterable(SourceIterable): diff --git a/src/importer/source.py b/cartridges/importer/source.py similarity index 97% rename from src/importer/source.py rename to cartridges/importer/source.py index 6f0003d..aca2ba3 100644 --- a/src/importer/source.py +++ b/cartridges/importer/source.py @@ -22,8 +22,8 @@ from abc import abstractmethod from collections.abc import Iterable from typing import Any, Collection, Generator, Optional -from src.game import Game -from src.importer.location import Location +from cartridges.game import Game +from cartridges.importer.location import Location # Type of the data returned by iterating on a Source SourceIterationResult = Optional[Game | tuple[Game, tuple[Any]]] diff --git a/src/importer/steam_source.py b/cartridges/importer/steam_source.py similarity index 94% rename from src/importer/steam_source.py rename to cartridges/importer/steam_source.py index fa53937..b3a1f99 100644 --- a/src/importer/steam_source.py +++ b/cartridges/importer/steam_source.py @@ -23,11 +23,11 @@ import re from pathlib import Path from typing import Iterable, NamedTuple -from src import shared -from src.game import Game -from src.importer.location import Location, LocationSubPath -from src.importer.source import SourceIterable, URLExecutableSource -from src.utils.steam import SteamFileHelper, SteamInvalidManifestError +from cartridges import shared +from cartridges.game import Game +from cartridges.importer.location import Location, LocationSubPath +from cartridges.importer.source import SourceIterable, URLExecutableSource +from cartridges.utils.steam import SteamFileHelper, SteamInvalidManifestError class SteamSourceIterable(SourceIterable): diff --git a/src/logging/color_log_formatter.py b/cartridges/logging/color_log_formatter.py similarity index 100% rename from src/logging/color_log_formatter.py rename to cartridges/logging/color_log_formatter.py diff --git a/src/logging/session_file_handler.py b/cartridges/logging/session_file_handler.py similarity index 99% rename from src/logging/session_file_handler.py rename to cartridges/logging/session_file_handler.py index d207e29..bc3023c 100644 --- a/src/logging/session_file_handler.py +++ b/cartridges/logging/session_file_handler.py @@ -25,7 +25,7 @@ from os import PathLike from pathlib import Path from typing import Optional -from src import shared +from cartridges import shared class SessionFileHandler(StreamHandler): diff --git a/src/logging/setup.py b/cartridges/logging/setup.py similarity index 94% rename from src/logging/setup.py rename to cartridges/logging/setup.py index f3498cc..40e6a15 100644 --- a/src/logging/setup.py +++ b/cartridges/logging/setup.py @@ -24,7 +24,7 @@ import platform import subprocess import sys -from src import shared +from cartridges import shared def setup_logging() -> None: @@ -47,12 +47,12 @@ def setup_logging() -> None: }, "console_formatter": { "format": "%(name)s %(levelname)s - %(message)s", - "class": "src.logging.color_log_formatter.ColorLogFormatter", + "class": "cartridges.logging.color_log_formatter.ColorLogFormatter", }, }, "handlers": { "file_handler": { - "class": "src.logging.session_file_handler.SessionFileHandler", + "class": "cartridges.logging.session_file_handler.SessionFileHandler", "formatter": "file_formatter", "level": "DEBUG", "filename": log_filename, diff --git a/src/main.py b/cartridges/main.py similarity index 77% rename from src/main.py rename to cartridges/main.py index 5b379db..c35ba1c 100644 --- a/src/main.py +++ b/cartridges/main.py @@ -20,6 +20,7 @@ import json import lzma import os +import shlex import sys from typing import Any, Optional @@ -31,39 +32,42 @@ gi.require_version("Adw", "1") # pylint: disable=wrong-import-position 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.bottles_source import BottlesSource -from src.importer.desktop_source import DesktopSource -from src.importer.flatpak_source import FlatpakSource -from src.importer.heroic_source import HeroicSource -from src.importer.importer import Importer -from src.importer.itch_source import ItchSource -from src.importer.legendary_source import LegendarySource -from src.importer.lutris_source import LutrisSource -from src.importer.retroarch_source import RetroarchSource -from src.importer.steam_source import SteamSource -from src.logging.setup import log_system_info, setup_logging -from src.preferences import PreferencesWindow -from src.store.managers.cover_manager import CoverManager -from src.store.managers.display_manager import DisplayManager -from src.store.managers.file_manager import FileManager -from src.store.managers.sgdb_manager import SgdbManager -from src.store.managers.steam_api_manager import SteamAPIManager -from src.store.store import Store -from src.utils.migrate_files_v1_to_v2 import migrate_files_v1_to_v2 -from src.window import CartridgesWindow +from cartridges import shared +from cartridges.details_window import DetailsWindow +from cartridges.game import Game +from cartridges.importer.bottles_source import BottlesSource +from cartridges.importer.desktop_source import DesktopSource +from cartridges.importer.flatpak_source import FlatpakSource +from cartridges.importer.heroic_source import HeroicSource +from cartridges.importer.importer import Importer +from cartridges.importer.itch_source import ItchSource +from cartridges.importer.legendary_source import LegendarySource +from cartridges.importer.lutris_source import LutrisSource +from cartridges.importer.retroarch_source import RetroarchSource +from cartridges.importer.steam_source import SteamSource +from cartridges.logging.setup import log_system_info, setup_logging +from cartridges.preferences import PreferencesWindow +from cartridges.store.managers.cover_manager import CoverManager +from cartridges.store.managers.display_manager import DisplayManager +from cartridges.store.managers.file_manager import FileManager +from cartridges.store.managers.sgdb_manager import SgdbManager +from cartridges.store.managers.steam_api_manager import SteamAPIManager +from cartridges.store.store import Store +from cartridges.utils.migrate_files_v1_to_v2 import migrate_files_v1_to_v2 +from cartridges.utils.run_executable import run_executable +from cartridges.window import CartridgesWindow class CartridgesApplication(Adw.Application): state = shared.AppState.DEFAULT win: CartridgesWindow + init_search_term: Optional[str] = None def __init__(self) -> None: shared.store = Store() super().__init__( - application_id=shared.APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE + application_id=shared.APP_ID, + flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, ) def do_activate(self) -> None: # pylint: disable=arguments-differ @@ -147,8 +151,47 @@ class CartridgesApplication(Adw.Application): sort_action, shared.state_schema.get_value("sort-mode") ) + if self.init_search_term: # For command line activation + shared.win.search_bar.set_search_mode(True) + shared.win.search_entry.set_text(self.init_search_term) + shared.win.search_entry.set_position(-1) + shared.win.present() + def do_command_line(self, command_line) -> int: + for index, arg in enumerate(args := command_line.get_arguments()): + if arg == "--search": + try: + self.init_search_term = args[index + 1] + except IndexError: + pass + break + + if arg == "--launch": + try: + game_id = args[index + 1] + data = json.load((shared.games_dir / (game_id + ".json")).open("r")) + executable = ( + shlex.join(data["executable"]) + if isinstance(data["executable"], list) + else data["executable"] + ) + name = data["name"] + + run_executable(executable) + except (IndexError, KeyError, OSError, json.decoder.JSONDecodeError): + return 1 + + notification = Gio.Notification.new(_("Cartridges")) + notification.set_body(_("{} launched").format(name)) + self.send_notification( + "launch", + notification, + ) + return 0 + self.activate() + return 0 + def load_games_from_disk(self) -> None: if shared.games_dir.is_dir(): for game_file in shared.games_dir.iterdir(): diff --git a/src/meson.build b/cartridges/meson.build similarity index 93% rename from src/meson.build rename to cartridges/meson.build index 5bb4a75..535284d 100644 --- a/src/meson.build +++ b/cartridges/meson.build @@ -1,4 +1,4 @@ -moduledir = join_paths(pkgdatadir, 'src') +moduledir = join_paths(python_dir, 'cartridges') configure_file( input: 'cartridges.in', diff --git a/src/preferences.py b/cartridges/preferences.py similarity index 95% rename from src/preferences.py rename to cartridges/preferences.py index bc61ec2..d5377bb 100644 --- a/src/preferences.py +++ b/cartridges/preferences.py @@ -25,21 +25,21 @@ from typing import Any, Callable, Optional from gi.repository import Adw, Gio, GLib, Gtk -from src import shared -from src.errors.friendly_error import FriendlyError -from src.game import Game -from src.importer.bottles_source import BottlesSource -from src.importer.flatpak_source import FlatpakSource -from src.importer.heroic_source import HeroicSource -from src.importer.itch_source import ItchSource -from src.importer.legendary_source import LegendarySource -from src.importer.location import UnresolvableLocationError -from src.importer.lutris_source import LutrisSource -from src.importer.retroarch_source import RetroarchSource -from src.importer.source import Source -from src.importer.steam_source import SteamSource -from src.store.managers.sgdb_manager import SgdbManager -from src.utils.create_dialog import create_dialog +from cartridges import shared +from cartridges.errors.friendly_error import FriendlyError +from cartridges.game import Game +from cartridges.importer.bottles_source import BottlesSource +from cartridges.importer.flatpak_source import FlatpakSource +from cartridges.importer.heroic_source import HeroicSource +from cartridges.importer.itch_source import ItchSource +from cartridges.importer.legendary_source import LegendarySource +from cartridges.importer.location import UnresolvableLocationError +from cartridges.importer.lutris_source import LutrisSource +from cartridges.importer.retroarch_source import RetroarchSource +from cartridges.importer.source import Source +from cartridges.importer.steam_source import SteamSource +from cartridges.store.managers.sgdb_manager import SgdbManager +from cartridges.utils.create_dialog import create_dialog @Gtk.Template(resource_path=shared.PREFIX + "/gtk/preferences.ui") diff --git a/src/shared.py.in b/cartridges/shared.py.in similarity index 82% rename from src/shared.py.in rename to cartridges/shared.py.in index 19c6c85..74e19fa 100644 --- a/src/shared.py.in +++ b/cartridges/shared.py.in @@ -51,13 +51,20 @@ games_dir = data_dir / "cartridges" / "games" covers_dir = data_dir / "cartridges" / "covers" appdata_dir = Path(os.getenv("appdata") or "C:\\Users\\Default\\AppData\\Roaming") -local_appdata_dir = Path(os.getenv("csidl_local_appdata") or "C:\\Users\\Default\\AppData\\Local") +local_appdata_dir = Path( + os.getenv("csidl_local_appdata") or "C:\\Users\\Default\\AppData\\Local" +) programfiles32_dir = Path(os.getenv("programfiles(x86)") or "C:\\Program Files (x86)") -scale_factor = max( - monitor.get_scale_factor() for monitor in Gdk.Display.get_default().get_monitors() -) -image_size = (200 * scale_factor, 300 * scale_factor) +try: + scale_factor = max( + monitor.get_scale_factor() + for monitor in Gdk.Display.get_default().get_monitors() + ) +except AttributeError: # If shared.py is imported by the search provider + pass +else: + image_size = (200 * scale_factor, 300 * scale_factor) # pylint: disable=invalid-name win = None diff --git a/src/store/managers/async_manager.py b/cartridges/store/managers/async_manager.py similarity index 95% rename from src/store/managers/async_manager.py rename to cartridges/store/managers/async_manager.py index 332bf29..9fd784b 100644 --- a/src/store/managers/async_manager.py +++ b/cartridges/store/managers/async_manager.py @@ -21,8 +21,8 @@ from typing import Any, Callable from gi.repository import Gio -from src.game import Game -from src.store.managers.manager import Manager +from cartridges.game import Game +from cartridges.store.managers.manager import Manager class AsyncManager(Manager): diff --git a/src/store/managers/cover_manager.py b/cartridges/store/managers/cover_manager.py similarity index 96% rename from src/store/managers/cover_manager.py rename to cartridges/store/managers/cover_manager.py index 217ef7b..34a3580 100644 --- a/src/store/managers/cover_manager.py +++ b/cartridges/store/managers/cover_manager.py @@ -25,11 +25,11 @@ import requests from gi.repository import GdkPixbuf, Gio from requests.exceptions import HTTPError, SSLError -from src import shared -from src.game import Game -from src.store.managers.manager import Manager -from src.store.managers.steam_api_manager import SteamAPIManager -from src.utils.save_cover import convert_cover, save_cover +from cartridges import shared +from cartridges.game import Game +from cartridges.store.managers.manager import Manager +from cartridges.store.managers.steam_api_manager import SteamAPIManager +from cartridges.utils.save_cover import convert_cover, save_cover class ImageSize(NamedTuple): diff --git a/src/store/managers/display_manager.py b/cartridges/store/managers/display_manager.py similarity index 89% rename from src/store/managers/display_manager.py rename to cartridges/store/managers/display_manager.py index 0eb1c5f..41279ff 100644 --- a/src/store/managers/display_manager.py +++ b/cartridges/store/managers/display_manager.py @@ -17,12 +17,12 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from src import shared -from src.game import Game -from src.game_cover import GameCover -from src.store.managers.manager import Manager -from src.store.managers.sgdb_manager import SgdbManager -from src.store.managers.steam_api_manager import SteamAPIManager +from cartridges import shared +from cartridges.game import Game +from cartridges.game_cover import GameCover +from cartridges.store.managers.manager import Manager +from cartridges.store.managers.sgdb_manager import SgdbManager +from cartridges.store.managers.steam_api_manager import SteamAPIManager class DisplayManager(Manager): diff --git a/src/store/managers/file_manager.py b/cartridges/store/managers/file_manager.py similarity index 89% rename from src/store/managers/file_manager.py rename to cartridges/store/managers/file_manager.py index 8eee69b..b1f437a 100644 --- a/src/store/managers/file_manager.py +++ b/cartridges/store/managers/file_manager.py @@ -19,10 +19,10 @@ import json -from src import shared -from src.game import Game -from src.store.managers.async_manager import AsyncManager -from src.store.managers.steam_api_manager import SteamAPIManager +from cartridges import shared +from cartridges.game import Game +from cartridges.store.managers.async_manager import AsyncManager +from cartridges.store.managers.steam_api_manager import SteamAPIManager class FileManager(AsyncManager): diff --git a/src/store/managers/manager.py b/cartridges/store/managers/manager.py similarity index 96% rename from src/store/managers/manager.py rename to cartridges/store/managers/manager.py index a727a22..9d1bec1 100644 --- a/src/store/managers/manager.py +++ b/cartridges/store/managers/manager.py @@ -22,9 +22,9 @@ from abc import abstractmethod from time import sleep from typing import Any, Callable, Container -from src.errors.error_producer import ErrorProducer -from src.errors.friendly_error import FriendlyError -from src.game import Game +from cartridges.errors.error_producer import ErrorProducer +from cartridges.errors.friendly_error import FriendlyError +from cartridges.game import Game class Manager(ErrorProducer): diff --git a/src/store/managers/sgdb_manager.py b/cartridges/store/managers/sgdb_manager.py similarity index 80% rename from src/store/managers/sgdb_manager.py rename to cartridges/store/managers/sgdb_manager.py index 67c40e6..7fcf584 100644 --- a/src/store/managers/sgdb_manager.py +++ b/cartridges/store/managers/sgdb_manager.py @@ -21,12 +21,12 @@ from json import JSONDecodeError from requests.exceptions import HTTPError, SSLError -from src.errors.friendly_error import FriendlyError -from src.game import Game -from src.store.managers.async_manager import AsyncManager -from src.store.managers.cover_manager import CoverManager -from src.store.managers.steam_api_manager import SteamAPIManager -from src.utils.steamgriddb import SgdbAuthError, SgdbHelper +from cartridges.errors.friendly_error import FriendlyError +from cartridges.game import Game +from cartridges.store.managers.async_manager import AsyncManager +from cartridges.store.managers.cover_manager import CoverManager +from cartridges.store.managers.steam_api_manager import SteamAPIManager +from cartridges.utils.steamgriddb import SgdbAuthError, SgdbHelper class SgdbManager(AsyncManager): diff --git a/src/store/managers/steam_api_manager.py b/cartridges/store/managers/steam_api_manager.py similarity index 93% rename from src/store/managers/steam_api_manager.py rename to cartridges/store/managers/steam_api_manager.py index 2dc88d5..6cc2a0d 100644 --- a/src/store/managers/steam_api_manager.py +++ b/cartridges/store/managers/steam_api_manager.py @@ -20,9 +20,9 @@ from requests.exceptions import HTTPError, SSLError from urllib3.exceptions import ConnectionError as Urllib3ConnectionError -from src.game import Game -from src.store.managers.async_manager import AsyncManager -from src.utils.steam import ( +from cartridges.game import Game +from cartridges.store.managers.async_manager import AsyncManager +from cartridges.utils.steam import ( SteamAPIHelper, SteamGameNotFoundError, SteamNotAGameError, diff --git a/src/store/pipeline.py b/cartridges/store/pipeline.py similarity index 97% rename from src/store/pipeline.py rename to cartridges/store/pipeline.py index f552f04..9a1b6e3 100644 --- a/src/store/pipeline.py +++ b/cartridges/store/pipeline.py @@ -22,8 +22,8 @@ from typing import Iterable from gi.repository import GObject -from src.game import Game -from src.store.managers.manager import Manager +from cartridges.game import Game +from cartridges.store.managers.manager import Manager class Pipeline(GObject.Object): diff --git a/src/store/store.py b/cartridges/store/store.py similarity index 97% rename from src/store/store.py rename to cartridges/store/store.py index 9605d5b..bbd4c15 100644 --- a/src/store/store.py +++ b/cartridges/store/store.py @@ -20,10 +20,10 @@ import logging from typing import Any, Generator, MutableMapping, Optional -from src import shared -from src.game import Game -from src.store.managers.manager import Manager -from src.store.pipeline import Pipeline +from cartridges import shared +from cartridges.game import Game +from cartridges.store.managers.manager import Manager +from cartridges.store.pipeline import Pipeline class Store: diff --git a/src/utils/create_dialog.py b/cartridges/utils/create_dialog.py similarity index 100% rename from src/utils/create_dialog.py rename to cartridges/utils/create_dialog.py diff --git a/src/utils/migrate_files_v1_to_v2.py b/cartridges/utils/migrate_files_v1_to_v2.py similarity index 99% rename from src/utils/migrate_files_v1_to_v2.py rename to cartridges/utils/migrate_files_v1_to_v2.py index 41c166b..526d48b 100644 --- a/src/utils/migrate_files_v1_to_v2.py +++ b/cartridges/utils/migrate_files_v1_to_v2.py @@ -21,7 +21,7 @@ import json import logging from pathlib import Path -from src import shared +from cartridges import shared old_data_dir = shared.home / ".local" / "share" old_cartridges_data_dir = old_data_dir / "cartridges" diff --git a/src/utils/rate_limiter.py b/cartridges/utils/rate_limiter.py similarity index 100% rename from src/utils/rate_limiter.py rename to cartridges/utils/rate_limiter.py diff --git a/src/utils/relative_date.py b/cartridges/utils/relative_date.py similarity index 100% rename from src/utils/relative_date.py rename to cartridges/utils/relative_date.py diff --git a/cartridges/utils/run_executable.py b/cartridges/utils/run_executable.py new file mode 100644 index 0000000..f481781 --- /dev/null +++ b/cartridges/utils/run_executable.py @@ -0,0 +1,24 @@ +import logging +import os +import subprocess +from shlex import quote + +from cartridges import shared + + +def run_executable(executable) -> None: + args = ( + "flatpak-spawn --host /bin/sh -c " + quote(executable) # Flatpak + if os.getenv("FLATPAK_ID") == shared.APP_ID + else executable # Others + ) + + logging.info("Launching `%s`", str(args)) + # pylint: disable=consider-using-with + subprocess.Popen( + args, + cwd=shared.home, + shell=True, + start_new_session=True, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0, # type: ignore + ) diff --git a/src/utils/save_cover.py b/cartridges/utils/save_cover.py similarity index 99% rename from src/utils/save_cover.py rename to cartridges/utils/save_cover.py index 336161d..a62ecb9 100644 --- a/src/utils/save_cover.py +++ b/cartridges/utils/save_cover.py @@ -25,7 +25,7 @@ from typing import Optional from gi.repository import Gdk, GdkPixbuf, Gio, GLib from PIL import Image, ImageSequence, UnidentifiedImageError -from src import shared +from cartridges import shared def convert_cover( diff --git a/src/utils/sqlite.py b/cartridges/utils/sqlite.py similarity index 100% rename from src/utils/sqlite.py rename to cartridges/utils/sqlite.py diff --git a/src/utils/steam.py b/cartridges/utils/steam.py similarity index 98% rename from src/utils/steam.py rename to cartridges/utils/steam.py index 6e53f27..416daf0 100644 --- a/src/utils/steam.py +++ b/cartridges/utils/steam.py @@ -27,8 +27,8 @@ from typing import TypedDict import requests from requests.exceptions import HTTPError -from src import shared -from src.utils.rate_limiter import RateLimiter +from cartridges import shared +from cartridges.utils.rate_limiter import RateLimiter class SteamError(Exception): diff --git a/src/utils/steamgriddb.py b/cartridges/utils/steamgriddb.py similarity index 97% rename from src/utils/steamgriddb.py rename to cartridges/utils/steamgriddb.py index b9044cb..d4d8aff 100644 --- a/src/utils/steamgriddb.py +++ b/cartridges/utils/steamgriddb.py @@ -26,9 +26,9 @@ import requests from gi.repository import Gio from requests.exceptions import HTTPError -from src import shared -from src.game import Game -from src.utils.save_cover import convert_cover, save_cover +from cartridges import shared +from cartridges.game import Game +from cartridges.utils.save_cover import convert_cover, save_cover class SgdbError(Exception): diff --git a/src/window.py b/cartridges/window.py similarity index 99% rename from src/window.py rename to cartridges/window.py index 4d76b07..73139fa 100644 --- a/src/window.py +++ b/cartridges/window.py @@ -21,10 +21,10 @@ from typing import Any, Optional from gi.repository import Adw, Gio, GLib, Gtk -from src import shared -from src.game import Game -from src.game_cover import GameCover -from src.utils.relative_date import relative_date +from cartridges import shared +from cartridges.game import Game +from cartridges.game_cover import GameCover +from cartridges.utils.relative_date import relative_date @Gtk.Template(resource_path=shared.PREFIX + "/gtk/window.ui") diff --git a/meson.build b/meson.build index de1944f..7a2ff24 100644 --- a/meson.build +++ b/meson.build @@ -8,7 +8,12 @@ i18n = import('i18n') gnome = import('gnome') python = import('python') +py_installation = python.find_installation('python3') +python_dir = join_paths(get_option('prefix'), py_installation.get_install_dir()) + +python_dir = join_paths(get_option('prefix'), py_installation.get_install_dir()) pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) +libexecdir = join_paths(get_option('prefix'), get_option('libexecdir')) profile = get_option('profile') if profile == 'development' @@ -20,21 +25,24 @@ elif profile == 'release' endif conf = configuration_data() -conf.set('PYTHON', python.find_installation('python3').full_path()) -conf.set('PYTHON_VERSION', python.find_installation('python3').language_version()) +conf.set('PYTHON', py_installation.full_path()) +conf.set('PYTHON_VERSION', py_installation.language_version()) conf.set('APP_ID', app_id) conf.set('PREFIX', prefix) conf.set('VERSION', meson.project_version()) conf.set('PROFILE', profile) conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) conf.set('pkgdatadir', pkgdatadir) +conf.set('libexecdir', libexecdir) subdir('data') -subdir('src') +subdir('cartridges') subdir('po') if host_machine.system() == 'windows' subdir('windows') +else + subdir('search-provider') endif gnome.post_install( diff --git a/po/POTFILES b/po/POTFILES index 290edce..24ef6fe 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -8,25 +8,26 @@ data/gtk/help-overlay.blp data/gtk/preferences.blp data/gtk/window.blp -src/main.py -src/window.py -src/details_window.py -src/game.py -src/preferences.py +cartridges/main.py +cartridges/window.py +cartridges/details_window.py +cartridges/game.py +cartridges/preferences.py -src/utils/create_dialog.py -src/importer/importer.py -src/importer/sources/source.py -src/importer/sources/location.py -src/importer/sources/location.py -src/store/managers/sgdb_manager.py +cartridges/utils/create_dialog.py -src/importer/sources/bottles_source.py -src/importer/sources/desktop_source.py -src/importer/sources/flatpak_source.py -src/importer/sources/heroic_source.py -src/importer/sources/itch_source.py -src/importer/sources/legendary_source.py -src/importer/sources/lutris_source.py -src/importer/sources/retroarch_source.py -src/importer/sources/steam_source.py \ No newline at end of file +cartridges/importer/importer.py +cartridges/importer/source.py +cartridges/importer/location.py +cartridges/importer/location.py +cartridges/importer/bottles_source.py +cartridges/importer/desktop_source.py +cartridges/importer/flatpak_source.py +cartridges/importer/heroic_source.py +cartridges/importer/itch_source.py +cartridges/importer/legendary_source.py +cartridges/importer/lutris_source.py +cartridges/importer/retroarch_source.py +cartridges/importer/steam_source.py + +cartridges/store/managers/sgdb_manager.py \ No newline at end of file diff --git a/po/cartridges.pot b/po/cartridges.pot index b61ce4b..b557514 100644 --- a/po/cartridges.pot +++ b/po/cartridges.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-26 13:51+0200\n" +"POT-Creation-Date: 2023-10-10 22:22+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -20,7 +20,7 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 #: data/hu.kramo.Cartridges.metainfo.xml.in:30 data/gtk/window.blp:47 -#: data/gtk/window.blp:80 +#: data/gtk/window.blp:80 cartridges/main.py:185 msgid "Cartridges" msgstr "" @@ -47,7 +47,7 @@ msgid "" msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:34 data/gtk/window.blp:288 -#: src/details_window.py:71 +#: cartridges/details_window.py:71 msgid "Game Details" msgstr "" @@ -56,8 +56,8 @@ msgid "Edit Game Details" msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/help-overlay.blp:19 -#: data/gtk/window.blp:515 src/details_window.py:271 -#: src/importer/importer.py:319 src/importer/importer.py:370 +#: data/gtk/window.blp:515 cartridges/details_window.py:271 +#: cartridges/importer/importer.py:319 cartridges/importer/importer.py:370 msgid "Preferences" msgstr "" @@ -73,7 +73,7 @@ msgstr "" msgid "Delete Cover" msgstr "" -#: data/gtk/details-window.blp:100 data/gtk/game.blp:80 +#: data/gtk/details-window.blp:100 data/gtk/game.blp:81 msgid "Title" msgstr "" @@ -93,20 +93,20 @@ msgstr "" msgid "More Info" msgstr "" -#: data/gtk/game.blp:102 data/gtk/game.blp:121 data/gtk/window.blp:415 +#: data/gtk/game.blp:103 data/gtk/game.blp:122 data/gtk/window.blp:415 msgid "Edit" msgstr "" -#: data/gtk/game.blp:107 src/window.py:350 +#: data/gtk/game.blp:108 cartridges/window.py:350 msgid "Hide" msgstr "" -#: data/gtk/game.blp:112 data/gtk/game.blp:131 data/gtk/preferences.blp:40 +#: data/gtk/game.blp:113 data/gtk/game.blp:132 data/gtk/preferences.blp:40 #: data/gtk/window.blp:435 msgid "Remove" msgstr "" -#: data/gtk/game.blp:126 src/window.py:352 +#: data/gtk/game.blp:127 cartridges/window.py:352 msgid "Unhide" msgstr "" @@ -123,8 +123,8 @@ msgstr "" msgid "Keyboard Shortcuts" msgstr "" -#: data/gtk/help-overlay.blp:29 src/game.py:105 src/preferences.py:125 -#: src/importer/importer.py:394 +#: data/gtk/help-overlay.blp:29 cartridges/game.py:103 +#: cartridges/preferences.py:125 cartridges/importer/importer.py:394 msgid "Undo" msgstr "" @@ -178,7 +178,7 @@ msgstr "" msgid "Swaps the behavior of the cover image and the play button" msgstr "" -#: data/gtk/preferences.blp:25 src/details_window.py:85 +#: data/gtk/preferences.blp:25 cartridges/details_window.py:85 msgid "Images" msgstr "" @@ -206,7 +206,7 @@ msgstr "" msgid "Sources" msgstr "" -#: data/gtk/preferences.blp:83 src/importer/sources/steam_source.py:114 +#: data/gtk/preferences.blp:83 cartridges/importer/steam_source.py:114 msgid "Steam" msgstr "" @@ -217,7 +217,7 @@ msgstr "" msgid "Install Location" msgstr "" -#: data/gtk/preferences.blp:100 src/importer/sources/lutris_source.py:92 +#: data/gtk/preferences.blp:100 cartridges/importer/lutris_source.py:92 msgid "Lutris" msgstr "" @@ -233,7 +233,7 @@ msgstr "" msgid "Import Flatpak Games" msgstr "" -#: data/gtk/preferences.blp:137 src/importer/sources/heroic_source.py:355 +#: data/gtk/preferences.blp:137 cartridges/importer/heroic_source.py:355 msgid "Heroic" msgstr "" @@ -253,23 +253,23 @@ msgstr "" msgid "Import Sideloaded Games" msgstr "" -#: data/gtk/preferences.blp:170 src/importer/sources/bottles_source.py:86 +#: data/gtk/preferences.blp:170 cartridges/importer/bottles_source.py:86 msgid "Bottles" msgstr "" -#: data/gtk/preferences.blp:187 src/importer/sources/itch_source.py:81 +#: data/gtk/preferences.blp:187 cartridges/importer/itch_source.py:81 msgid "itch" msgstr "" -#: data/gtk/preferences.blp:204 src/importer/sources/legendary_source.py:97 +#: data/gtk/preferences.blp:204 cartridges/importer/legendary_source.py:97 msgid "Legendary" msgstr "" -#: data/gtk/preferences.blp:221 src/importer/sources/retroarch_source.py:142 +#: data/gtk/preferences.blp:221 cartridges/importer/retroarch_source.py:142 msgid "RetroArch" msgstr "" -#: data/gtk/preferences.blp:238 src/importer/sources/flatpak_source.py:118 +#: data/gtk/preferences.blp:238 cartridges/importer/flatpak_source.py:118 msgid "Flatpak" msgstr "" @@ -277,7 +277,7 @@ msgstr "" msgid "Import Game Launchers" msgstr "" -#: data/gtk/preferences.blp:259 src/importer/sources/desktop_source.py:217 +#: data/gtk/preferences.blp:259 cartridges/importer/desktop_source.py:215 msgid "Desktop Entries" msgstr "" @@ -345,11 +345,11 @@ msgstr "" msgid "Games you hide will appear here." msgstr "" -#: data/gtk/window.blp:75 data/gtk/window.blp:106 src/main.py:166 +#: data/gtk/window.blp:75 data/gtk/window.blp:106 cartridges/main.py:207 msgid "All Games" msgstr "" -#: data/gtk/window.blp:126 src/main.py:168 +#: data/gtk/window.blp:126 cartridges/main.py:209 msgid "Added" msgstr "" @@ -401,62 +401,67 @@ msgstr "" msgid "About Cartridges" msgstr "" +#. The variable is the title of the game +#: cartridges/main.py:186 cartridges/game.py:125 +msgid "{} launched" +msgstr "" + #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:208 +#: cartridges/main.py:249 msgid "translator_credits" msgstr "" #. The variable is the date when the game was added -#: src/window.py:373 +#: cartridges/window.py:373 msgid "Added: {}" msgstr "" -#: src/window.py:376 +#: cartridges/window.py:376 msgid "Never" msgstr "" #. The variable is the date when the game was last played -#: src/window.py:380 +#: cartridges/window.py:380 msgid "Last played: {}" msgstr "" -#: src/details_window.py:76 +#: cartridges/details_window.py:76 msgid "Apply" msgstr "" -#: src/details_window.py:82 +#: cartridges/details_window.py:82 msgid "Add New Game" msgstr "" -#: src/details_window.py:83 +#: cartridges/details_window.py:83 msgid "Add" msgstr "" -#: src/details_window.py:93 +#: cartridges/details_window.py:93 msgid "Executables" msgstr "" #. Translate this string as you would translate "file" -#: src/details_window.py:108 +#: cartridges/details_window.py:108 msgid "file.txt" msgstr "" #. As in software -#: src/details_window.py:110 +#: cartridges/details_window.py:110 msgid "program" msgstr "" #. Translate this string as you would translate "path to {}" -#: src/details_window.py:115 src/details_window.py:117 +#: cartridges/details_window.py:115 cartridges/details_window.py:117 msgid "C:\\path\\to\\{}" msgstr "" #. Translate this string as you would translate "path to {}" -#: src/details_window.py:121 src/details_window.py:123 +#: cartridges/details_window.py:121 cartridges/details_window.py:123 msgid "/path/to/{}" msgstr "" -#: src/details_window.py:128 +#: cartridges/details_window.py:128 msgid "" "To launch the executable \"{}\", use the command:\n" "\n" @@ -469,141 +474,136 @@ msgid "" "If the path contains spaces, make sure to wrap it in double quotes!" msgstr "" -#: src/details_window.py:171 src/details_window.py:177 +#: cartridges/details_window.py:171 cartridges/details_window.py:177 msgid "Couldn't Add Game" msgstr "" -#: src/details_window.py:171 src/details_window.py:213 +#: cartridges/details_window.py:171 cartridges/details_window.py:213 msgid "Game title cannot be empty." msgstr "" -#: src/details_window.py:177 src/details_window.py:221 +#: cartridges/details_window.py:177 cartridges/details_window.py:221 msgid "Executable cannot be empty." msgstr "" -#: src/details_window.py:212 src/details_window.py:220 +#: cartridges/details_window.py:212 cartridges/details_window.py:220 msgid "Couldn't Apply Preferences" msgstr "" #. The variable is the title of the game -#: src/game.py:141 -msgid "{} launched" -msgstr "" - -#. The variable is the title of the game -#: src/game.py:155 +#: cartridges/game.py:139 msgid "{} hidden" msgstr "" -#: src/game.py:155 +#: cartridges/game.py:139 msgid "{} unhidden" msgstr "" #. The variable is the title of the game #. The variable is the number of games removed -#: src/game.py:169 src/importer/importer.py:391 +#: cartridges/game.py:153 cartridges/importer/importer.py:391 msgid "{} removed" msgstr "" -#: src/preferences.py:124 +#: cartridges/preferences.py:124 msgid "All games removed" msgstr "" -#: src/preferences.py:172 +#: cartridges/preferences.py:172 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" -#: src/preferences.py:184 +#: cartridges/preferences.py:184 msgid "Downloading covers…" msgstr "" -#: src/preferences.py:203 +#: cartridges/preferences.py:203 msgid "Covers updated" msgstr "" -#: src/preferences.py:335 +#: cartridges/preferences.py:335 msgid "Installation Not Found" msgstr "" -#: src/preferences.py:336 +#: cartridges/preferences.py:336 msgid "Select a valid directory." msgstr "" -#: src/preferences.py:372 src/importer/importer.py:317 +#: cartridges/preferences.py:372 cartridges/importer/importer.py:317 msgid "Warning" msgstr "" -#: src/preferences.py:406 +#: cartridges/preferences.py:406 msgid "Invalid Directory" msgstr "" -#: src/preferences.py:412 +#: cartridges/preferences.py:412 msgid "Set Location" msgstr "" -#: src/utils/create_dialog.py:33 src/importer/importer.py:318 +#: cartridges/utils/create_dialog.py:33 cartridges/importer/importer.py:318 msgid "Dismiss" msgstr "" -#: src/importer/importer.py:145 +#: cartridges/importer/importer.py:145 msgid "Importing Games…" msgstr "" -#: src/importer/importer.py:338 +#: cartridges/importer/importer.py:338 msgid "The following errors occured during import:" msgstr "" -#: src/importer/importer.py:367 +#: cartridges/importer/importer.py:367 msgid "No new games found" msgstr "" -#: src/importer/importer.py:379 +#: cartridges/importer/importer.py:379 msgid "1 game imported" msgstr "" #. The variable is the number of games -#: src/importer/importer.py:383 +#: cartridges/importer/importer.py:383 msgid "{} games imported" msgstr "" #. A single game removed -#: src/importer/importer.py:387 +#: cartridges/importer/importer.py:387 msgid "1 removed" msgstr "" #. The variable is the name of the source -#: src/importer/sources/location.py:33 +#: cartridges/importer/location.py:33 msgid "Select the {} cache directory." msgstr "" #. The variable is the name of the source -#: src/importer/sources/location.py:35 +#: cartridges/importer/location.py:35 msgid "Select the {} configuration directory." msgstr "" #. The variable is the name of the source -#: src/importer/sources/location.py:37 +#: cartridges/importer/location.py:37 msgid "Select the {} data directory." msgstr "" -#: src/store/managers/sgdb_manager.py:46 -msgid "Couldn't Authenticate SteamGridDB" -msgstr "" - -#: src/store/managers/sgdb_manager.py:47 -msgid "Verify your API key in preferences" -msgstr "" - -#: src/importer/sources/retroarch_source.py:129 +#: cartridges/importer/retroarch_source.py:129 msgid "No RetroArch Core Selected" msgstr "" #. The variable is a newline separated list of playlists -#: src/importer/sources/retroarch_source.py:131 +#: cartridges/importer/retroarch_source.py:131 msgid "The following playlists have no default core:" msgstr "" -#: src/importer/sources/retroarch_source.py:133 +#: cartridges/importer/retroarch_source.py:133 msgid "Games with no core selected were not imported" msgstr "" + +#: cartridges/store/managers/sgdb_manager.py:46 +msgid "Couldn't Authenticate SteamGridDB" +msgstr "" + +#: cartridges/store/managers/sgdb_manager.py:47 +msgid "Verify your API key in preferences" +msgstr "" diff --git a/search-provider/cartridges-search-provider.in b/search-provider/cartridges-search-provider.in new file mode 100755 index 0000000..982f0b2 --- /dev/null +++ b/search-provider/cartridges-search-provider.in @@ -0,0 +1,292 @@ +#!@PYTHON@ + +# cartridges-search-provider.in +# +# Copyright 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 . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Heavily inspired by: +# https://gitlab.gnome.org/World/lollypop/-/blob/master/search-provider/lollypop-sp.in + +import json + +import gi + +gi.require_version("Gdk", "4.0") +gi.require_version("GdkPixbuf", "2.0") + +# pylint: disable=wrong-import-position +from gi.repository import GdkPixbuf, Gio, GLib + +from cartridges import shared + + +class Server: + def __init__(self, con, path): + method_outargs = {} + method_inargs = {} + for interface in Gio.DBusNodeInfo.new_for_xml(self.__doc__).interfaces: + for method in interface.methods: + method_outargs[method.name] = ( + "(" + "".join([arg.signature for arg in method.out_args]) + ")" + ) + method_inargs[method.name] = tuple( + arg.signature for arg in method.in_args + ) + + con.register_object( + object_path=path, + interface_info=interface, + method_call_closure=self.on_method_call, + ) + + self.method_inargs = method_inargs + self.method_outargs = method_outargs + + def on_method_call( + self, + _connection, + _sender, + _object_path, + _interface_name, + method_name, + parameters, + invocation, + ): + args = list(parameters.unpack()) + for i, sig in enumerate(self.method_inargs[method_name]): + if sig == "h": + msg = invocation.get_message() + fd_list = msg.get_unix_fd_list() + args[i] = fd_list.get(args[i]) + + try: + result = getattr(self, method_name)(*args) + + # out_args is atleast (signature1). + # We therefore always wrap the result as a tuple. + # Refer to https://bugzilla.gnome.org/show_bug.cgi?id=765603 + result = (result,) + + out_args = self.method_outargs[method_name] + if out_args != "()": + variant = GLib.Variant(out_args, result) + invocation.return_value(variant) + else: + invocation.return_value(None) + except Exception: # pylint: disable=broad-exception-caught + pass + + +class SearchCartridgesService(Server, Gio.Application): + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + + # pylint: disable=invalid-name + + __SEARCH_BUS = "org.gnome.Shell.SearchProvider2" + __PATH_BUS = "@PREFIX@/SearchProvider" + + def __init__(self): + Gio.Application.__init__( + self, + application_id="@APP_ID@.SearchProvider", + flags=Gio.ApplicationFlags.IS_SERVICE, + inactivity_timeout=10000, + ) + + self.games = {} + self.load_games_from_disk() + + self.__bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + Gio.bus_own_name_on_connection( + self.__bus, self.__SEARCH_BUS, Gio.BusNameOwnerFlags.NONE, None, None + ) + Server.__init__(self, self.__bus, self.__PATH_BUS) + + def load_games_from_disk(self): + if not shared.games_dir.is_dir(): + return + + for game_file in shared.games_dir.iterdir(): + try: + data = json.load(game_file.open()) + except (OSError, json.decoder.JSONDecodeError): + continue + + try: + if any({data["hidden"], data["blacklisted"], data["removed"]}): + continue + + self.games[data["game_id"]] = (data["name"], data["developer"]) + except KeyError: + continue + + def ActivateResult(self, game_id, _array, _utime): + argv = ["cartridges", "--launch", game_id] + (pid, _stdin, _stdout, _stderr) = GLib.spawn_async( + argv, + flags=GLib.SpawnFlags.SEARCH_PATH, + standard_input=False, + standard_output=False, + standard_error=False, + ) + GLib.spawn_close_pid(pid) + + def GetInitialResultSet(self, terms): + return self.__search(terms) + + def GetResultMetas(self, game_ids): + results = [] + + try: + for game_id in game_ids: + empty_pixbuf = GdkPixbuf.Pixbuf.new( + GdkPixbuf.Colorspace.RGB, True, 8, 32, 32 + ) + pixbuf = None + if (path := shared.covers_dir / (game_id + ".tiff")).is_file(): + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + str(path), -1, 32, True + ) + except GLib.GError as e: + print(e) + continue + elif (path := shared.covers_dir / (game_id + ".gif")).is_file(): + try: + pixbuf = GdkPixbuf.PixbufAnimation.new_from_file( + str(path) + ).get_static_image() + except GLib.GError as e: + print(e) + continue + d = { + "id": GLib.Variant("s", game_id), + "name": GLib.Variant("s", self.games[game_id][0]), + } + if pixbuf: + pixbuf.composite( + empty_pixbuf, + 6, + 0, + 21, + 32, + 6, + 0, + 21 / pixbuf.get_width(), + 32 / pixbuf.get_height(), + GdkPixbuf.InterpType.NEAREST, + 255, + ) + + d["icon-data"] = GLib.Variant( + "(iiibiiay)", + [ + empty_pixbuf.get_width(), + empty_pixbuf.get_height(), + empty_pixbuf.get_rowstride(), + empty_pixbuf.get_has_alpha(), + empty_pixbuf.get_bits_per_sample(), + empty_pixbuf.get_n_channels(), + empty_pixbuf.read_pixel_bytes().get_data(), + ], + ) + if self.games[game_id][1]: + d["description"] = GLib.Variant( + "s", GLib.markup_escape_text(self.games[game_id][1]) + ) + results.append(d) + except Exception as e: # pylint: disable=broad-exception-caught + print("SearchCartridgesService::GetResultMetas():", e) + return [] + return results + + def GetSubsearchResultSet(self, _previous_results, new_terms): + return self.__search(new_terms) + + def LaunchSearch(self, terms, _utime): + search = " ".join(terms) + argv = ["cartridges", "--search", search] + (pid, _stdin, _stdout, _stderr) = GLib.spawn_async( + argv, + flags=GLib.SpawnFlags.SEARCH_PATH, + standard_input=False, + standard_output=False, + standard_error=False, + ) + GLib.spawn_close_pid(pid) + + def __search(self, terms): + game_ids = [] + search = " ".join(terms).lower() + try: + for game_id, data in self.games.items(): + print(game_id, data) + if search in data[0].lower(): + game_ids.append(game_id) + continue + if data[1] and search in data[1].lower(): + game_ids.append(game_id) + continue + except Exception as e: # pylint: disable=broad-exception-caught + print("SearchCartridgesService::__search():", e) + return game_ids + + +def main(): + service = SearchCartridgesService() + service.run() + + +if __name__ == "__main__": + main() diff --git a/search-provider/hu.kramo.Cartridges.SearchProvider.ini b/search-provider/hu.kramo.Cartridges.SearchProvider.ini new file mode 100644 index 0000000..be27533 --- /dev/null +++ b/search-provider/hu.kramo.Cartridges.SearchProvider.ini @@ -0,0 +1,5 @@ +[Shell Search Provider] +DesktopId=@APP_ID@.desktop +BusName=@APP_ID@.SearchProvider +ObjectPath=@PREFIX@/SearchProvider +Version=2 \ No newline at end of file diff --git a/search-provider/hu.kramo.Cartridges.SearchProvider.service.in b/search-provider/hu.kramo.Cartridges.SearchProvider.service.in new file mode 100644 index 0000000..6174ebd --- /dev/null +++ b/search-provider/hu.kramo.Cartridges.SearchProvider.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=@APP_ID@.SearchProvider +Exec=@libexecdir@/cartridges-search-provider \ No newline at end of file diff --git a/search-provider/meson.build b/search-provider/meson.build new file mode 100644 index 0000000..fc19c87 --- /dev/null +++ b/search-provider/meson.build @@ -0,0 +1,25 @@ +# Heavily inspired by https://gitlab.gnome.org/World/lollypop/-/blob/master/search-provider/meson.build + +service_dir = join_paths(get_option('datadir'), 'dbus-1', 'services') +serarch_provider_dir = join_paths(get_option('datadir'), 'gnome-shell', 'search-providers') + +configure_file( + input: 'cartridges-search-provider.in', + output: 'cartridges-search-provider', + configuration: conf, + install_dir: libexecdir +) + +configure_file( + input: 'hu.kramo.Cartridges.SearchProvider.service.in', + output: app_id + '.SearchProvider.service', + configuration: conf, + install_dir: service_dir +) + +configure_file( + input: 'hu.kramo.Cartridges.SearchProvider.ini', + output: app_id + '.SearchProvider.ini', + configuration: conf, + install_dir: serarch_provider_dir +) \ No newline at end of file