From c2c998adcd5408e235b57a3fff9ab0acb78c0be2 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sun, 13 Aug 2023 18:13:17 +0200 Subject: [PATCH 01/80] Created source subclass, improved RetroArch exec Steam RetroArch still not working on my machine. --- src/importer/sources/flatpak_source.py | 4 +-- src/importer/sources/legendary_source.py | 8 +++-- src/importer/sources/retroarch_source.py | 45 +++++++++++++++++++----- src/importer/sources/source.py | 23 +++++++++--- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index ee4ebbe..b4af7c4 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -26,7 +26,7 @@ from gi.repository import GLib, Gtk from src import shared from src.game import Game from src.importer.sources.location import Location, LocationSubPath -from src.importer.sources.source import Source, SourceIterable +from src.importer.sources.source import ExecutableFormatSource, SourceIterable class FlatpakSourceIterable(SourceIterable): @@ -116,7 +116,7 @@ class FlatpakLocations(NamedTuple): data: Location -class FlatpakSource(Source): +class FlatpakSource(ExecutableFormatSource): """Generic Flatpak source""" source_id = "flatpak" diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 03bbdd2..6c46ada 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -26,7 +26,11 @@ from typing import NamedTuple from src import shared from src.game import Game from src.importer.sources.location import Location, LocationSubPath -from src.importer.sources.source import Source, SourceIterationResult, SourceIterable +from src.importer.sources.source import ( + ExecutableFormatSource, + SourceIterationResult, + SourceIterable, +) class LegendarySourceIterable(SourceIterable): @@ -93,7 +97,7 @@ class LegendaryLocations(NamedTuple): config: Location -class LegendarySource(Source): +class LegendarySource(ExecutableFormatSource): source_id = "legendary" name = _("Legendary") executable_format = "legendary launch {app_name}" diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 96b16e1..b5dc4fb 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -26,6 +26,8 @@ from pathlib import Path from time import time from typing import NamedTuple +from urllib.parse import quote + from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game @@ -103,7 +105,7 @@ class RetroarchSourceIterable(SourceIterable): "added": added_time, "name": item["label"], "game_id": self.source.game_id_format.format(game_id=game_id), - "executable": self.source.executable_format.format( + "executable": self.source.make_executable( rom_path=item["path"], core_path=core_path, ), @@ -168,14 +170,6 @@ class RetroarchSource(Source): ) ) - @property - def executable_format(self): - self.locations.config.resolve() - is_flatpak = self.locations.config.root.is_relative_to(shared.flatpak_dir) - base = "flatpak run org.libretro.RetroArch" if is_flatpak else "retroarch" - args = '-L "{core_path}" "{rom_path}"' - return f"{base} {args}" - def __init__(self) -> None: super().__init__() try: @@ -214,3 +208,36 @@ class RetroarchSource(Source): return Path(f"{library_path}/steamapps/common/RetroArch") # Not found raise ValueError("RetroArch not found in Steam library") + + def make_executable(self, rom_path: Path, core_path: Path) -> str: + """ + Generate an executable command from the rom path and core path, + depending on the source's location. + + The format depends on RetroArch's installation method, + detected from the source config location + + :param Path rom_path: the game's rom path + :param Path core_path: the game's core path + :return str: an executable command + """ + + self.locations.config.resolve() + args = f'-L "{core_path}" "{rom_path}"' + + # Steam RetroArch + # (Must check before Flatpak, because Steam itself can be installed as one) + if self.locations.config.root.parent.parent.name == "steamapps": + uri = "steam://run/1118310//" + quote(args) + "/" + return f"xdg-open {uri}" + + # Flatpak RetroArch + if self.locations.config.root.is_relative_to(shared.flatpak_dir): + return f"flatpak run org.libretro.RetroArch {args}" + + # TODO executable override for non-sandboxed sources + + # Linux native RetroArch + return f"retroarch {args}" + + # TODO implement for windows (needs override) diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 26b2a84..e434d14 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -75,10 +75,12 @@ class Source(Iterable): def is_available(self): return sys.platform in self.available_on - @property @abstractmethod - def executable_format(self) -> str: - """The executable format used to construct game executables""" + def make_executable(self, *args, **kwargs) -> str: + """ + Create a game executable command. + Should be implemented by child classes. + """ def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ @@ -93,8 +95,21 @@ class Source(Iterable): return iter(self.iterable_class(self)) +class ExecutableFormatSource(Source): + """Source class that uses a simple executable format to start games""" + + @property + @abstractmethod + def executable_format(self) -> str: + """The executable format used to construct game executables""" + + def make_executable(self, *args, **kwargs) -> str: + """Use the executable format to""" + return self.executable_format.format(args, kwargs) + + # pylint: disable=abstract-method -class URLExecutableSource(Source): +class URLExecutableSource(ExecutableFormatSource): """Source class that use custom URLs to start games""" url_format: str From 86ac95641ca6717666aaf54e2760ff4d84928b36 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sun, 13 Aug 2023 19:18:54 +0200 Subject: [PATCH 02/80] Work on Retroarch Steam executable --- src/importer/sources/retroarch_source.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index b5dc4fb..7556f86 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -26,8 +26,6 @@ from pathlib import Path from time import time from typing import NamedTuple -from urllib.parse import quote - from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game @@ -223,13 +221,20 @@ class RetroarchSource(Source): """ self.locations.config.resolve() - args = f'-L "{core_path}" "{rom_path}"' + args = f"-L '{core_path}' '{rom_path}'" # Steam RetroArch # (Must check before Flatpak, because Steam itself can be installed as one) if self.locations.config.root.parent.parent.name == "steamapps": - uri = "steam://run/1118310//" + quote(args) + "/" - return f"xdg-open {uri}" + # TODO fix the RetroArch Steam arguments + # It seems that the space after "-L" is parsed as the value for that arg. + # The URI protocol is proprietary, a community doc is available: + # https://developer.valvesoftware.com/wiki/Steam_browser_protocol + # ... But it doesn't sepcify HOW the args should be formatted. + # Space delimited? Quoted individually? URL-Encoded? + # I don't know. It no workie :D + uri = f"steam://run/1118310//{args}/" + return f'xdg-open "{uri}"' # Flatpak RetroArch if self.locations.config.root.is_relative_to(shared.flatpak_dir): From 1c2c844f898de2c6f008ecaf5ed1030f4d136293 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Tue, 15 Aug 2023 02:52:17 +0200 Subject: [PATCH 03/80] Disabled Steam RetroArch candidate --- src/importer/sources/retroarch_source.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 7556f86..260dae7 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -170,6 +170,11 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() + # TODO enable when RetroArch Steam's executable issue is resolved + # self.add_steam_location_candidate() + + def add_steam_location_candidate(self) -> None: + """Add the Steam RetroAcrh location to the config candidates""" try: self.locations.config.candidates.append(self.get_steam_location()) except (OSError, KeyError, UnresolvableLocationError): @@ -225,16 +230,10 @@ class RetroarchSource(Source): # Steam RetroArch # (Must check before Flatpak, because Steam itself can be installed as one) - if self.locations.config.root.parent.parent.name == "steamapps": - # TODO fix the RetroArch Steam arguments - # It seems that the space after "-L" is parsed as the value for that arg. - # The URI protocol is proprietary, a community doc is available: - # https://developer.valvesoftware.com/wiki/Steam_browser_protocol - # ... But it doesn't sepcify HOW the args should be formatted. - # Space delimited? Quoted individually? URL-Encoded? - # I don't know. It no workie :D - uri = f"steam://run/1118310//{args}/" - return f'xdg-open "{uri}"' + # TODO enable if/when we can pass args with steam://run + # if self.locations.config.root.parent.parent.name == "steamapps": + # uri = f"steam://run/1118310//{args}/" + # return f'xdg-open "{uri}"' # Flatpak RetroArch if self.locations.config.root.is_relative_to(shared.flatpak_dir): From f8037e25427008a396032b8510db0b808f6913f6 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 16 Aug 2023 02:34:59 +0200 Subject: [PATCH 04/80] Improved Steam RetroArch command generation - Verified that libretro core is selected - Verified that rom is selected - Ensure proper quoting --- src/importer/sources/retroarch_source.py | 27 ++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 260dae7..426b4ee 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -23,8 +23,10 @@ import re from hashlib import md5 from json import JSONDecodeError from pathlib import Path +from shlex import quote as shell_quote from time import time from typing import NamedTuple +from urllib.parse import quote as url_quote from src import shared from src.errors.friendly_error import FriendlyError @@ -104,8 +106,8 @@ class RetroarchSourceIterable(SourceIterable): "name": item["label"], "game_id": self.source.game_id_format.format(game_id=game_id), "executable": self.source.make_executable( - rom_path=item["path"], core_path=core_path, + rom_path=item["path"], ), } @@ -170,8 +172,7 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() - # TODO enable when RetroArch Steam's executable issue is resolved - # self.add_steam_location_candidate() + self.add_steam_location_candidate() def add_steam_location_candidate(self) -> None: """Add the Steam RetroAcrh location to the config candidates""" @@ -212,7 +213,7 @@ class RetroarchSource(Source): # Not found raise ValueError("RetroArch not found in Steam library") - def make_executable(self, rom_path: Path, core_path: Path) -> str: + def make_executable(self, core_path: Path, rom_path: Path) -> str: """ Generate an executable command from the rom path and core path, depending on the source's location. @@ -226,22 +227,26 @@ class RetroarchSource(Source): """ self.locations.config.resolve() - args = f"-L '{core_path}' '{rom_path}'" + args = ("-L", core_path, rom_path) # Steam RetroArch # (Must check before Flatpak, because Steam itself can be installed as one) - # TODO enable if/when we can pass args with steam://run - # if self.locations.config.root.parent.parent.name == "steamapps": - # uri = f"steam://run/1118310//{args}/" - # return f'xdg-open "{uri}"' + if self.locations.config.root.parent.parent.name == "steamapps": + # steam://run exepects args to be url-encoded and separated by spaces. + args = map(lambda s: url_quote(str(s), safe=""), args) + args_str = " ".join(args) + uri = f"steam://run/1118310//{args_str}/" + return f"xdg-open {shell_quote(uri)}" # Flatpak RetroArch + args = map(lambda s: shell_quote(str(s)), args) + args_str = " ".join(args) if self.locations.config.root.is_relative_to(shared.flatpak_dir): - return f"flatpak run org.libretro.RetroArch {args}" + return f"flatpak run org.libretro.RetroArch {args_str}" # TODO executable override for non-sandboxed sources # Linux native RetroArch - return f"retroarch {args}" + return f"retroarch {args_str}" # TODO implement for windows (needs override) From 0e2918cfe868731ef9ae70f64c129ff13c8781e7 Mon Sep 17 00:00:00 2001 From: kramo Date: Wed, 16 Aug 2023 17:22:12 +0200 Subject: [PATCH 05/80] More sensible approach to screen reader in popups --- data/gtk/details-window.blp | 1 - src/details_window.py | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/data/gtk/details-window.blp b/data/gtk/details-window.blp index d84b4f8..272e2eb 100644 --- a/data/gtk/details-window.blp +++ b/data/gtk/details-window.blp @@ -127,7 +127,6 @@ template $DetailsWindow : Adw.Window { margin-bottom: 6; margin-start: 6; margin-end: 6; - selectable: true; } }; diff --git a/src/details_window.py b/src/details_window.py index e3cff21..3a96916 100644 --- a/src/details_window.py +++ b/src/details_window.py @@ -48,6 +48,7 @@ class DetailsWindow(Adw.Window): developer = Gtk.Template.Child() executable = Gtk.Template.Child() + exec_info_button = Gtk.Template.Child() exec_info_label = Gtk.Template.Child() exec_info_popover = Gtk.Template.Child() @@ -114,10 +115,18 @@ class DetailsWindow(Adw.Window): self.exec_info_label.set_label(exec_info_text) - def clear_info_selection(*_args: Any) -> None: - self.exec_info_label.select_region(-1, -1) + self.exec_info_popover.set_focusable(True) + self.exec_info_popover.update_property( + (Gtk.AccessibleProperty.LABEL,), + ( + exec_info_text.replace("", "").replace("", ""), + ), # Remove formatting, else the screen reader reads it + ) - self.exec_info_popover.connect("show", clear_info_selection) + def set_exec_info_a11y_label(*_args: Any) -> None: + self.set_focus(self.exec_info_popover) + + self.exec_info_popover.connect("show", set_exec_info_a11y_label) self.cover_button_delete.connect("clicked", self.delete_pixbuf) self.cover_button_edit.connect("clicked", self.choose_cover) From a3aa7f9ccf8f87f043e4eb051291597d01c2e83d Mon Sep 17 00:00:00 2001 From: kramo Date: Wed, 16 Aug 2023 17:31:54 +0200 Subject: [PATCH 06/80] Cleanups --- data/gtk/details-window.blp | 1 + src/details_window.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/data/gtk/details-window.blp b/data/gtk/details-window.blp index 272e2eb..21d12c1 100644 --- a/data/gtk/details-window.blp +++ b/data/gtk/details-window.blp @@ -116,6 +116,7 @@ template $DetailsWindow : Adw.Window { tooltip-text: _("More Info"); popover: Popover exec_info_popover { + focusable: true; Label exec_info_label { use-markup: true; diff --git a/src/details_window.py b/src/details_window.py index 3a96916..ead7710 100644 --- a/src/details_window.py +++ b/src/details_window.py @@ -48,7 +48,6 @@ class DetailsWindow(Adw.Window): developer = Gtk.Template.Child() executable = Gtk.Template.Child() - exec_info_button = Gtk.Template.Child() exec_info_label = Gtk.Template.Child() exec_info_popover = Gtk.Template.Child() @@ -115,7 +114,6 @@ class DetailsWindow(Adw.Window): self.exec_info_label.set_label(exec_info_text) - self.exec_info_popover.set_focusable(True) self.exec_info_popover.update_property( (Gtk.AccessibleProperty.LABEL,), ( From eeb18eb0176a19f305972f6b2c985aa26e67d2a5 Mon Sep 17 00:00:00 2001 From: kramo Date: Wed, 16 Aug 2023 19:18:03 +0200 Subject: [PATCH 07/80] Typing --- src/details_window.py | 4 +- src/errors/error_producer.py | 6 +-- src/errors/friendly_error.py | 6 +-- src/game.py | 8 ++-- src/game_cover.py | 20 ++++----- src/importer/importer.py | 67 ++++++++++++++++------------- src/importer/sources/location.py | 10 +++-- src/importer/sources/source.py | 12 +++--- src/logging/color_log_formatter.py | 2 +- src/logging/session_file_handler.py | 10 +++-- src/logging/setup.py | 4 +- src/main.py | 6 +-- src/preferences.py | 4 +- src/store/managers/manager.py | 12 +++--- src/store/pipeline.py | 2 +- src/store/store.py | 12 +++--- src/window.py | 6 +-- 17 files changed, 102 insertions(+), 89 deletions(-) diff --git a/src/details_window.py b/src/details_window.py index ead7710..bb234da 100644 --- a/src/details_window.py +++ b/src/details_window.py @@ -19,7 +19,7 @@ import os from time import time -from typing import Any +from typing import Any, Optional from gi.repository import Adw, Gio, GLib, Gtk from PIL import Image @@ -55,7 +55,7 @@ class DetailsWindow(Adw.Window): cover_changed: bool = False - def __init__(self, game: Game | None = None, **kwargs: Any): + def __init__(self, game: Optional[Game] = None, **kwargs: Any): super().__init__(**kwargs) self.game: Game = game diff --git a/src/errors/error_producer.py b/src/errors/error_producer.py index 7f31fea..f3c9b53 100644 --- a/src/errors/error_producer.py +++ b/src/errors/error_producer.py @@ -8,14 +8,14 @@ class ErrorProducer: Specifies the report_error and collect_errors methods in a thread-safe manner. """ - errors: list[Exception] = None - errors_lock: Lock = None + errors: list[Exception] + errors_lock: Lock def __init__(self) -> None: self.errors = [] self.errors_lock = Lock() - def report_error(self, error: Exception): + def report_error(self, error: Exception) -> None: """Report an error""" with self.errors_lock: self.errors.append(error) diff --git a/src/errors/friendly_error.py b/src/errors/friendly_error.py index 4c9ca7f..586d784 100644 --- a/src/errors/friendly_error.py +++ b/src/errors/friendly_error.py @@ -1,4 +1,4 @@ -from typing import Iterable +from typing import Iterable, Optional class FriendlyError(Exception): @@ -27,8 +27,8 @@ class FriendlyError(Exception): self, title: str, subtitle: str, - title_args: Iterable[str] = None, - subtitle_args: Iterable[str] = None, + title_args: Optional[Iterable[str]] = None, + subtitle_args: Optional[Iterable[str]] = None, ) -> None: """Create a friendly error diff --git a/src/game.py b/src/game.py index edeaad4..e144460 100644 --- a/src/game.py +++ b/src/game.py @@ -23,7 +23,7 @@ import shlex import subprocess from pathlib import Path from time import time -from typing import Any +from typing import Any, Optional from gi.repository import Adw, GLib, GObject, Gtk @@ -57,7 +57,7 @@ class Game(Gtk.Box): hidden: bool = False last_played: int = 0 name: str - developer: str | None = None + developer: Optional[str] = None removed: bool = False blacklisted: bool = False game_cover: GameCover = None @@ -97,7 +97,7 @@ class Game(Gtk.Box): def save(self) -> None: self.emit("save-ready", {}) - def create_toast(self, title: str, action: str | None = None) -> None: + def create_toast(self, title: str, action: Optional[str] = None) -> None: toast = Adw.Toast.new(title.format(self.name)) toast.set_priority(Adw.ToastPriority.HIGH) @@ -180,7 +180,7 @@ class Game(Gtk.Box): self.cover.set_opacity(int(not loading)) self.spinner.set_spinning(loading) - def get_cover_path(self) -> Path | None: + def get_cover_path(self) -> Optional[Path]: cover_path = shared.covers_dir / f"{self.game_id}.gif" if cover_path.is_file(): return cover_path # type: ignore diff --git a/src/game_cover.py b/src/game_cover.py index 24ebf09..221075d 100644 --- a/src/game_cover.py +++ b/src/game_cover.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from pathlib import Path -from typing import Any, Callable +from typing import Any, Callable, Optional from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk from PIL import Image, ImageFilter, ImageStat @@ -27,12 +27,12 @@ from src import shared class GameCover: - texture: Gdk.Texture | None - blurred: Gdk.Texture | None - luminance: tuple[float, float] | None - path: Path | None - animation: GdkPixbuf.PixbufAnimation | None - anim_iter: GdkPixbuf.PixbufAnimationIter | None + texture: Optional[Gdk.Texture] + blurred: Optional[Gdk.Texture] + luminance: Optional[tuple[float, float]] + path: Optional[Path] + animation: Optional[GdkPixbuf.PixbufAnimation] + anim_iter: Optional[GdkPixbuf.PixbufAnimationIter] placeholder = Gdk.Texture.new_from_resource( shared.PREFIX + "/library_placeholder.svg" @@ -41,12 +41,12 @@ class GameCover: shared.PREFIX + "/library_placeholder_small.svg" ) - def __init__(self, pictures: set[Gtk.Picture], path: Path | None = None) -> None: + def __init__(self, pictures: set[Gtk.Picture], path: Optional[Path] = None) -> None: self.pictures = pictures self.new_cover(path) # Wrap the function in another one as Gio.Task.run_in_thread does not allow for passing args - def create_func(self, path: Path | None) -> Callable: + def create_func(self, path: Optional[Path]) -> Callable: self.animation = GdkPixbuf.PixbufAnimation.new_from_file(str(path)) self.anim_iter = self.animation.get_iter() @@ -55,7 +55,7 @@ class GameCover: return wrapper - def new_cover(self, path: Path | None = None) -> None: + def new_cover(self, path: Optional[Path] = None) -> None: self.animation = None self.texture = None self.blurred = None diff --git a/src/importer/importer.py b/src/importer/importer.py index dbcc715..e987760 100644 --- a/src/importer/importer.py +++ b/src/importer/importer.py @@ -19,6 +19,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import logging +from typing import Any, Optional from gi.repository import Adw, GLib, Gtk @@ -37,22 +38,22 @@ from src.utils.task import Task class Importer(ErrorProducer): """A class in charge of scanning sources for games""" - progressbar = None - import_statuspage = None - import_dialog = None - summary_toast = None + progressbar: Gtk.ProgressBar + import_statuspage: Adw.StatusPage + import_dialog: Adw.MessageDialog + summary_toast: Adw.Toast - sources: set[Source] = None + sources: set[Source] n_source_tasks_created: int = 0 n_source_tasks_done: int = 0 n_pipelines_done: int = 0 - game_pipelines: set[Pipeline] = None + game_pipelines: set[Pipeline] removed_game_ids: set[str] = set() imported_game_ids: set[str] = set() - def __init__(self): + def __init__(self) -> None: super().__init__() # TODO: make this stateful @@ -63,23 +64,23 @@ class Importer(ErrorProducer): self.sources = set() @property - def n_games_added(self): + def n_games_added(self) -> int: return sum( 1 if not (pipeline.game.blacklisted or pipeline.game.removed) else 0 for pipeline in self.game_pipelines ) @property - def pipelines_progress(self): + def pipelines_progress(self) -> float: progress = sum(pipeline.progress for pipeline in self.game_pipelines) try: progress = progress / len(self.game_pipelines) except ZeroDivisionError: progress = 0 - return progress + return progress # type: ignore @property - def sources_progress(self): + def sources_progress(self) -> float: try: progress = self.n_source_tasks_done / self.n_source_tasks_created except ZeroDivisionError: @@ -87,16 +88,16 @@ class Importer(ErrorProducer): return progress @property - def finished(self): + def finished(self) -> bool: return ( self.n_source_tasks_created == self.n_source_tasks_done and len(self.game_pipelines) == self.n_pipelines_done ) - def add_source(self, source): + def add_source(self, source: Source) -> None: self.sources.add(source) - def run(self): + def run(self) -> None: """Use several Gio.Task to import games from added sources""" shared.win.get_application().lookup_action("import").set_enabled(False) @@ -121,7 +122,7 @@ class Importer(ErrorProducer): self.progress_changed_callback() - def create_dialog(self): + def create_dialog(self) -> None: """Create the import dialog""" self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12) self.import_statuspage = Adw.StatusPage( @@ -138,7 +139,9 @@ class Importer(ErrorProducer): ) self.import_dialog.present() - def source_task_thread_func(self, _task, _obj, data, _cancellable): + def source_task_thread_func( + self, _task: Any, _obj: Any, data: tuple, _cancellable: Any + ) -> None: """Source import task code""" source: Source @@ -192,27 +195,27 @@ class Importer(ErrorProducer): pipeline.connect("advanced", self.pipeline_advanced_callback) self.game_pipelines.add(pipeline) - def update_progressbar(self): + def update_progressbar(self) -> None: """Update the progressbar to show the overall import progress""" # Reserve 10% for the sources discovery, the rest is the pipelines self.progressbar.set_fraction( (0.1 * self.sources_progress) + (0.9 * self.pipelines_progress) ) - def source_callback(self, _obj, _result, data): + def source_callback(self, _obj: Any, _result: Any, data: tuple) -> None: """Callback executed when a source is fully scanned""" source, *_rest = data logging.debug("Import done for source %s", source.source_id) self.n_source_tasks_done += 1 self.progress_changed_callback() - def pipeline_advanced_callback(self, pipeline: Pipeline): + def pipeline_advanced_callback(self, pipeline: Pipeline) -> None: """Callback called when a pipeline for a game has advanced""" if pipeline.is_done: self.n_pipelines_done += 1 self.progress_changed_callback() - def progress_changed_callback(self): + def progress_changed_callback(self) -> None: """ Callback called when the import process has progressed @@ -225,7 +228,7 @@ class Importer(ErrorProducer): if self.finished: self.import_callback() - def remove_games(self): + def remove_games(self) -> None: """Set removed to True for missing games""" if not shared.schema.get_boolean("remove-missing"): return @@ -249,7 +252,7 @@ class Importer(ErrorProducer): game.update() self.removed_game_ids.add(game.game_id) - def import_callback(self): + def import_callback(self) -> None: """Callback called when importing has finished""" logging.info("Import done") self.remove_games() @@ -261,11 +264,11 @@ class Importer(ErrorProducer): self.create_error_dialog() shared.win.get_application().lookup_action("import").set_enabled(True) - def create_error_dialog(self): + def create_error_dialog(self) -> None: """Dialog containing all errors raised by importers""" # Collect all errors that happened in the importer and the managers - errors: list[Exception] = [] + errors = [] errors.extend(self.collect_errors()) for manager in shared.store.managers.values(): errors.extend(manager.collect_errors()) @@ -313,7 +316,7 @@ class Importer(ErrorProducer): dialog.present() - def undo_import(self, *_args): + def undo_import(self, *_args: Any) -> None: for game_id in self.imported_game_ids: shared.store[game_id].removed = True shared.store[game_id].update() @@ -330,7 +333,7 @@ class Importer(ErrorProducer): logging.info("Import undone") - def create_summary_toast(self): + def create_summary_toast(self) -> Adw.Toast: """N games imported, removed toast""" toast = Adw.Toast(timeout=0, priority=Adw.ToastPriority.HIGH) @@ -371,16 +374,20 @@ class Importer(ErrorProducer): shared.win.toast_overlay.add_toast(toast) return toast - def open_preferences(self, page=None, expander_row=None): + def open_preferences( + self, + page_name: Optional[str] = None, + expander_row: Optional[Adw.ExpanderRow] = None, + ) -> Adw.PreferencesWindow: return shared.win.get_application().on_preferences_action( - page_name=page, expander_row=expander_row + page_name=page_name, expander_row=expander_row ) - def timeout_toast(self, *_args): + def timeout_toast(self, *_args: Any) -> None: """Manually timeout the toast after the user has dismissed all warnings""" GLib.timeout_add_seconds(5, self.summary_toast.dismiss) - def dialog_response_callback(self, _widget, response, *args): + def dialog_response_callback(self, _widget: Any, response: str, *args: Any) -> None: """Handle after-import dialogs callback""" logging.debug("After-import dialog response: %s (%s)", response, str(args)) if response == "open_preferences": diff --git a/src/importer/sources/location.py b/src/importer/sources/location.py index dde8e3f..36f592b 100644 --- a/src/importer/sources/location.py +++ b/src/importer/sources/location.py @@ -1,7 +1,7 @@ import logging from os import PathLike from pathlib import Path -from typing import Iterable, Mapping, NamedTuple +from typing import Iterable, Mapping, NamedTuple, Optional from src import shared @@ -41,7 +41,7 @@ class Location: paths: Mapping[str, LocationSubPath] invalid_subtitle: str - root: Path = None + root: Optional[Path] = None def __init__( self, @@ -94,7 +94,9 @@ class Location: shared.schema.set_string(self.schema_key, value) logging.debug("Resolved value for schema key %s: %s", self.schema_key, value) - def __getitem__(self, key: str): + def __getitem__(self, key: str) -> Optional[Path]: """Get the computed path from its key for the location""" self.resolve() - return self.root / self.paths[key].segment + if self.root: + return self.root / self.paths[key].segment + return None diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 499881a..74a13eb 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -20,19 +20,19 @@ import sys from abc import abstractmethod from collections.abc import Iterable -from typing import Any, Collection, Generator +from typing import Any, Collection, Generator, Optional from src.game import Game from src.importer.sources.location import Location # Type of the data returned by iterating on a Source -SourceIterationResult = None | Game | tuple[Game, tuple[Any]] +SourceIterationResult = Optional[Game | tuple[Game, tuple[Any]]] class SourceIterable(Iterable): """Data producer for a source of games""" - source: "Source" = None + source: "Source" def __init__(self, source: "Source") -> None: self.source = source @@ -53,7 +53,7 @@ class Source(Iterable): source_id: str name: str - variant: str = None + variant: Optional[str] = None available_on: set[str] = set() iterable_class: type[SourceIterable] @@ -65,7 +65,7 @@ class Source(Iterable): def full_name(self) -> str: """The source's full name""" full_name_ = self.name - if self.variant is not None: + if self.variant: full_name_ += f" ({self.variant})" return full_name_ @@ -75,7 +75,7 @@ class Source(Iterable): return self.source_id + "_{game_id}" @property - def is_available(self): + def is_available(self) -> bool: return sys.platform in self.available_on @property diff --git a/src/logging/color_log_formatter.py b/src/logging/color_log_formatter.py index 53fe261..6f5545e 100644 --- a/src/logging/color_log_formatter.py +++ b/src/logging/color_log_formatter.py @@ -29,7 +29,7 @@ class ColorLogFormatter(Formatter): RED = "\033[31m" YELLOW = "\033[33m" - def format(self, record: LogRecord): + def format(self, record: LogRecord) -> str: super_format = super().format(record) match record.levelname: case "CRITICAL": diff --git a/src/logging/session_file_handler.py b/src/logging/session_file_handler.py index bc6e06e..a6f46b3 100644 --- a/src/logging/session_file_handler.py +++ b/src/logging/session_file_handler.py @@ -18,11 +18,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later import lzma -from io import StringIO +from io import TextIOWrapper from logging import StreamHandler from lzma import FORMAT_XZ, PRESET_DEFAULT from os import PathLike from pathlib import Path +from typing import Optional from src import shared @@ -37,7 +38,7 @@ class SessionFileHandler(StreamHandler): backup_count: int filename: Path - log_file: StringIO = None + log_file: Optional[TextIOWrapper] = None def create_dir(self) -> None: """Create the log dir if needed""" @@ -83,7 +84,7 @@ class SessionFileHandler(StreamHandler): logfiles.sort(key=self.file_sort_key, reverse=True) return logfiles - def rotate_file(self, path: Path): + def rotate_file(self, path: Path) -> None: """Rotate a file's number suffix and remove it if it's too old""" # If uncompressed, compress @@ -128,5 +129,6 @@ class SessionFileHandler(StreamHandler): super().__init__(self.log_file) def close(self) -> None: - self.log_file.close() + if self.log_file: + self.log_file.close() super().close() diff --git a/src/logging/setup.py b/src/logging/setup.py index e9737cd..f3498cc 100644 --- a/src/logging/setup.py +++ b/src/logging/setup.py @@ -27,7 +27,7 @@ import sys from src import shared -def setup_logging(): +def setup_logging() -> None: """Intitate the app's logging""" is_dev = shared.PROFILE == "development" @@ -89,7 +89,7 @@ def setup_logging(): logging_dot_config.dictConfig(config) -def log_system_info(): +def log_system_info() -> None: """Log system debug information""" logging.debug("Starting %s v%s (%s)", shared.APP_ID, shared.VERSION, shared.PROFILE) diff --git a/src/main.py b/src/main.py index a1e3fdf..6c59d22 100644 --- a/src/main.py +++ b/src/main.py @@ -21,7 +21,7 @@ import json import lzma import os import sys -from typing import Any +from typing import Any, Optional import gi @@ -199,8 +199,8 @@ class CartridgesApplication(Adw.Application): self, _action: Any = None, _parameter: Any = None, - page_name: (str | None) = None, - expander_row: (str | None) = None, + page_name: Optional[str] = None, + expander_row: Optional[str] = None, ) -> CartridgesWindow: win = PreferencesWindow() if page_name: diff --git a/src/preferences.py b/src/preferences.py index c2f2596..9adf84b 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -21,7 +21,7 @@ import logging import re from pathlib import Path from shutil import rmtree -from typing import Any, Callable +from typing import Any, Callable, Optional from gi.repository import Adw, Gio, GLib, Gtk @@ -215,7 +215,7 @@ class PreferencesWindow(Adw.PreferencesWindow): ) def choose_folder( - self, _widget: Any, callback: Callable, callback_data: str | None = None + self, _widget: Any, callback: Callable, callback_data: Optional[str] = None ) -> None: self.file_chooser.select_folder(self.win, None, callback, callback_data) diff --git a/src/store/managers/manager.py b/src/store/managers/manager.py index 4ef50d0..a727a22 100644 --- a/src/store/managers/manager.py +++ b/src/store/managers/manager.py @@ -46,7 +46,7 @@ class Manager(ErrorProducer): max_tries: int = 3 @property - def name(self): + def name(self) -> str: return type(self).__name__ @abstractmethod @@ -59,13 +59,13 @@ class Manager(ErrorProducer): * May raise other exceptions that will be reported """ - def run(self, game: Game, additional_data: dict): + def run(self, game: Game, additional_data: dict) -> None: """Handle errors (retry, ignore or raise) that occur in the manager logic""" # Keep track of the number of tries tries = 1 - def handle_error(error: Exception): + def handle_error(error: Exception) -> None: nonlocal tries # If FriendlyError, handle its cause instead @@ -83,11 +83,11 @@ class Manager(ErrorProducer): retrying_format = "Retrying %s in %s for %s" unretryable_format = "Unretryable %s in %s for %s" - if error in self.continue_on: + if type(error) in self.continue_on: # Handle skippable errors (skip silently) return - if error in self.retryable_on: + if type(error) in self.retryable_on: if tries > self.max_tries: # Handle being out of retries logging.error(out_of_retries_format, *log_args) @@ -104,7 +104,7 @@ class Manager(ErrorProducer): logging.error(unretryable_format, *log_args, exc_info=error) self.report_error(base_error) - def try_manager_logic(): + def try_manager_logic() -> None: try: self.main(game, additional_data) except Exception as error: # pylint: disable=broad-exception-caught diff --git a/src/store/pipeline.py b/src/store/pipeline.py index 559ee7f..f552f04 100644 --- a/src/store/pipeline.py +++ b/src/store/pipeline.py @@ -83,7 +83,7 @@ class Pipeline(GObject.Object): progress = 1 return progress - def advance(self): + def advance(self) -> None: """Spawn tasks for managers that are able to run for a game""" # Separate blocking / async managers diff --git a/src/store/store.py b/src/store/store.py index a11bf5a..159da90 100644 --- a/src/store/store.py +++ b/src/store/store.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import logging -from typing import Any, Generator, MutableMapping +from typing import Any, Generator, MutableMapping, Optional from src import shared from src.game import Game @@ -77,13 +77,15 @@ class Store: except KeyError: return default - def add_manager(self, manager: Manager, in_pipeline=True): + def add_manager(self, manager: Manager, in_pipeline: bool = True) -> None: """Add a manager to the store""" manager_type = type(manager) self.managers[manager_type] = manager self.toggle_manager_in_pipelines(manager_type, in_pipeline) - def toggle_manager_in_pipelines(self, manager_type: type[Manager], enable: bool): + def toggle_manager_in_pipelines( + self, manager_type: type[Manager], enable: bool + ) -> None: """Change if a manager should run in new pipelines""" if enable: self.pipeline_managers.add(self.managers[manager_type]) @@ -108,8 +110,8 @@ class Store: pass def add_game( - self, game: Game, additional_data: dict, run_pipeline=True - ) -> Pipeline | None: + self, game: Game, additional_data: dict, run_pipeline: bool = True + ) -> Optional[Pipeline]: """Add a game to the app""" # Ignore games from a newer spec version diff --git a/src/window.py b/src/window.py index 6b485d8..b3af2f4 100644 --- a/src/window.py +++ b/src/window.py @@ -17,7 +17,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Any +from typing import Any, Optional from gi.repository import Adw, Gio, GLib, Gtk @@ -71,7 +71,7 @@ class CartridgesWindow(Adw.ApplicationWindow): game_covers: dict = {} toasts: dict = {} active_game: Game - details_view_game_cover: GameCover | None = None + details_view_game_cover: Optional[GameCover] = None sort_state: str = "a-z" def __init__(self, **kwargs: Any) -> None: @@ -334,7 +334,7 @@ class CartridgesWindow(Adw.ApplicationWindow): index += 1 def on_undo_action( - self, _widget: Any, game: Game | None = None, undo: str | None = None + self, _widget: Any, game: Optional[Game] = None, undo: Optional[str] = None ) -> None: if not game: # If the action was activated via Ctrl + Z if shared.importer and ( From 0d32414f1e4ab1f7cc2d5edc43748f944a749766 Mon Sep 17 00:00:00 2001 From: kramo Date: Wed, 16 Aug 2023 20:16:30 +0200 Subject: [PATCH 08/80] Add type hints to utils --- src/utils/create_dialog.py | 12 ++++++-- src/utils/migrate_files_v1_to_v2.py | 4 +-- src/utils/rate_limiter.py | 48 ++++++++++++++--------------- src/utils/relative_date.py | 3 +- src/utils/save_cover.py | 9 ++++-- src/utils/steam.py | 9 ++++-- src/utils/steamgriddb.py | 10 +++--- src/utils/task.py | 29 +++++++++++------ 8 files changed, 75 insertions(+), 49 deletions(-) diff --git a/src/utils/create_dialog.py b/src/utils/create_dialog.py index c77f619..06219e2 100644 --- a/src/utils/create_dialog.py +++ b/src/utils/create_dialog.py @@ -17,10 +17,18 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from gi.repository import Adw +from typing import Optional + +from gi.repository import Adw, Gtk -def create_dialog(win, heading, body, extra_option=None, extra_label=None): +def create_dialog( + win: Gtk.Window, + heading: str, + body: str, + extra_option: Optional[str] = None, + extra_label: Optional[str] = None, +) -> Adw.MessageDialog: dialog = Adw.MessageDialog.new(win, heading, body) dialog.add_response("dismiss", _("Dismiss")) diff --git a/src/utils/migrate_files_v1_to_v2.py b/src/utils/migrate_files_v1_to_v2.py index 7c00c7e..9405a7a 100644 --- a/src/utils/migrate_files_v1_to_v2.py +++ b/src/utils/migrate_files_v1_to_v2.py @@ -30,7 +30,7 @@ old_games_dir = old_cartridges_data_dir / "games" old_covers_dir = old_cartridges_data_dir / "covers" -def migrate_game_covers(game_path: Path): +def migrate_game_covers(game_path: Path) -> None: """Migrate a game covers from a source game path to the current dir""" for suffix in (".tiff", ".gif"): cover_path = old_covers_dir / game_path.with_suffix(suffix).name @@ -41,7 +41,7 @@ def migrate_game_covers(game_path: Path): cover_path.rename(destination_cover_path) -def migrate_files_v1_to_v2(): +def migrate_files_v1_to_v2() -> None: """ Migrate user data from the v1.X locations to the latest location. diff --git a/src/utils/rate_limiter.py b/src/utils/rate_limiter.py index 09b17a9..2b3a647 100644 --- a/src/utils/rate_limiter.py +++ b/src/utils/rate_limiter.py @@ -17,11 +17,11 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Optional, Sized -from threading import Lock, Thread, BoundedSemaphore -from time import sleep, time from collections import deque from contextlib import AbstractContextManager +from threading import BoundedSemaphore, Lock, Thread +from time import sleep, time +from typing import Any, Optional, Sized class PickHistory(Sized): @@ -30,22 +30,22 @@ class PickHistory(Sized): period: int - timestamps: list[int] = None - timestamps_lock: Lock = None + timestamps: list[float] + timestamps_lock: Lock def __init__(self, period: int) -> None: self.period = period self.timestamps = [] self.timestamps_lock = Lock() - def remove_old_entries(self): + def remove_old_entries(self) -> None: """Remove history entries older than the period""" now = time() cutoff = now - self.period with self.timestamps_lock: self.timestamps = [entry for entry in self.timestamps if entry > cutoff] - def add(self, *new_timestamps: Optional[int]): + def add(self, *new_timestamps: float) -> None: """Add timestamps to the history. If none given, will add the current timestamp""" if len(new_timestamps) == 0: @@ -60,7 +60,7 @@ class PickHistory(Sized): return len(self.timestamps) @property - def start(self) -> int: + def start(self) -> float: """Get the time at which the history started""" self.remove_old_entries() with self.timestamps_lock: @@ -70,7 +70,7 @@ class PickHistory(Sized): entry = time() return entry - def copy_timestamps(self) -> str: + def copy_timestamps(self) -> list[float]: """Get a copy of the timestamps history""" self.remove_old_entries() with self.timestamps_lock: @@ -88,22 +88,22 @@ class RateLimiter(AbstractContextManager): # Max number of tokens that can be consumed instantly burst_tokens: int - pick_history: PickHistory = None - bucket: BoundedSemaphore = None - queue: deque[Lock] = None - queue_lock: Lock = None + pick_history: Optional[PickHistory] = None # TODO: Geoff: make this required + bucket: BoundedSemaphore + queue: deque[Lock] + queue_lock: Lock # Protect the number of tokens behind a lock - __n_tokens_lock: Lock = None + __n_tokens_lock: Lock __n_tokens = 0 @property - def n_tokens(self): + def n_tokens(self) -> int: with self.__n_tokens_lock: return self.__n_tokens @n_tokens.setter - def n_tokens(self, value: int): + def n_tokens(self, value: int) -> None: with self.__n_tokens_lock: self.__n_tokens = value @@ -147,8 +147,8 @@ class RateLimiter(AbstractContextManager): """ # Compute ideal spacing - tokens_left = self.refill_period_tokens - len(self.pick_history) - seconds_left = self.pick_history.start + self.refill_period_seconds - time() + tokens_left = self.refill_period_tokens - len(self.pick_history) # type: ignore + seconds_left = self.pick_history.start + self.refill_period_seconds - time() # type: ignore try: spacing_seconds = seconds_left / tokens_left except ZeroDivisionError: @@ -159,7 +159,7 @@ class RateLimiter(AbstractContextManager): natural_spacing = self.refill_period_seconds / self.refill_period_tokens return max(natural_spacing, spacing_seconds) - def refill(self): + def refill(self) -> None: """Add a token back in the bucket""" sleep(self.refill_spacing) try: @@ -170,7 +170,7 @@ class RateLimiter(AbstractContextManager): else: self.n_tokens += 1 - def refill_thread_func(self): + def refill_thread_func(self) -> None: """Entry point for the daemon thread that is refilling the bucket""" while True: self.refill() @@ -200,18 +200,18 @@ class RateLimiter(AbstractContextManager): self.queue.appendleft(lock) return lock - def acquire(self): + def acquire(self) -> None: """Acquires a token from the bucket when it's your turn in queue""" lock = self.add_to_queue() self.update_queue() # Wait until our turn in queue lock.acquire() # pylint: disable=consider-using-with - self.pick_history.add() + self.pick_history.add() # type: ignore # --- Support for use in with statements - def __enter__(self): + def __enter__(self) -> None: self.acquire() - def __exit__(self, *_args): + def __exit__(self, *_args: Any) -> None: pass diff --git a/src/utils/relative_date.py b/src/utils/relative_date.py index 6ecde67..fbded0d 100644 --- a/src/utils/relative_date.py +++ b/src/utils/relative_date.py @@ -18,11 +18,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later from datetime import datetime +from typing import Any from gi.repository import GLib -def relative_date(timestamp): # pylint: disable=too-many-return-statements +def relative_date(timestamp: int) -> Any: # pylint: disable=too-many-return-statements days_no = ((today := datetime.today()) - datetime.fromtimestamp(timestamp)).days if days_no == 0: diff --git a/src/utils/save_cover.py b/src/utils/save_cover.py index 0bbcd4b..0bb2381 100644 --- a/src/utils/save_cover.py +++ b/src/utils/save_cover.py @@ -20,14 +20,17 @@ from pathlib import Path from shutil import copyfile +from typing import Optional -from gi.repository import Gdk, Gio, GLib +from gi.repository import Gdk, GdkPixbuf, Gio, GLib from PIL import Image, ImageSequence, UnidentifiedImageError from src import shared -def resize_cover(cover_path=None, pixbuf=None): +def resize_cover( + cover_path: Optional[Path] = None, pixbuf: Optional[GdkPixbuf.Pixbuf] = None +) -> Optional[Path]: if not cover_path and not pixbuf: return None @@ -74,7 +77,7 @@ def resize_cover(cover_path=None, pixbuf=None): return tmp_path -def save_cover(game_id, cover_path): +def save_cover(game_id: str, cover_path: Path) -> None: shared.covers_dir.mkdir(parents=True, exist_ok=True) animated_path = shared.covers_dir / f"{game_id}.gif" diff --git a/src/utils/steam.py b/src/utils/steam.py index e0cb0f2..456869c 100644 --- a/src/utils/steam.py +++ b/src/utils/steam.py @@ -21,6 +21,7 @@ import json import logging import re +from pathlib import Path from typing import TypedDict import requests @@ -80,7 +81,7 @@ class SteamRateLimiter(RateLimiter): self.pick_history.remove_old_entries() super().__init__() - def acquire(self): + def acquire(self) -> None: """Get a token from the bucket and store the pick history in the schema""" super().acquire() timestamps_str = json.dumps(self.pick_history.copy_timestamps()) @@ -90,7 +91,9 @@ class SteamRateLimiter(RateLimiter): class SteamFileHelper: """Helper for steam file formats""" - def get_manifest_data(self, manifest_path) -> SteamManifestData: + def get_manifest_data( + self, manifest_path: Path + ) -> SteamManifestData: # TODO: Geoff: fix typing issue """Get local data for a game from its manifest""" with open(manifest_path, "r", encoding="utf-8") as file: @@ -116,7 +119,7 @@ class SteamAPIHelper: def __init__(self, rate_limiter: RateLimiter) -> None: self.rate_limiter = rate_limiter - def get_api_data(self, appid) -> SteamAPIData: + def get_api_data(self, appid: str) -> SteamAPIData: """ Get online data for a game from its appid. May block to satisfy the Steam web API limitations. diff --git a/src/utils/steamgriddb.py b/src/utils/steamgriddb.py index 57dda3e..c87da1c 100644 --- a/src/utils/steamgriddb.py +++ b/src/utils/steamgriddb.py @@ -20,12 +20,14 @@ import logging from pathlib import Path +from typing import Any 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 resize_cover, save_cover @@ -55,12 +57,12 @@ class SGDBHelper: base_url = "https://www.steamgriddb.com/api/v2/" @property - def auth_headers(self): + def auth_headers(self) -> dict[str, str]: key = shared.schema.get_string("sgdb-key") headers = {"Authorization": f"Bearer {key}"} return headers - def get_game_id(self, game): + def get_game_id(self, game: Game) -> Any: """Get grid results for a game. Can raise an exception.""" uri = f"{self.base_url}search/autocomplete/{game.name}" res = requests.get(uri, headers=self.auth_headers, timeout=5) @@ -74,7 +76,7 @@ class SGDBHelper: case _: res.raise_for_status() - def get_image_uri(self, game_id, animated=False): + def get_image_uri(self, game_id: str, animated: bool = False) -> Any: """Get the image for a SGDB game id""" uri = f"{self.base_url}grids/game/{game_id}?dimensions=600x900" if animated: @@ -93,7 +95,7 @@ class SGDBHelper: case _: res.raise_for_status() - def conditionaly_update_cover(self, game): + def conditionaly_update_cover(self, game: Game) -> None: """Update the game's cover if appropriate""" # Obvious skips diff --git a/src/utils/task.py b/src/utils/task.py index 190d7c4..30ef68f 100644 --- a/src/utils/task.py +++ b/src/utils/task.py @@ -18,25 +18,28 @@ # SPDX-License-Identifier: GPL-3.0-or-later from functools import wraps +from typing import Any, Callable from gi.repository import Gio -def create_task_thread_func_closure(func, data): +def create_task_thread_func_closure(func: Callable, data: Any) -> Callable: """Wrap a Gio.TaskThreadFunc with the given data in a closure""" - def closure(task, source_object, _data, cancellable): + def closure( + task: Gio.Task, source_object: object, _data: Any, cancellable: Gio.Cancellable + ) -> Any: func(task, source_object, data, cancellable) return closure -def decorate_set_task_data(task): +def decorate_set_task_data(task: Gio.Task) -> Callable: """Decorate Gio.Task.set_task_data to replace it""" - def decorator(original_method): + def decorator(original_method: Callable) -> Callable: @wraps(original_method) - def new_method(task_data): + def new_method(task_data: Any) -> None: task.task_data = task_data return new_method @@ -44,13 +47,13 @@ def decorate_set_task_data(task): return decorator -def decorate_run_in_thread(task): +def decorate_run_in_thread(task: Gio.Task) -> Callable: """Decorate Gio.Task.run_in_thread to pass the task data correctly Creates a closure around task_thread_func with the task data available.""" - def decorator(original_method): + def decorator(original_method: Callable) -> Callable: @wraps(original_method) - def new_method(task_thread_func): + def new_method(task_thread_func: Callable) -> None: closure = create_task_thread_func_closure(task_thread_func, task.task_data) original_method(closure) @@ -64,11 +67,17 @@ class Task: """Wrapper around Gio.Task to patch task data not being passed""" @classmethod - def new(cls, source_object, cancellable, callback, callback_data): + def new( + cls, + source_object: object, + cancellable: Gio.Cancellable, + callback: Callable, + callback_data: Any, + ) -> Gio.Task: """Create a new, monkey-patched Gio.Task. The `set_task_data` and `run_in_thread` methods are decorated. - As of 2023-05-19, pygobject does not work well with Gio.Task, so to pass data + As of 2023-05-19, PyGObject does not work well with Gio.Task, so to pass data the only viable way it to create a closure with the thread function and its data. This class is supposed to make Gio.Task comply with its expected behaviour per the docs: From a0c736b7cf314d07c44e56db10dd913f5fba2890 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 17 Aug 2023 01:09:41 +0200 Subject: [PATCH 09/80] Improved rate limiter code --- src/utils/rate_limiter.py | 46 +++++++++++++++++++++------------------ src/utils/steam.py | 18 +++++++-------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/utils/rate_limiter.py b/src/utils/rate_limiter.py index 2b3a647..431af60 100644 --- a/src/utils/rate_limiter.py +++ b/src/utils/rate_limiter.py @@ -21,7 +21,7 @@ from collections import deque from contextlib import AbstractContextManager from threading import BoundedSemaphore, Lock, Thread from time import sleep, time -from typing import Any, Optional, Sized +from typing import Any, Sized class PickHistory(Sized): @@ -79,16 +79,23 @@ class PickHistory(Sized): # pylint: disable=too-many-instance-attributes class RateLimiter(AbstractContextManager): - """Rate limiter implementing the token bucket algorithm""" + """ + Base rate limiter implementing the token bucket algorithm. + + Do not use directly, create a child class to tailor the rate limiting to the + underlying service's limits. + + Subclasses must provide values to the following attributes: + * refill_period_seconds - Period in which we have a max amount of tokens + * refill_period_tokens - Number of tokens allowed in this period + * burst_tokens - Max number of tokens that can be consumed instantly + """ - # Period in which we have a max amount of tokens refill_period_seconds: int - # Number of tokens allowed in this period refill_period_tokens: int - # Max number of tokens that can be consumed instantly burst_tokens: int - pick_history: Optional[PickHistory] = None # TODO: Geoff: make this required + pick_history: PickHistory bucket: BoundedSemaphore queue: deque[Lock] queue_lock: Lock @@ -107,23 +114,20 @@ class RateLimiter(AbstractContextManager): with self.__n_tokens_lock: self.__n_tokens = value - def __init__( - self, - refill_period_seconds: Optional[int] = None, - refill_period_tokens: Optional[int] = None, - burst_tokens: Optional[int] = None, - ) -> None: + def _init_pick_history(self) -> None: + """ + Initialize the tocken pick history + (only for use in this class and its children) + + By default, creates an empty pick history. + Should be overriden or extended by subclasses. + """ + self.pick_history = PickHistory(self.refill_period_seconds) + + def __init__(self) -> None: """Initialize the limiter""" - # Initialize default values - if refill_period_seconds is not None: - self.refill_period_seconds = refill_period_seconds - if refill_period_tokens is not None: - self.refill_period_tokens = refill_period_tokens - if burst_tokens is not None: - self.burst_tokens = burst_tokens - if self.pick_history is None: - self.pick_history = PickHistory(self.refill_period_seconds) + self._init_pick_history() # Create synchronization data self.__n_tokens_lock = Lock() diff --git a/src/utils/steam.py b/src/utils/steam.py index 456869c..a38d731 100644 --- a/src/utils/steam.py +++ b/src/utils/steam.py @@ -28,7 +28,7 @@ import requests from requests.exceptions import HTTPError from src import shared -from src.utils.rate_limiter import PickHistory, RateLimiter +from src.utils.rate_limiter import RateLimiter class SteamError(Exception): @@ -72,14 +72,16 @@ class SteamRateLimiter(RateLimiter): refill_period_tokens = 200 burst_tokens = 100 - def __init__(self) -> None: - # Load pick history from schema - # (Remember API limits through restarts of Cartridges) + def _init_pick_history(self) -> None: + """ + Load the pick history from schema. + + Allows remembering API limits through restarts of Cartridges. + """ + super()._init_pick_history() timestamps_str = shared.state_schema.get_string("steam-limiter-tokens-history") - self.pick_history = PickHistory(self.refill_period_seconds) self.pick_history.add(*json.loads(timestamps_str)) self.pick_history.remove_old_entries() - super().__init__() def acquire(self) -> None: """Get a token from the bucket and store the pick history in the schema""" @@ -91,9 +93,7 @@ class SteamRateLimiter(RateLimiter): class SteamFileHelper: """Helper for steam file formats""" - def get_manifest_data( - self, manifest_path: Path - ) -> SteamManifestData: # TODO: Geoff: fix typing issue + def get_manifest_data(self, manifest_path: Path) -> SteamManifestData: """Get local data for a game from its manifest""" with open(manifest_path, "r", encoding="utf-8") as file: From f3c590b033ce75d3459ebc6c7e89d509c40716a9 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 17 Aug 2023 01:25:39 +0200 Subject: [PATCH 10/80] Explicit SteamManifestData building --- src/utils/steam.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/steam.py b/src/utils/steam.py index a38d731..bdc8d84 100644 --- a/src/utils/steam.py +++ b/src/utils/steam.py @@ -107,7 +107,11 @@ class SteamFileHelper: raise SteamInvalidManifestError() data[key] = match.group(1) - return SteamManifestData(**data) + return SteamManifestData( + name=data["name"], + appid=data["appid"], + stateflags=data["stateflags"], + ) class SteamAPIHelper: From 191beed12e732fea678737ae0a9618c49b891bdf Mon Sep 17 00:00:00 2001 From: kramo Date: Thu, 17 Aug 2023 13:52:56 +0200 Subject: [PATCH 11/80] Use shared.home consistently --- src/game.py | 2 +- src/importer/sources/heroic_source.py | 2 +- src/utils/migrate_files_v1_to_v2.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game.py b/src/game.py index e144460..8dc981e 100644 --- a/src/game.py +++ b/src/game.py @@ -128,7 +128,7 @@ class Game(Gtk.Box): # pylint: disable=consider-using-with subprocess.Popen( args, - cwd=Path.home(), + 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/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 42dc446..f31bbe7 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -241,7 +241,7 @@ class LegendaryIterable(StoreSubSourceIterable): else: # Heroic native logging.debug("Using Heroic native <= 2.8 legendary file") - path = Path.home() / ".config" + path = shared.home / ".config" path = path / "legendary" / "installed.json" logging.debug("Using Heroic %s installed.json path %s", self.name, path) diff --git a/src/utils/migrate_files_v1_to_v2.py b/src/utils/migrate_files_v1_to_v2.py index 9405a7a..41c166b 100644 --- a/src/utils/migrate_files_v1_to_v2.py +++ b/src/utils/migrate_files_v1_to_v2.py @@ -23,7 +23,7 @@ from pathlib import Path from src import shared -old_data_dir = Path.home() / ".local" / "share" +old_data_dir = shared.home / ".local" / "share" old_cartridges_data_dir = old_data_dir / "cartridges" migrated_file_path = old_cartridges_data_dir / ".migrated" old_games_dir = old_cartridges_data_dir / "games" From 195f7dbb7eb4ad2a9c8b685e59aa9d8693890a96 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 17 Aug 2023 14:07:04 +0200 Subject: [PATCH 12/80] Disabled Steam RetroArch to merge improvements to main --- src/importer/sources/retroarch_source.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 426b4ee..9eb3ee6 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -172,7 +172,8 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() - self.add_steam_location_candidate() + # TODO enable when we get the Steam RetroArch games work + # self.add_steam_location_candidate() def add_steam_location_candidate(self) -> None: """Add the Steam RetroAcrh location to the config candidates""" @@ -231,12 +232,13 @@ class RetroarchSource(Source): # Steam RetroArch # (Must check before Flatpak, because Steam itself can be installed as one) - if self.locations.config.root.parent.parent.name == "steamapps": - # steam://run exepects args to be url-encoded and separated by spaces. - args = map(lambda s: url_quote(str(s), safe=""), args) - args_str = " ".join(args) - uri = f"steam://run/1118310//{args_str}/" - return f"xdg-open {shell_quote(uri)}" + # TODO enable when we get Steam RetroArch executable to work + # if self.locations.config.root.parent.parent.name == "steamapps": + # # steam://run exepects args to be url-encoded and separated by spaces. + # args = map(lambda s: url_quote(str(s), safe=""), args) + # args_str = " ".join(args) + # uri = f"steam://run/1118310//{args_str}/" + # return f"xdg-open {shell_quote(uri)}" # Flatpak RetroArch args = map(lambda s: shell_quote(str(s)), args) From 37f838e4a2005a48f0887c74da3d2306fd81188c Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 17 Aug 2023 14:13:55 +0200 Subject: [PATCH 13/80] Added back locations --- src/importer/sources/retroarch_source.py | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 796c7ee..7e6beea 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -151,6 +151,31 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() + self.locations = RetroarchLocations( + Location( + schema_key="retroarch-location", + candidates=[ + shared.flatpak_dir + / "org.libretro.RetroArch" + / "config" + / "retroarch", + shared.config_dir / "retroarch", + shared.home / ".config" / "retroarch", + # TODO: Windows support, waiting for executable path setting improvement + # Path("C:\\RetroArch-Win64"), + # Path("C:\\RetroArch-Win32"), + # TODO: UWP support (URL handler - https://github.com/libretro/RetroArch/pull/13563) + # shared.local_appdata_dir + # / "Packages" + # / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" + # / "LocalState", + ], + paths={ + "retroarch.cfg": LocationSubPath("retroarch.cfg"), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) + ) # TODO enable when we get the Steam RetroArch games work # self.add_steam_location_candidate() From 91f4e05abe2a12586ccab41dbba3bac2386b6e23 Mon Sep 17 00:00:00 2001 From: kramo Date: Thu, 17 Aug 2023 14:29:04 +0200 Subject: [PATCH 14/80] Cleanups --- README.md | 1 + src/importer/sources/heroic_source.py | 15 ++++++--------- src/importer/sources/retroarch_source.py | 6 +++--- src/importer/sources/steam_source.py | 2 +- src/store/managers/sgdb_manager.py | 4 ++-- src/store/managers/steam_api_manager.py | 4 ++-- src/utils/steam.py | 2 +- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 1455504..a22ba2a 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Cartridges is a simple game launcher written in Python using GTK4 and Libadwaita - Bottles - itch - Legendary + - RetroArch - Flatpak - Hiding games - Searching and sorting by title, date added and last played diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index f31bbe7..6b8002b 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -115,8 +115,8 @@ class SubSourceIterable(Iterable): } game = Game(values) - # Get the image path from the heroic cache - # Filenames are derived from the URL that heroic used to get the file + # Get the image path from the Heroic cache + # Filenames are derived from the URL that Heroic used to get the file uri: str = entry["art_square"] + self.image_uri_params digest = sha256(uri.encode()).hexdigest() image_path = self.source.locations.config.root / "images-cache" / digest @@ -221,13 +221,10 @@ class LegendaryIterable(StoreSubSourceIterable): @cached_property def installed_path(self) -> Path: - """ - Get the right path depending on the Heroic version - - TODO after heroic 2.9 has been out for a while - We should use the commented out relative_installed_path - and remove this property override. - """ + """Get the right path depending on the Heroic version""" + # TODO after Heroic 2.9 has been out for a while + # We should use the commented out relative_installed_path + # and remove this property override. heroic_config_path = self.source.locations.config.root # Heroic >= 2.9 diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 7e6beea..93b6065 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -176,7 +176,7 @@ class RetroarchSource(Source): invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, ) ) - # TODO enable when we get the Steam RetroArch games work + # TODO enable when we get the Steam RetroArch games working # self.add_steam_location_candidate() def add_steam_location_candidate(self) -> None: @@ -192,13 +192,13 @@ class RetroarchSource(Source): """ Get the RetroArch installed via Steam location - :raise UnresolvableLocationError: if steam isn't installed + :raise UnresolvableLocationError: if Steam isn't installed :raise KeyError: if there is no libraryfolders.vdf subpath :raise OSError: if libraryfolders.vdf can't be opened :raise ValueError: if RetroArch isn't installed through Steam """ - # Find steam location + # Find Steam location libraryfolders = SteamSource().locations.data["libraryfolders.vdf"] parse_apps = False with open(libraryfolders, "r", encoding="utf-8") as open_file: diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 90460bd..75b8f69 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -35,7 +35,7 @@ class SteamSourceIterable(SourceIterable): source: "SteamSource" def get_manifest_dirs(self) -> Iterable[Path]: - """Get dirs that contain steam app manifests""" + """Get dirs that contain Steam app manifests""" libraryfolders_path = self.source.locations.data["libraryfolders.vdf"] with open(libraryfolders_path, "r", encoding="utf-8") as file: contents = file.read() diff --git a/src/store/managers/sgdb_manager.py b/src/store/managers/sgdb_manager.py index 5c002cb..4cba471 100644 --- a/src/store/managers/sgdb_manager.py +++ b/src/store/managers/sgdb_manager.py @@ -24,13 +24,13 @@ 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.steam_api_manager import SteamAPIManager from src.store.managers.cover_manager import CoverManager +from src.store.managers.steam_api_manager import SteamAPIManager from src.utils.steamgriddb import SGDBAuthError, SGDBHelper class SGDBManager(AsyncManager): - """Manager in charge of downloading a game's cover from steamgriddb""" + """Manager in charge of downloading a game's cover from SteamGridDB""" run_after = (SteamAPIManager, CoverManager) retryable_on = (HTTPError, SSLError, ConnectionError, JSONDecodeError) diff --git a/src/store/managers/steam_api_manager.py b/src/store/managers/steam_api_manager.py index 1a62ddd..2dc88d5 100644 --- a/src/store/managers/steam_api_manager.py +++ b/src/store/managers/steam_api_manager.py @@ -23,8 +23,8 @@ 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 ( - SteamGameNotFoundError, SteamAPIHelper, + SteamGameNotFoundError, SteamNotAGameError, SteamRateLimiter, ) @@ -44,7 +44,7 @@ class SteamAPIManager(AsyncManager): self.steam_api_helper = SteamAPIHelper(self.steam_rate_limiter) def main(self, game: Game, additional_data: dict) -> None: - # Skip non-steam games + # Skip non-Steam games appid = additional_data.get("steam_appid", None) if appid is None: return diff --git a/src/utils/steam.py b/src/utils/steam.py index bdc8d84..6e53f27 100644 --- a/src/utils/steam.py +++ b/src/utils/steam.py @@ -91,7 +91,7 @@ class SteamRateLimiter(RateLimiter): class SteamFileHelper: - """Helper for steam file formats""" + """Helper for Steam file formats""" def get_manifest_data(self, manifest_path: Path) -> SteamManifestData: """Get local data for a game from its manifest""" From 420ca939ef16fba961ee4c441f442c140f5b855a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 17 Aug 2023 14:30:30 +0200 Subject: [PATCH 15/80] Translations update from Hosted Weblate (#161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Arabic) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Arabic) Currently translated at 100.0% (122 of 122 strings) Co-authored-by: Ali Aljishi Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ar/ Translation: Cartridges/Cartridges * Translated using Weblate (Spanish) Currently translated at 100.0% (130 of 130 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Spanish) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Spanish) Currently translated at 100.0% (122 of 122 strings) Translated using Weblate (Spanish) Currently translated at 99.1% (121 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/es/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Russian) Currently translated at 100.0% (122 of 122 strings) Co-authored-by: Hosted Weblate Co-authored-by: Сергей Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ru/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (German) Currently translated at 96.7% (118 of 122 strings) Translated using Weblate (German) Currently translated at 93.4% (114 of 122 strings) Translated using Weblate (German) Currently translated at 90.9% (111 of 122 strings) Co-authored-by: Hosted Weblate Co-authored-by: Simon Hahne Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/de/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Dutch) Currently translated at 100.0% (122 of 122 strings) Co-authored-by: Hosted Weblate Co-authored-by: Philip Goto Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/nl/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Ukrainian) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Ukrainian) Currently translated at 100.0% (122 of 122 strings) Co-authored-by: Dan Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/uk/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Italian) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Co-authored-by: albanobattistella Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/it/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (130 of 130 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (120 of 120 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Portuguese (Brazil)) Currently translated at 96.7% (118 of 122 strings) Co-authored-by: Hosted Weblate Co-authored-by: Rafael Fontenelle Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/pt_BR/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Czech) Currently translated at 100.0% (122 of 122 strings) Co-authored-by: Hosted Weblate Co-authored-by: foo expert Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/cs/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Norwegian Bokmål) Currently translated at 76.2% (93 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Allan Nordhøy Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/nb_NO/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Tamil) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Tamil) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Co-authored-by: K.B.Dharun Krishna Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ta/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Swedish) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Co-authored-by: Luna Jernberg Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/sv/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (French) Currently translated at 100.0% (122 of 122 strings) Co-authored-by: Hosted Weblate Co-authored-by: John Donne Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/fr/ Translation: Cartridges/Cartridges * Translated using Weblate (Hungarian) Currently translated at 100.0% (130 of 130 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Hungarian) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Hungarian) Currently translated at 100.0% (120 of 120 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Hungarian) Currently translated at 100.0% (120 of 120 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translated using Weblate (Hungarian) Currently translated at 100.0% (122 of 122 strings) Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Co-authored-by: kramo Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/hu/ Translation: Cartridges/Cartridges --------- Co-authored-by: Ali Aljishi Co-authored-by: gallegonovato Co-authored-by: Сергей Co-authored-by: Simon Hahne Co-authored-by: Philip Goto Co-authored-by: Dan Co-authored-by: albanobattistella Co-authored-by: Rafael Fontenelle Co-authored-by: foo expert Co-authored-by: Allan Nordhøy Co-authored-by: K.B.Dharun Krishna Co-authored-by: Luna Jernberg Co-authored-by: John Donne Co-authored-by: kramo --- po/ar.po | 265 ++++++++++++++++++++++++------------------ po/cs.po | 264 ++++++++++++++++++++++++++---------------- po/de.po | 325 +++++++++++++++++++++++++++++----------------------- po/el.po | 260 ++++++++++++++++++++++++----------------- po/es.po | 273 ++++++++++++++++++++++++------------------- po/fa.po | 239 ++++++++++++++++++++++---------------- po/fi.po | 274 +++++++++++++++++++++++++------------------ po/fr.po | 277 +++++++++++++++++++++++++------------------- po/hu.po | 265 ++++++++++++++++++++++++------------------ po/it.po | 271 +++++++++++++++++++++++++------------------ po/ko.po | 229 +++++++++++++++++++++--------------- po/nb_NO.po | 294 +++++++++++++++++++++++++---------------------- po/nl.po | 272 +++++++++++++++++++++++++------------------ po/pl.po | 272 +++++++++++++++++++++++++------------------ po/pt.po | 258 ++++++++++++++++++++++++----------------- po/pt_BR.po | 303 +++++++++++++++++++++++++----------------------- po/ro.po | 235 +++++++++++++++++++++---------------- po/ru.po | 269 +++++++++++++++++++++++++------------------ po/sv.po | 272 +++++++++++++++++++++++++------------------ po/ta.po | 274 ++++++++++++++++++++++++------------------- po/tr.po | 258 ++++++++++++++++++++++++----------------- po/uk.po | 265 ++++++++++++++++++++++++------------------ 22 files changed, 3442 insertions(+), 2472 deletions(-) diff --git a/po/ar.po b/po/ar.po index 506fe72..b84c890 100644 --- a/po/ar.po +++ b/po/ar.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-25 20:33+0200\n" -"PO-Revision-Date: 2023-07-09 07:59+0000\n" +"POT-Creation-Date: 2023-08-16 11:06+0200\n" +"PO-Revision-Date: 2023-08-15 17:07+0000\n" "Last-Translator: Ali Aljishi \n" "Language-Team: Arabic \n" @@ -22,7 +22,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:170 +#: src/main.py:169 msgid "Cartridges" msgstr "خراطيش" @@ -36,10 +36,10 @@ msgid "Launch all your games" msgstr "شغِّل كلَّ ألعابك" #: data/hu.kramo.Cartridges.desktop.in:11 -#, fuzzy -#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" -msgstr "لعب;مشغل;ستيم;لوترس;هروك;قوارير;إتش;هيرويك;بوتلز;" +msgid "" +"gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;retroarch;" +msgstr "" +"لعب;مشغل;ستيم;لوترس;هروك;قوارير;إتش;هيرويك;بوتلز;لجندري;فلاتباك;رتروآرتش;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 msgid "" @@ -56,16 +56,18 @@ msgstr "" msgid "Library" msgstr "المكتبة" -#: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67 +#: data/hu.kramo.Cartridges.metainfo.xml.in:34 msgid "Edit Game Details" msgstr "حرِّر تفاصيل اللعبة" #: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71 +#: src/details_window.py:67 msgid "Game Details" msgstr "تفاصيل اللعبة" -#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:241 +#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:418 +#: src/details_window.py:241 src/importer/importer.py:292 +#: src/importer/importer.py:342 msgid "Preferences" msgstr "التفضيلات" @@ -81,32 +83,19 @@ msgstr "غلاف جديد" msgid "Delete Cover" msgstr "احذف الغلاف" -#: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106 -#: data/gtk/game.blp:80 +#: data/gtk/details-window.blp:102 data/gtk/game.blp:80 msgid "Title" msgstr "العنوان" -#: data/gtk/details-window.blp:102 -msgid "The title of the game" -msgstr "عنوان اللعبة" +#: data/gtk/details-window.blp:105 +msgid "Developer (optional)" +msgstr "المطوِّر (اختياري)" -#: data/gtk/details-window.blp:112 data/gtk/details-window.blp:117 -msgid "Developer" -msgstr "المطوِّر" - -#: data/gtk/details-window.blp:113 -msgid "The developer or publisher (optional)" -msgstr "المطوِّر أو الناشر (اختياري)" - -#: data/gtk/details-window.blp:123 data/gtk/details-window.blp:155 +#: data/gtk/details-window.blp:110 msgid "Executable" msgstr "ملفُّ التنفيذ" -#: data/gtk/details-window.blp:124 -msgid "File to open or command to run when launching the game" -msgstr "الملفُّ المفتوح أو الأمر المشغَّل عند بدء اللعبة" - -#: data/gtk/details-window.blp:130 +#: data/gtk/details-window.blp:116 msgid "More Info" msgstr "معلومات أكثر" @@ -136,7 +125,7 @@ msgid "Quit" msgstr "أنهِ" #: data/gtk/help-overlay.blp:19 data/gtk/window.blp:217 data/gtk/window.blp:257 -#: data/gtk/window.blp:323 +#: data/gtk/window.blp:324 msgid "Search" msgstr "ابحث" @@ -148,7 +137,8 @@ msgstr "أظهر التفضيلات" msgid "Shortcuts" msgstr "الاختصارات" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 +#: data/gtk/help-overlay.blp:34 src/game.py:103 src/preferences.py:120 +#: src/importer/importer.py:366 msgid "Undo" msgstr "تراجع" @@ -176,7 +166,8 @@ msgstr "أظهر الألعاب المخفية" msgid "Remove game" msgstr "أزل اللعبة" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:89 +#: data/gtk/preferences.blp:304 msgid "Behavior" msgstr "السلوك" @@ -212,108 +203,116 @@ msgstr "منطقة خطر" msgid "Remove All Games" msgstr "أزل كلَّ الألعاب" -#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:442 +#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:444 msgid "Import" msgstr "استورد" -#: data/gtk/preferences.blp:89 +#: data/gtk/preferences.blp:92 +#, fuzzy +#| msgid "Remove All Games" +msgid "Remove Uninstalled Games" +msgstr "أزل كلَّ الألعاب" + +#: data/gtk/preferences.blp:102 msgid "Sources" msgstr "المصادر" -#: data/gtk/preferences.blp:92 +#: data/gtk/preferences.blp:105 msgid "Steam" msgstr "ستيم" -#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 -#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:109 data/gtk/preferences.blp:123 +#: data/gtk/preferences.blp:164 data/gtk/preferences.blp:214 +#: data/gtk/preferences.blp:228 data/gtk/preferences.blp:242 +#: data/gtk/preferences.blp:256 data/gtk/preferences.blp:270 msgid "Install Location" msgstr "موضع التثبيت" -#: data/gtk/preferences.blp:106 +#: data/gtk/preferences.blp:119 msgid "Lutris" msgstr "لوترس" -#: data/gtk/preferences.blp:119 +#: data/gtk/preferences.blp:132 msgid "Cache Location" msgstr "موضع الذاكرة المؤقتة" -#: data/gtk/preferences.blp:128 +#: data/gtk/preferences.blp:141 msgid "Import Steam Games" msgstr "استورد ألعابًا من ستيم" -#: data/gtk/preferences.blp:137 +#: data/gtk/preferences.blp:150 msgid "Import Flatpak Games" msgstr "استورد ألعاب فلاتباك" -#: data/gtk/preferences.blp:147 +#: data/gtk/preferences.blp:160 msgid "Heroic" msgstr "هِرُوِك" -#: data/gtk/preferences.blp:160 +#: data/gtk/preferences.blp:173 msgid "Import Epic Games" msgstr "استورد ألعاب أَبِك" -#: data/gtk/preferences.blp:169 +#: data/gtk/preferences.blp:182 msgid "Import GOG Games" msgstr "استورد ألعاب جي‌أو‌جي" -#: data/gtk/preferences.blp:178 -#, fuzzy -#| msgid "Import Steam Games" +#: data/gtk/preferences.blp:191 msgid "Import Amazon Games" -msgstr "استورد ألعابًا من ستيم" +msgstr "استورد ألعابًا من أمازون" -#: data/gtk/preferences.blp:187 +#: data/gtk/preferences.blp:200 msgid "Import Sideloaded Games" msgstr "استورد ألعابًا مثبَّتةً بغير متجر" -#: data/gtk/preferences.blp:197 +#: data/gtk/preferences.blp:210 msgid "Bottles" msgstr "قوارير" -#: data/gtk/preferences.blp:211 +#: data/gtk/preferences.blp:224 msgid "itch" msgstr "إتش" -#: data/gtk/preferences.blp:225 +#: data/gtk/preferences.blp:238 msgid "Legendary" msgstr "لجندري" -#: data/gtk/preferences.blp:239 +#: data/gtk/preferences.blp:252 +msgid "RetroArch" +msgstr "رتروآرتش" + +#: data/gtk/preferences.blp:266 msgid "Flatpak" msgstr "فلاتباك" -#: data/gtk/preferences.blp:252 +#: data/gtk/preferences.blp:279 msgid "Import Game Launchers" msgstr "استورد مشغِّلات ألعاب" -#: data/gtk/preferences.blp:265 +#: data/gtk/preferences.blp:292 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:269 +#: data/gtk/preferences.blp:296 msgid "Authentication" msgstr "الاستيثاق" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:299 msgid "API Key" msgstr "مفتاح واجهة البرمجة" -#: data/gtk/preferences.blp:280 +#: data/gtk/preferences.blp:307 msgid "Use SteamGridDB" msgstr "استخدم SteamGridDB" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:308 msgid "Download images when adding or importing games" msgstr "نزِّل الصور حينما تنزِّل أو تستورد الألعاب" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:317 msgid "Prefer Over Official Images" msgstr "فضِّلها على الصور الرسمية" -#: data/gtk/preferences.blp:299 +#: data/gtk/preferences.blp:326 msgid "Prefer Animated Images" msgstr "فضِّل الصور المتحرِّكة" @@ -341,7 +340,7 @@ msgstr "لا توجد ألعاب مخفية" msgid "Games you hide will appear here." msgstr "هنا يظهر ما أخفيت من ألعاب." -#: data/gtk/window.blp:64 data/gtk/window.blp:304 +#: data/gtk/window.blp:64 data/gtk/window.blp:305 msgid "Back" msgstr "عد" @@ -353,51 +352,59 @@ msgstr "عنوان اللعبة" msgid "Play" msgstr "العب" -#: data/gtk/window.blp:243 data/gtk/window.blp:435 +#: data/gtk/window.blp:243 data/gtk/window.blp:437 msgid "Add Game" msgstr "أضف لعبةً" -#: data/gtk/window.blp:250 data/gtk/window.blp:316 +#: data/gtk/window.blp:250 data/gtk/window.blp:317 msgid "Main Menu" msgstr "القائمة الرئيسة" -#: data/gtk/window.blp:311 +#: data/gtk/window.blp:272 +msgid "Search games" +msgstr "ابحث عن ألعاب" + +#: data/gtk/window.blp:312 msgid "Hidden Games" msgstr "الألعاب المخفية" -#: data/gtk/window.blp:374 +#: data/gtk/window.blp:339 +msgid "Search hidden games" +msgstr "ابحث في الألعاب المخفية" + +#: data/gtk/window.blp:376 msgid "Sort" msgstr "رتِّب" -#: data/gtk/window.blp:377 +#: data/gtk/window.blp:379 msgid "A-Z" msgstr "أ-ي" -#: data/gtk/window.blp:383 +#: data/gtk/window.blp:385 msgid "Z-A" msgstr "ي-أ" -#: data/gtk/window.blp:389 +#: data/gtk/window.blp:391 msgid "Newest" msgstr "الأجدد" -#: data/gtk/window.blp:395 +#: data/gtk/window.blp:397 msgid "Oldest" msgstr "الأقدم" -#: data/gtk/window.blp:401 +#: data/gtk/window.blp:403 msgid "Last Played" msgstr "لُعبت آخر مرَّة" -#: data/gtk/window.blp:408 +#: data/gtk/window.blp:410 msgid "Show Hidden" msgstr "أظهر ما أخفي" -#: data/gtk/window.blp:421 +#: data/gtk/window.blp:423 msgid "Keyboard Shortcuts" msgstr "اختصارات لوحة المفاتيح" -#: data/gtk/window.blp:426 +#: data/gtk/window.blp:428 msgid "About Cartridges" msgstr "عن «خراطيش»" @@ -429,8 +436,8 @@ msgid "Add New Game" msgstr "أضف لعبةً جديدةً" #: src/details_window.py:79 -msgid "Confirm" -msgstr "أكِّد" +msgid "Add" +msgstr "أضف" #. Translate this string as you would translate "file" #: src/details_window.py:92 @@ -491,76 +498,122 @@ msgid "Couldn't Apply Preferences" msgstr "تعذَّر تطبيق التفضيلات" #. The variable is the title of the game -#: src/game.py:138 +#: src/game.py:139 msgid "{} launched" msgstr "بُدئت {}" #. The variable is the title of the game -#: src/game.py:152 +#: src/game.py:153 msgid "{} hidden" msgstr "أٌخفيت {}" -#: src/game.py:152 +#: src/game.py:153 msgid "{} unhidden" msgstr "أٌظهرت {}" -#: src/game.py:169 +#. The variable is the title of the game +#. The variable is the number of games removed +#: src/game.py:170 src/importer/importer.py:363 msgid "{} removed" msgstr "أزيلت {}" -#: src/preferences.py:112 +#: src/preferences.py:119 msgid "All games removed" msgstr "أُزيلت كلُّ الألعاب" -#: src/preferences.py:160 +#: src/preferences.py:168 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "تحتاج مفتاح واجهة برمجة حال ما أردت استخدام SteamGridDB، {}هنا تولِّده{}." -#: src/preferences.py:285 +#: src/preferences.py:294 msgid "Installation Not Found" msgstr "لم يُعثر على التثبيت" -#: src/preferences.py:287 +#: src/preferences.py:296 msgid "Select a valid directory." msgstr "حدِّد مجلَّدًا صالحًا." -#: src/preferences.py:349 +#: src/preferences.py:351 msgid "Invalid Directory" msgstr "مجلَّد غير صالح" +#: src/preferences.py:357 +msgid "Set Location" +msgstr "عيِّن الموضع" + +#: src/utils/create_dialog.py:25 src/importer/importer.py:291 +msgid "Dismiss" +msgstr "تجاهل" + +#: src/importer/importer.py:128 +msgid "Importing Games…" +msgstr "تُستورد الألعاب…" + +#: src/importer/importer.py:290 +msgid "Warning" +msgstr "" + +#: src/importer/importer.py:311 +msgid "The following errors occured during import:" +msgstr "" + +#: src/importer/importer.py:339 +msgid "No new games found" +msgstr "لم يُعثر على ألعاب جديدة" + +#: src/importer/importer.py:351 +msgid "1 game imported" +msgstr "اُستوردت لعبة واحدة" + +#. The variable is the number of games +#: src/importer/importer.py:355 +msgid "{} games imported" +msgstr "اُستوردت {} لعبة" + +#. A single game removed +#: src/importer/importer.py:359 +#, fuzzy +#| msgid "{} removed" +msgid "1 removed" +msgstr "أزيلت {}" + #. The variable is the name of the source -#: src/preferences.py:353 +#: src/importer/sources/location.py:33 msgid "Select the {} cache directory." msgstr "حدِّد مجلَّد ذاكرة {} المؤقتة." #. The variable is the name of the source -#: src/preferences.py:356 +#: src/importer/sources/location.py:35 msgid "Select the {} configuration directory." msgstr "حدِّد مجلَّد ضبط {}." #. The variable is the name of the source -#: src/preferences.py:359 +#: src/importer/sources/location.py:37 msgid "Select the {} data directory." msgstr "حدِّد مجلَّد بيانات {}." -#: src/preferences.py:365 -msgid "Set Location" -msgstr "عيِّن الموضع" - -#: src/utils/create_dialog.py:25 -msgid "Dismiss" -msgstr "تجاهل" - -#: src/store/managers/sgdb_manager.py:47 +#: src/store/managers/sgdb_manager.py:46 msgid "Couldn't Authenticate SteamGridDB" msgstr "تعذَّر استيثاق SteamGridDB" -#: src/store/managers/sgdb_manager.py:48 +#: src/store/managers/sgdb_manager.py:47 msgid "Verify your API key in preferences" msgstr "أكِّد مفتاح واجهة البرمجة في التفضيلات" +#~ msgid "The title of the game" +#~ msgstr "عنوان اللعبة" + +#~ msgid "Developer" +#~ msgstr "المطوِّر" + +#~ msgid "File to open or command to run when launching the game" +#~ msgstr "الملفُّ المفتوح أو الأمر المشغَّل عند بدء اللعبة" + +#~ msgid "Confirm" +#~ msgstr "أكِّد" + #, fuzzy #~| msgid "The Steam directory cannot be found." #~ msgid "Directory not Valid" @@ -595,21 +648,9 @@ msgstr "أكِّد مفتاح واجهة البرمجة في التفضيلات" #~ msgid "Select the Lutris cache directory." #~ msgstr "حدِّد مجلَّد ذاكرة لوترس المؤقَّتة." -#~ msgid "Importing Games…" -#~ msgstr "تُستورد الألعاب…" - #~ msgid "Importing Covers…" #~ msgstr "تُستورد الغُلُف…" -#~ msgid "No new games found" -#~ msgstr "لم يُعثر على ألعاب جديدة" - -#~ msgid "1 game imported" -#~ msgstr "اُستوردت لعبة واحدة" - -#~ msgid "{} games imported" -#~ msgstr "اُستوردت {} لعبة" - #~ msgid "Directory to use when importing games" #~ msgstr "المجلَّد المستخدم عند استيراد الألعاب" diff --git a/po/cs.po b/po/cs.po index 83da395..b802469 100644 --- a/po/cs.po +++ b/po/cs.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-25 20:33+0200\n" -"PO-Revision-Date: 2023-07-24 13:05+0000\n" +"POT-Creation-Date: 2023-08-16 11:06+0200\n" +"PO-Revision-Date: 2023-08-05 08:07+0000\n" "Last-Translator: foo expert \n" "Language-Team: Czech \n" @@ -20,7 +20,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:170 +#: src/main.py:169 msgid "Cartridges" msgstr "Kazety" @@ -35,9 +35,10 @@ msgstr "Spusťte všechny vaše hry" #: data/hu.kramo.Cartridges.desktop.in:11 #, fuzzy -#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" -msgstr "hraní;spouštěč;steam;lutris;heroic;láhve;itch;" +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" +msgid "" +"gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;retroarch;" +msgstr "hraní;spouštěč;steam;lutris;heroic;láhve;itch;flatpak;legendary;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 msgid "" @@ -54,16 +55,18 @@ msgstr "" msgid "Library" msgstr "Knihovna" -#: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67 +#: data/hu.kramo.Cartridges.metainfo.xml.in:34 msgid "Edit Game Details" msgstr "Upravit podrobnosti o hře" #: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71 +#: src/details_window.py:67 msgid "Game Details" msgstr "Podrobnosti o hře" -#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:241 +#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:418 +#: src/details_window.py:241 src/importer/importer.py:292 +#: src/importer/importer.py:342 msgid "Preferences" msgstr "Předvolby" @@ -79,32 +82,21 @@ msgstr "Nový obal" msgid "Delete Cover" msgstr "Odstranit obal" -#: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106 -#: data/gtk/game.blp:80 +#: data/gtk/details-window.blp:102 data/gtk/game.blp:80 msgid "Title" msgstr "Název" -#: data/gtk/details-window.blp:102 -msgid "The title of the game" -msgstr "Název hry" - -#: data/gtk/details-window.blp:112 data/gtk/details-window.blp:117 -msgid "Developer" -msgstr "Vývojář" - -#: data/gtk/details-window.blp:113 -msgid "The developer or publisher (optional)" +#: data/gtk/details-window.blp:105 +#, fuzzy +#| msgid "The developer or publisher (optional)" +msgid "Developer (optional)" msgstr "Vývojář nebo vydavatel (nepovinné)" -#: data/gtk/details-window.blp:123 data/gtk/details-window.blp:155 +#: data/gtk/details-window.blp:110 msgid "Executable" msgstr "Spustitelný soubor" -#: data/gtk/details-window.blp:124 -msgid "File to open or command to run when launching the game" -msgstr "Soubor nebo příkaz pro spuštění hry" - -#: data/gtk/details-window.blp:130 +#: data/gtk/details-window.blp:116 msgid "More Info" msgstr "Více informací" @@ -134,7 +126,7 @@ msgid "Quit" msgstr "Ukončit" #: data/gtk/help-overlay.blp:19 data/gtk/window.blp:217 data/gtk/window.blp:257 -#: data/gtk/window.blp:323 +#: data/gtk/window.blp:324 msgid "Search" msgstr "Vyhledávání" @@ -146,7 +138,8 @@ msgstr "Zobrazit předvolby" msgid "Shortcuts" msgstr "Zkratky" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 +#: data/gtk/help-overlay.blp:34 src/game.py:103 src/preferences.py:120 +#: src/importer/importer.py:366 msgid "Undo" msgstr "Zpět" @@ -174,7 +167,8 @@ msgstr "Zobrazit skryté hry" msgid "Remove game" msgstr "Odstranit hru" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:89 +#: data/gtk/preferences.blp:304 msgid "Behavior" msgstr "Chování" @@ -210,108 +204,116 @@ msgstr "Nebezpečná zóna" msgid "Remove All Games" msgstr "Odstranit všechny hry" -#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:442 +#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:444 msgid "Import" msgstr "Import" -#: data/gtk/preferences.blp:89 +#: data/gtk/preferences.blp:92 +#, fuzzy +#| msgid "Remove All Games" +msgid "Remove Uninstalled Games" +msgstr "Odstranit všechny hry" + +#: data/gtk/preferences.blp:102 msgid "Sources" msgstr "Zdroje" -#: data/gtk/preferences.blp:92 +#: data/gtk/preferences.blp:105 msgid "Steam" msgstr "Steam" -#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 -#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:109 data/gtk/preferences.blp:123 +#: data/gtk/preferences.blp:164 data/gtk/preferences.blp:214 +#: data/gtk/preferences.blp:228 data/gtk/preferences.blp:242 +#: data/gtk/preferences.blp:256 data/gtk/preferences.blp:270 msgid "Install Location" msgstr "Umístění instalace" -#: data/gtk/preferences.blp:106 +#: data/gtk/preferences.blp:119 msgid "Lutris" msgstr "Lutris" -#: data/gtk/preferences.blp:119 +#: data/gtk/preferences.blp:132 msgid "Cache Location" msgstr "Umístění dočasných souborů" -#: data/gtk/preferences.blp:128 +#: data/gtk/preferences.blp:141 msgid "Import Steam Games" msgstr "Importovat Steam hry" -#: data/gtk/preferences.blp:137 +#: data/gtk/preferences.blp:150 msgid "Import Flatpak Games" msgstr "Importovat Flatpak hry" -#: data/gtk/preferences.blp:147 +#: data/gtk/preferences.blp:160 msgid "Heroic" msgstr "Heroic" -#: data/gtk/preferences.blp:160 +#: data/gtk/preferences.blp:173 msgid "Import Epic Games" msgstr "Importovat Epic Games hry" -#: data/gtk/preferences.blp:169 +#: data/gtk/preferences.blp:182 msgid "Import GOG Games" msgstr "Importovat GOG hry" -#: data/gtk/preferences.blp:178 -#, fuzzy -#| msgid "Import Steam Games" +#: data/gtk/preferences.blp:191 msgid "Import Amazon Games" -msgstr "Importovat Steam hry" +msgstr "Importovat Amazon hry" -#: data/gtk/preferences.blp:187 +#: data/gtk/preferences.blp:200 msgid "Import Sideloaded Games" msgstr "Importovat ručně načtené hry" -#: data/gtk/preferences.blp:197 +#: data/gtk/preferences.blp:210 msgid "Bottles" msgstr "Láhve" -#: data/gtk/preferences.blp:211 +#: data/gtk/preferences.blp:224 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:225 +#: data/gtk/preferences.blp:238 msgid "Legendary" msgstr "Legendary" -#: data/gtk/preferences.blp:239 +#: data/gtk/preferences.blp:252 +msgid "RetroArch" +msgstr "" + +#: data/gtk/preferences.blp:266 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:252 +#: data/gtk/preferences.blp:279 msgid "Import Game Launchers" msgstr "Importovat spouštěče her" -#: data/gtk/preferences.blp:265 +#: data/gtk/preferences.blp:292 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:269 +#: data/gtk/preferences.blp:296 msgid "Authentication" msgstr "Ověření" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:299 msgid "API Key" msgstr "Klíč API" -#: data/gtk/preferences.blp:280 +#: data/gtk/preferences.blp:307 msgid "Use SteamGridDB" msgstr "Používat SteamGridDB" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:308 msgid "Download images when adding or importing games" msgstr "Stahovat obrázky při přidávání nebo importování her" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:317 msgid "Prefer Over Official Images" msgstr "Upřednostnit před oficiálními obrázky" -#: data/gtk/preferences.blp:299 +#: data/gtk/preferences.blp:326 msgid "Prefer Animated Images" msgstr "Upřednostnit animované obrázky" @@ -339,7 +341,7 @@ msgstr "Žádné skryté hry" msgid "Games you hide will appear here." msgstr "Hry, které skryjete, se zobrazí zde." -#: data/gtk/window.blp:64 data/gtk/window.blp:304 +#: data/gtk/window.blp:64 data/gtk/window.blp:305 msgid "Back" msgstr "Zpět" @@ -351,51 +353,63 @@ msgstr "Název hry" msgid "Play" msgstr "Hrát" -#: data/gtk/window.blp:243 data/gtk/window.blp:435 +#: data/gtk/window.blp:243 data/gtk/window.blp:437 msgid "Add Game" msgstr "Přidat hru" -#: data/gtk/window.blp:250 data/gtk/window.blp:316 +#: data/gtk/window.blp:250 data/gtk/window.blp:317 msgid "Main Menu" msgstr "Hlavní nabídka" -#: data/gtk/window.blp:311 +#: data/gtk/window.blp:272 +#, fuzzy +#| msgid "Search" +msgid "Search games" +msgstr "Vyhledávání" + +#: data/gtk/window.blp:312 msgid "Hidden Games" msgstr "Skryté hry" -#: data/gtk/window.blp:374 +#: data/gtk/window.blp:339 +#, fuzzy +#| msgid "Show hidden games" +msgid "Search hidden games" +msgstr "Zobrazit skryté hry" + +#: data/gtk/window.blp:376 msgid "Sort" msgstr "Třídit" -#: data/gtk/window.blp:377 +#: data/gtk/window.blp:379 msgid "A-Z" msgstr "A-Ž" -#: data/gtk/window.blp:383 +#: data/gtk/window.blp:385 msgid "Z-A" msgstr "Ž-A" -#: data/gtk/window.blp:389 +#: data/gtk/window.blp:391 msgid "Newest" msgstr "Nejnovější" -#: data/gtk/window.blp:395 +#: data/gtk/window.blp:397 msgid "Oldest" msgstr "Nejstarší" -#: data/gtk/window.blp:401 +#: data/gtk/window.blp:403 msgid "Last Played" msgstr "Naposledy hráno" -#: data/gtk/window.blp:408 +#: data/gtk/window.blp:410 msgid "Show Hidden" msgstr "Zobrazit Skryté" -#: data/gtk/window.blp:421 +#: data/gtk/window.blp:423 msgid "Keyboard Shortcuts" msgstr "Klávesové zkratky" -#: data/gtk/window.blp:426 +#: data/gtk/window.blp:428 msgid "About Cartridges" msgstr "O Kazetách" @@ -427,8 +441,8 @@ msgid "Add New Game" msgstr "Přidat novou hru" #: src/details_window.py:79 -msgid "Confirm" -msgstr "Potvrdit" +msgid "Add" +msgstr "" #. Translate this string as you would translate "file" #: src/details_window.py:92 @@ -489,73 +503,127 @@ msgid "Couldn't Apply Preferences" msgstr "Nelze použít předvolby" #. The variable is the title of the game -#: src/game.py:138 +#: src/game.py:139 msgid "{} launched" msgstr "{} spuštěno" #. The variable is the title of the game -#: src/game.py:152 +#: src/game.py:153 msgid "{} hidden" msgstr "{} skryto" -#: src/game.py:152 +#: src/game.py:153 msgid "{} unhidden" msgstr "{} odkryto" -#: src/game.py:169 +#. The variable is the title of the game +#. The variable is the number of games removed +#: src/game.py:170 src/importer/importer.py:363 msgid "{} removed" msgstr "{} odstraněno" -#: src/preferences.py:112 +#: src/preferences.py:119 msgid "All games removed" msgstr "Všechny hry odstraněny" -#: src/preferences.py:160 +#: src/preferences.py:168 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "K používání služby SteamGridDB je vyžadován klíč API. Můžete si ho " "vygenerovat {}zde{}." -#: src/preferences.py:285 +#: src/preferences.py:294 msgid "Installation Not Found" msgstr "Instalace nebyla nalezena" -#: src/preferences.py:287 +#: src/preferences.py:296 msgid "Select a valid directory." msgstr "Vyberte platný adresář." -#: src/preferences.py:349 +#: src/preferences.py:351 msgid "Invalid Directory" msgstr "Neplatný adresář" +#: src/preferences.py:357 +msgid "Set Location" +msgstr "Nastavit umístění" + +#: src/utils/create_dialog.py:25 src/importer/importer.py:291 +msgid "Dismiss" +msgstr "Zahodit" + +#: src/importer/importer.py:128 +#, fuzzy +#| msgid "Import Epic Games" +msgid "Importing Games…" +msgstr "Importovat Epic Games hry" + +#: src/importer/importer.py:290 +msgid "Warning" +msgstr "" + +#: src/importer/importer.py:311 +msgid "The following errors occured during import:" +msgstr "" + +#: src/importer/importer.py:339 +#, fuzzy +#| msgid "No Games Found" +msgid "No new games found" +msgstr "Nebyly nalezeny žádné hry" + +#: src/importer/importer.py:351 +#, fuzzy +#| msgid "All games removed" +msgid "1 game imported" +msgstr "Všechny hry odstraněny" + +#. The variable is the number of games +#: src/importer/importer.py:355 +#, fuzzy +#| msgid "All games removed" +msgid "{} games imported" +msgstr "Všechny hry odstraněny" + +#. A single game removed +#: src/importer/importer.py:359 +#, fuzzy +#| msgid "{} removed" +msgid "1 removed" +msgstr "{} odstraněno" + #. The variable is the name of the source -#: src/preferences.py:353 +#: src/importer/sources/location.py:33 msgid "Select the {} cache directory." msgstr "Vyberte adresář {} mezipaměti." #. The variable is the name of the source -#: src/preferences.py:356 +#: src/importer/sources/location.py:35 msgid "Select the {} configuration directory." msgstr "Vyberte konfigurační adresář {}." #. The variable is the name of the source -#: src/preferences.py:359 +#: src/importer/sources/location.py:37 msgid "Select the {} data directory." msgstr "Vyberte datový adresář {}." -#: src/preferences.py:365 -msgid "Set Location" -msgstr "Nastavit umístění" - -#: src/utils/create_dialog.py:25 -msgid "Dismiss" -msgstr "Zahodit" - -#: src/store/managers/sgdb_manager.py:47 +#: src/store/managers/sgdb_manager.py:46 msgid "Couldn't Authenticate SteamGridDB" msgstr "Nelze ověřit SteamGridDB" -#: src/store/managers/sgdb_manager.py:48 +#: src/store/managers/sgdb_manager.py:47 msgid "Verify your API key in preferences" msgstr "Ověřte váš klíč API v předvolbách" + +#~ msgid "The title of the game" +#~ msgstr "Název hry" + +#~ msgid "Developer" +#~ msgstr "Vývojář" + +#~ msgid "File to open or command to run when launching the game" +#~ msgstr "Soubor nebo příkaz pro spuštění hry" + +#~ msgid "Confirm" +#~ msgstr "Potvrdit" diff --git a/po/de.po b/po/de.po index bf27566..c1ab6d7 100644 --- a/po/de.po +++ b/po/de.po @@ -4,13 +4,14 @@ # Jummit , 2023. # WebSnke , 2023. # Ettore Atalan , 2023. +# Simon Hahne , 2023. msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-25 20:33+0200\n" -"PO-Revision-Date: 2023-04-17 17:20+0000\n" -"Last-Translator: Ettore Atalan \n" +"POT-Creation-Date: 2023-08-16 11:06+0200\n" +"PO-Revision-Date: 2023-08-05 08:07+0000\n" +"Last-Translator: Simon Hahne \n" "Language-Team: German \n" "Language: de\n" @@ -18,11 +19,11 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.0-dev\n" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:170 +#: src/main.py:169 msgid "Cartridges" msgstr "Cartridges" @@ -36,8 +37,11 @@ msgid "Launch all your games" msgstr "Starte all deine Spiele" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" -msgstr "" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" +msgid "" +"gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;retroarch;" +msgstr "spiel;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 msgid "" @@ -55,16 +59,18 @@ msgstr "" msgid "Library" msgstr "Bibliothek" -#: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67 +#: data/hu.kramo.Cartridges.metainfo.xml.in:34 msgid "Edit Game Details" msgstr "Spieldetails bearbeiten" #: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71 +#: src/details_window.py:67 msgid "Game Details" msgstr "Spieldetails" -#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:241 +#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:418 +#: src/details_window.py:241 src/importer/importer.py:292 +#: src/importer/importer.py:342 msgid "Preferences" msgstr "Einstellungen" @@ -74,40 +80,29 @@ msgstr "Abbrechen" #: data/gtk/details-window.blp:57 msgid "New Cover" -msgstr "" +msgstr "Neues Cover" #: data/gtk/details-window.blp:75 msgid "Delete Cover" -msgstr "" +msgstr "Cover löschen" -#: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106 -#: data/gtk/game.blp:80 +#: data/gtk/details-window.blp:102 data/gtk/game.blp:80 msgid "Title" msgstr "Titel" -#: data/gtk/details-window.blp:102 -msgid "The title of the game" -msgstr "Der Titel des Spiels" - -#: data/gtk/details-window.blp:112 data/gtk/details-window.blp:117 -msgid "Developer" -msgstr "Entwickler" - -#: data/gtk/details-window.blp:113 -msgid "The developer or publisher (optional)" +#: data/gtk/details-window.blp:105 +#, fuzzy +#| msgid "The developer or publisher (optional)" +msgid "Developer (optional)" msgstr "Der Entwickler oder Verlag (optional)" -#: data/gtk/details-window.blp:123 data/gtk/details-window.blp:155 +#: data/gtk/details-window.blp:110 msgid "Executable" msgstr "Ausführbare Datei" -#: data/gtk/details-window.blp:124 -msgid "File to open or command to run when launching the game" -msgstr "Datei zum Öffnen oder Befehl zum Starten des Spiels" - -#: data/gtk/details-window.blp:130 +#: data/gtk/details-window.blp:116 msgid "More Info" -msgstr "" +msgstr "Weitere Informationen" #: data/gtk/game.blp:102 data/gtk/game.blp:121 data/gtk/window.blp:195 msgid "Edit" @@ -135,7 +130,7 @@ msgid "Quit" msgstr "Beenden" #: data/gtk/help-overlay.blp:19 data/gtk/window.blp:217 data/gtk/window.blp:257 -#: data/gtk/window.blp:323 +#: data/gtk/window.blp:324 msgid "Search" msgstr "Suchen" @@ -147,7 +142,8 @@ msgstr "Einstellungen anzeigen" msgid "Shortcuts" msgstr "Kürzel" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 +#: data/gtk/help-overlay.blp:34 src/game.py:103 src/preferences.py:120 +#: src/importer/importer.py:366 msgid "Undo" msgstr "Rückgängig" @@ -175,7 +171,8 @@ msgstr "Ausgeblendete Spiele anzeigen" msgid "Remove game" msgstr "Spiel entfernen" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:89 +#: data/gtk/preferences.blp:304 msgid "Behavior" msgstr "Verhalten" @@ -211,116 +208,116 @@ msgstr "Gefahrenzone" msgid "Remove All Games" msgstr "Alle Spiele entfernen" -#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:442 +#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:444 msgid "Import" msgstr "Importieren" -#: data/gtk/preferences.blp:89 +#: data/gtk/preferences.blp:92 +#, fuzzy +#| msgid "Remove All Games" +msgid "Remove Uninstalled Games" +msgstr "Alle Spiele entfernen" + +#: data/gtk/preferences.blp:102 msgid "Sources" msgstr "Quellen" -#: data/gtk/preferences.blp:92 +#: data/gtk/preferences.blp:105 msgid "Steam" msgstr "Steam" -#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 -#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 -#: data/gtk/preferences.blp:243 -#, fuzzy -#| msgid "itch Install Location" +#: data/gtk/preferences.blp:109 data/gtk/preferences.blp:123 +#: data/gtk/preferences.blp:164 data/gtk/preferences.blp:214 +#: data/gtk/preferences.blp:228 data/gtk/preferences.blp:242 +#: data/gtk/preferences.blp:256 data/gtk/preferences.blp:270 msgid "Install Location" -msgstr "itch-Installationsort" +msgstr "Installationsort" -#: data/gtk/preferences.blp:106 +#: data/gtk/preferences.blp:119 msgid "Lutris" msgstr "Lutris" -#: data/gtk/preferences.blp:119 -#, fuzzy -#| msgid "Lutris Cache Location" +#: data/gtk/preferences.blp:132 msgid "Cache Location" -msgstr "Lutris-Cacheort" +msgstr "Cache-Speicherort" -#: data/gtk/preferences.blp:128 +#: data/gtk/preferences.blp:141 msgid "Import Steam Games" msgstr "Steam-Spiele importieren" -#: data/gtk/preferences.blp:137 -#, fuzzy -#| msgid "Import Steam Games" +#: data/gtk/preferences.blp:150 msgid "Import Flatpak Games" -msgstr "Steam-Spiele importieren" +msgstr "Flatpak-Spiele importieren" -#: data/gtk/preferences.blp:147 +#: data/gtk/preferences.blp:160 msgid "Heroic" msgstr "Heroic" -#: data/gtk/preferences.blp:160 +#: data/gtk/preferences.blp:173 msgid "Import Epic Games" msgstr "Epic Games importieren" -#: data/gtk/preferences.blp:169 +#: data/gtk/preferences.blp:182 msgid "Import GOG Games" msgstr "GOG-Spiele importieren" -#: data/gtk/preferences.blp:178 -#, fuzzy -#| msgid "Import Steam Games" +#: data/gtk/preferences.blp:191 msgid "Import Amazon Games" -msgstr "Steam-Spiele importieren" +msgstr "Amazon-Spiele importieren" -#: data/gtk/preferences.blp:187 +#: data/gtk/preferences.blp:200 msgid "Import Sideloaded Games" msgstr "Sideloaded-Spiele importieren" -#: data/gtk/preferences.blp:197 +#: data/gtk/preferences.blp:210 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:211 +#: data/gtk/preferences.blp:224 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:225 +#: data/gtk/preferences.blp:238 msgid "Legendary" -msgstr "" - -#: data/gtk/preferences.blp:239 -msgid "Flatpak" -msgstr "" +msgstr "Legendary" #: data/gtk/preferences.blp:252 -#, fuzzy -#| msgid "Game Launcher" -msgid "Import Game Launchers" -msgstr "Spiele-Launcher" +msgid "RetroArch" +msgstr "" -#: data/gtk/preferences.blp:265 +#: data/gtk/preferences.blp:266 +msgid "Flatpak" +msgstr "Flatpak" + +#: data/gtk/preferences.blp:279 +msgid "Import Game Launchers" +msgstr "Spiele-Launcher importieren" + +#: data/gtk/preferences.blp:292 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:269 +#: data/gtk/preferences.blp:296 msgid "Authentication" msgstr "Authentifizierung" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:299 msgid "API Key" msgstr "API-Schlüssel" -#: data/gtk/preferences.blp:280 +#: data/gtk/preferences.blp:307 msgid "Use SteamGridDB" msgstr "SteamGridDB benutzen" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:308 msgid "Download images when adding or importing games" msgstr "Lade Bilder herunter, wenn Spiele hinzugefügt oder importiert werden" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:317 msgid "Prefer Over Official Images" msgstr "Über offizielien Images bevorzugen" -#: data/gtk/preferences.blp:299 +#: data/gtk/preferences.blp:326 msgid "Prefer Animated Images" msgstr "Animierte Bilder bevorzugen" @@ -348,7 +345,7 @@ msgstr "Keine versteckten Spiele" msgid "Games you hide will appear here." msgstr "Ausgeblendete Spiele, werden hier angezeigt." -#: data/gtk/window.blp:64 data/gtk/window.blp:304 +#: data/gtk/window.blp:64 data/gtk/window.blp:305 msgid "Back" msgstr "Zurück" @@ -360,51 +357,63 @@ msgstr "Spieltitel" msgid "Play" msgstr "Spielen" -#: data/gtk/window.blp:243 data/gtk/window.blp:435 +#: data/gtk/window.blp:243 data/gtk/window.blp:437 msgid "Add Game" msgstr "Spiel hinzufügen" -#: data/gtk/window.blp:250 data/gtk/window.blp:316 +#: data/gtk/window.blp:250 data/gtk/window.blp:317 msgid "Main Menu" msgstr "Hauptmenü" -#: data/gtk/window.blp:311 +#: data/gtk/window.blp:272 +#, fuzzy +#| msgid "Search" +msgid "Search games" +msgstr "Suchen" + +#: data/gtk/window.blp:312 msgid "Hidden Games" msgstr "Ausgeblendete Spiele" -#: data/gtk/window.blp:374 +#: data/gtk/window.blp:339 +#, fuzzy +#| msgid "Show hidden games" +msgid "Search hidden games" +msgstr "Ausgeblendete Spiele anzeigen" + +#: data/gtk/window.blp:376 msgid "Sort" msgstr "Sortierung" -#: data/gtk/window.blp:377 +#: data/gtk/window.blp:379 msgid "A-Z" msgstr "A-Z" -#: data/gtk/window.blp:383 +#: data/gtk/window.blp:385 msgid "Z-A" msgstr "Z-A" -#: data/gtk/window.blp:389 +#: data/gtk/window.blp:391 msgid "Newest" msgstr "Neuestes" -#: data/gtk/window.blp:395 +#: data/gtk/window.blp:397 msgid "Oldest" msgstr "Älteste" -#: data/gtk/window.blp:401 +#: data/gtk/window.blp:403 msgid "Last Played" msgstr "Zuletzt gespielt" -#: data/gtk/window.blp:408 +#: data/gtk/window.blp:410 msgid "Show Hidden" msgstr "Ausgeblendete anzeigen" -#: data/gtk/window.blp:421 +#: data/gtk/window.blp:423 msgid "Keyboard Shortcuts" msgstr "Tastaturkürzel" -#: data/gtk/window.blp:426 +#: data/gtk/window.blp:428 msgid "About Cartridges" msgstr "Über Cartridges" @@ -436,8 +445,8 @@ msgid "Add New Game" msgstr "Neues Spiel hinzufügen" #: src/details_window.py:79 -msgid "Confirm" -msgstr "Bestätigen" +msgid "Add" +msgstr "" #. Translate this string as you would translate "file" #: src/details_window.py:92 @@ -499,88 +508,136 @@ msgid "Couldn't Apply Preferences" msgstr "Einstellungen konnten nicht angewendet werden" #. The variable is the title of the game -#: src/game.py:138 +#: src/game.py:139 msgid "{} launched" msgstr "{} gestartet" #. The variable is the title of the game -#: src/game.py:152 +#: src/game.py:153 msgid "{} hidden" msgstr "{} versteckt" -#: src/game.py:152 +#: src/game.py:153 msgid "{} unhidden" msgstr "{} unversteckt" -#: src/game.py:169 +#. The variable is the title of the game +#. The variable is the number of games removed +#: src/game.py:170 src/importer/importer.py:363 msgid "{} removed" msgstr "{} entfernt" -#: src/preferences.py:112 +#: src/preferences.py:119 msgid "All games removed" msgstr "Alle Spiele entfernt" -#: src/preferences.py:160 +#: src/preferences.py:168 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Für die Nutzung von SteamGridDB ist ein API-Schlüssel erforderlich. Sie " "können ihn {}hier{} generieren." -#: src/preferences.py:285 -#, fuzzy -#| msgid "Installation Not Found" +#: src/preferences.py:294 msgid "Installation Not Found" msgstr "Installation nicht gefunden" -#: src/preferences.py:287 -#, fuzzy -#| msgid "Select the {} data directory." +#: src/preferences.py:296 msgid "Select a valid directory." -msgstr "Wähle das Datenverzeichnis von {} aus." +msgstr "Wähle ein gültiges Verzeichnis aus." -#: src/preferences.py:349 +#: src/preferences.py:351 msgid "Invalid Directory" +msgstr "Ungültiges Verzeichnis" + +#: src/preferences.py:357 +msgid "Set Location" +msgstr "Ort festlegen" + +#: src/utils/create_dialog.py:25 src/importer/importer.py:291 +msgid "Dismiss" +msgstr "Verstanden" + +#: src/importer/importer.py:128 +msgid "Importing Games…" +msgstr "Spiele werden importiert…" + +#: src/importer/importer.py:290 +msgid "Warning" msgstr "" +#: src/importer/importer.py:311 +msgid "The following errors occured during import:" +msgstr "" + +#: src/importer/importer.py:339 +#, fuzzy +#| msgid "No Games Found" +msgid "No new games found" +msgstr "Keine Spiele gefunden" + +#: src/importer/importer.py:351 +#, fuzzy +#| msgid "Game Imported" +msgid "1 game imported" +msgstr "Spiel Importiert" + +#. The variable is the number of games +#: src/importer/importer.py:355 +#, fuzzy +#| msgid "Games Imported" +msgid "{} games imported" +msgstr "Spiele importiert" + +#. A single game removed +#: src/importer/importer.py:359 +#, fuzzy +#| msgid "{} removed" +msgid "1 removed" +msgstr "{} entfernt" + #. The variable is the name of the source -#: src/preferences.py:353 +#: src/importer/sources/location.py:33 #, fuzzy #| msgid "Select the {} data directory." msgid "Select the {} cache directory." -msgstr "Wähle das Datenverzeichnis von {} aus." +msgstr "Wähle das Cache-Verzeichnis von {} aus." #. The variable is the name of the source -#: src/preferences.py:356 +#: src/importer/sources/location.py:35 #, fuzzy #| msgid "Select the {} configuration directory." msgid "Select the {} configuration directory." msgstr "Wähle das Konfigurationsverzeichnis von {} aus." #. The variable is the name of the source -#: src/preferences.py:359 +#: src/importer/sources/location.py:37 #, fuzzy #| msgid "Select the {} data directory." msgid "Select the {} data directory." msgstr "Wähle das Datenverzeichnis von {} aus." -#: src/preferences.py:365 -msgid "Set Location" -msgstr "Ort festlegen" - -#: src/utils/create_dialog.py:25 -msgid "Dismiss" -msgstr "Verstanden" - -#: src/store/managers/sgdb_manager.py:47 +#: src/store/managers/sgdb_manager.py:46 #, fuzzy #| msgid "Couldn't Connect to SteamGridDB" msgid "Couldn't Authenticate SteamGridDB" msgstr "Verbindung zu SteamGridDB konnte nicht hergestellt werden" -#: src/store/managers/sgdb_manager.py:48 +#: src/store/managers/sgdb_manager.py:47 msgid "Verify your API key in preferences" -msgstr "" +msgstr "Verifiziere deinen API-Schlüssel in den Einstellungen" + +#~ msgid "The title of the game" +#~ msgstr "Der Titel des Spiels" + +#~ msgid "Developer" +#~ msgstr "Entwickler" + +#~ msgid "File to open or command to run when launching the game" +#~ msgstr "Datei zum Öffnen oder Befehl zum Starten des Spiels" + +#~ msgid "Confirm" +#~ msgstr "Bestätigen" #, fuzzy #~| msgid "Cache Not Found" @@ -613,27 +670,9 @@ msgstr "" #~ msgid "Select the Lutris cache directory." #~ msgstr "Wähle das Lutris-Cache-Verzeichnis aus." -#~ msgid "Importing Games…" -#~ msgstr "Spiele werden importiert…" - #~ msgid "Importing Covers…" #~ msgstr "Spielecover werden importiert…" -#, fuzzy -#~| msgid "No Games Found" -#~ msgid "No new games found" -#~ msgstr "Keine Spiele gefunden" - -#, fuzzy -#~| msgid "Game Imported" -#~ msgid "1 game imported" -#~ msgstr "Spiel Importiert" - -#, fuzzy -#~| msgid "Games Imported" -#~ msgid "{} games imported" -#~ msgstr "Spiele importiert" - #~ msgid "Directory to use when importing games" #~ msgstr "Verzeichnis, das beim Importieren von Spielen verwendet wird" diff --git a/po/el.po b/po/el.po index 7b63b8d..89a75f0 100644 --- a/po/el.po +++ b/po/el.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-25 20:33+0200\n" +"POT-Creation-Date: 2023-08-16 11:06+0200\n" "PO-Revision-Date: 2023-05-29 18:48+0000\n" "Last-Translator: yiannis ioannides \n" "Language-Team: Greek