diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp index a0e8305..21a4769 100644 --- a/data/gtk/preferences.blp +++ b/data/gtk/preferences.blp @@ -174,6 +174,15 @@ template $PreferencesWindow : Adw.PreferencesWindow { } } + Adw.ActionRow { + title: _("Import Amazon Games"); + activatable-widget: heroic_import_amazon_switch; + + Switch heroic_import_amazon_switch { + valign: center; + } + } + Adw.ActionRow { title: _("Import Sideloaded Games"); activatable-widget: heroic_import_sideload_switch; diff --git a/data/hu.kramo.Cartridges.gschema.xml.in b/data/hu.kramo.Cartridges.gschema.xml.in index bf1199d..579d345 100644 --- a/data/hu.kramo.Cartridges.gschema.xml.in +++ b/data/hu.kramo.Cartridges.gschema.xml.in @@ -43,6 +43,9 @@ true + + true + true diff --git a/data/hu.kramo.Cartridges.metainfo.xml.in b/data/hu.kramo.Cartridges.metainfo.xml.in index e0eba2d..12ce985 100644 --- a/data/hu.kramo.Cartridges.metainfo.xml.in +++ b/data/hu.kramo.Cartridges.metainfo.xml.in @@ -44,10 +44,19 @@ - +
    -
  • Fixes an issue with Steam mods not importing properly
  • +
  • Fixes an issue with translations
  • +
  • Translations since 2.1
  • +
+
+
+ + +
    +
  • Added support for Amazon Games in the Heroic importer
  • +
  • Translations since 2.0
diff --git a/flatpak/hu.kramo.Cartridges.Devel.json b/flatpak/hu.kramo.Cartridges.Devel.json index e929dfa..28fd115 100644 --- a/flatpak/hu.kramo.Cartridges.Devel.json +++ b/flatpak/hu.kramo.Cartridges.Devel.json @@ -16,6 +16,7 @@ "--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro", "--filesystem=~/.var/app/net.lutris.Lutris/:ro", "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro", + "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/legendary/:ro", "--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro", "--filesystem=~/.var/app/io.itch.itch/config/itch/:ro", "--filesystem=/var/lib/flatpak:ro" diff --git a/meson.build b/meson.build index 94a902a..ac33e44 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('cartridges', - version: '2.0.6', + version: '2.1.1', meson_version: '>= 0.59.0', default_options: [ 'warning_level=2', 'werror=false', ], ) diff --git a/po/LINGUAS b/po/LINGUAS index 88aad50..799d587 100644 --- a/po/LINGUAS +++ b/po/LINGUAS @@ -19,3 +19,4 @@ pl sv tr el +cs diff --git a/po/ar.po b/po/ar.po index 73185ab..506fe72 100644 --- a/po/ar.po +++ b/po/ar.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-09 07:59+0000\n" "Last-Translator: Ali Aljishi \n" "Language-Team: Arabic " @@ -470,15 +478,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "تعذَّرت إضافة اللعبة" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "لا يجوز كون عنوان اللعبة فارغًا." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "لا يجوز كون ملفِّ التنفيذ فارغًا." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "تعذَّر تطبيق التفضيلات" @@ -500,44 +508,44 @@ msgstr "أٌظهرت {}" msgid "{} removed" msgstr "أزيلت {}" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "أُزيلت كلُّ الألعاب" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "تحتاج مفتاح واجهة برمجة حال ما أردت استخدام SteamGridDB، {}هنا تولِّده{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "لم يُعثر على التثبيت" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "حدِّد مجلَّدًا صالحًا." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "مجلَّد غير صالح" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "حدِّد مجلَّد ذاكرة {} المؤقتة." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "حدِّد مجلَّد ضبط {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "حدِّد مجلَّد بيانات {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "عيِّن الموضع" diff --git a/po/cartridges.pot b/po/cartridges.pot index 1aa2a79..6943828 100644 --- a/po/cartridges.pot +++ b/po/cartridges.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -19,7 +19,7 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" msgstr "" @@ -33,7 +33,7 @@ msgid "Launch all your games" msgstr "" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -57,7 +57,7 @@ msgid "Game Details" msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "" @@ -140,7 +140,7 @@ msgstr "" msgid "Shortcuts" msgstr "" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "" @@ -168,7 +168,7 @@ msgstr "" msgid "Remove game" msgstr "" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "" @@ -217,9 +217,9 @@ msgid "Steam" msgstr "" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "" @@ -252,54 +252,58 @@ msgid "Import GOG Games" msgstr "" #: data/gtk/preferences.blp:178 +msgid "Import Amazon Games" +msgstr "" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "" @@ -388,7 +392,7 @@ msgid "About Cartridges" msgstr "" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "" @@ -455,15 +459,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "" -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "" -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "" @@ -485,43 +489,43 @@ msgstr "" msgid "{} removed" msgstr "" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "" -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "" #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "" #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "" -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "" diff --git a/po/cs.po b/po/cs.po new file mode 100644 index 0000000..83da395 --- /dev/null +++ b/po/cs.po @@ -0,0 +1,561 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR kramo +# This file is distributed under the same license as the Cartridges package. +# foo expert , 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-07-24 13:05+0000\n" +"Last-Translator: foo expert \n" +"Language-Team: Czech \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\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 +msgid "Cartridges" +msgstr "Kazety" + +#: data/hu.kramo.Cartridges.desktop.in:4 +msgid "Game Launcher" +msgstr "Spouštěč her" + +#: data/hu.kramo.Cartridges.desktop.in:5 +#: data/hu.kramo.Cartridges.metainfo.xml.in:7 +msgid "Launch all your games" +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;" + +#: data/hu.kramo.Cartridges.metainfo.xml.in:9 +msgid "" +"Cartridges is a simple game launcher for all of your games. It has support " +"for importing games from Steam, Lutris, Heroic and more with no login " +"necessary. You can sort and hide games or download cover art from " +"SteamGridDB." +msgstr "" +"Kazety jsou jednoduchý spouštěč pro všechny vaše hry. Podporuje importovaní " +"her ze služeb Steam, Lutris, Heroic a dalších bez nutnosti přihlášení. Hry " +"můžete třídit a skrývat nebo stahovat obálky ze služby SteamGridDB." + +#: data/hu.kramo.Cartridges.metainfo.xml.in:30 +msgid "Library" +msgstr "Knihovna" + +#: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67 +msgid "Edit Game Details" +msgstr "Upravit podrobnosti o hře" + +#: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71 +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 +msgid "Preferences" +msgstr "Předvolby" + +#: data/gtk/details-window.blp:25 +msgid "Cancel" +msgstr "Zrušit" + +#: data/gtk/details-window.blp:57 +msgid "New Cover" +msgstr "Nový obal" + +#: data/gtk/details-window.blp:75 +msgid "Delete Cover" +msgstr "Odstranit obal" + +#: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106 +#: 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)" +msgstr "Vývojář nebo vydavatel (nepovinné)" + +#: data/gtk/details-window.blp:123 data/gtk/details-window.blp:155 +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 +msgid "More Info" +msgstr "Více informací" + +#: data/gtk/game.blp:102 data/gtk/game.blp:121 data/gtk/window.blp:195 +msgid "Edit" +msgstr "Upravit" + +#: data/gtk/game.blp:107 src/window.py:171 +msgid "Hide" +msgstr "Skrýt" + +#: data/gtk/game.blp:112 data/gtk/game.blp:131 data/gtk/preferences.blp:56 +#: data/gtk/window.blp:209 +msgid "Remove" +msgstr "Odstranit" + +#: data/gtk/game.blp:126 src/window.py:173 +msgid "Unhide" +msgstr "Odkrýt" + +#: data/gtk/help-overlay.blp:11 data/gtk/preferences.blp:9 +msgid "General" +msgstr "Obecné" + +#: data/gtk/help-overlay.blp:14 +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 +msgid "Search" +msgstr "Vyhledávání" + +#: data/gtk/help-overlay.blp:24 +msgid "Show preferences" +msgstr "Zobrazit předvolby" + +#: data/gtk/help-overlay.blp:29 +msgid "Shortcuts" +msgstr "Zkratky" + +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 +msgid "Undo" +msgstr "Zpět" + +#: data/gtk/help-overlay.blp:39 +msgid "Open menu" +msgstr "Otevřít nabídku" + +#: data/gtk/help-overlay.blp:45 +msgid "Games" +msgstr "Hry" + +#: data/gtk/help-overlay.blp:48 +msgid "Add new game" +msgstr "Přidat novou hru" + +#: data/gtk/help-overlay.blp:53 +msgid "Import games" +msgstr "Importovat hry" + +#: data/gtk/help-overlay.blp:58 +msgid "Show hidden games" +msgstr "Zobrazit skryté hry" + +#: data/gtk/help-overlay.blp:63 +msgid "Remove game" +msgstr "Odstranit hru" + +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 +msgid "Behavior" +msgstr "Chování" + +#: data/gtk/preferences.blp:16 +msgid "Exit After Launching Games" +msgstr "Ukončit po spuštění her" + +#: data/gtk/preferences.blp:25 +msgid "Cover Image Launches Game" +msgstr "Obrázek na obálce spouští hru" + +#: data/gtk/preferences.blp:26 +msgid "Swaps the behavior of the cover image and the play button" +msgstr "Vymění chování obrázku na obálce a tlačítka pro přehrávání" + +#: data/gtk/preferences.blp:36 src/details_window.py:81 +msgid "Images" +msgstr "Obrázky" + +#: data/gtk/preferences.blp:39 +msgid "High Quality Images" +msgstr "Vysoce kvalitní obrázky" + +#: data/gtk/preferences.blp:40 +msgid "Save game covers losslessly at the cost of storage" +msgstr "Ukládat obaly her bezztrátově na úkor většího místa na disku" + +#: data/gtk/preferences.blp:50 +msgid "Danger Zone" +msgstr "Nebezpečná zóna" + +#: data/gtk/preferences.blp:53 +msgid "Remove All Games" +msgstr "Odstranit všechny hry" + +#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:442 +msgid "Import" +msgstr "Import" + +#: data/gtk/preferences.blp:89 +msgid "Sources" +msgstr "Zdroje" + +#: data/gtk/preferences.blp:92 +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 +msgid "Install Location" +msgstr "Umístění instalace" + +#: data/gtk/preferences.blp:106 +msgid "Lutris" +msgstr "Lutris" + +#: data/gtk/preferences.blp:119 +msgid "Cache Location" +msgstr "Umístění dočasných souborů" + +#: data/gtk/preferences.blp:128 +msgid "Import Steam Games" +msgstr "Importovat Steam hry" + +#: data/gtk/preferences.blp:137 +msgid "Import Flatpak Games" +msgstr "Importovat Flatpak hry" + +#: data/gtk/preferences.blp:147 +msgid "Heroic" +msgstr "Heroic" + +#: data/gtk/preferences.blp:160 +msgid "Import Epic Games" +msgstr "Importovat Epic Games hry" + +#: data/gtk/preferences.blp:169 +msgid "Import GOG Games" +msgstr "Importovat GOG hry" + +#: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Importovat Steam hry" + +#: data/gtk/preferences.blp:187 +msgid "Import Sideloaded Games" +msgstr "Importovat ručně načtené hry" + +#: data/gtk/preferences.blp:197 +msgid "Bottles" +msgstr "Láhve" + +#: data/gtk/preferences.blp:211 +msgid "itch" +msgstr "itch" + +#: data/gtk/preferences.blp:225 +msgid "Legendary" +msgstr "Legendary" + +#: data/gtk/preferences.blp:239 +msgid "Flatpak" +msgstr "Flatpak" + +#: data/gtk/preferences.blp:252 +msgid "Import Game Launchers" +msgstr "Importovat spouštěče her" + +#: data/gtk/preferences.blp:265 +msgid "SteamGridDB" +msgstr "SteamGridDB" + +#: data/gtk/preferences.blp:269 +msgid "Authentication" +msgstr "Ověření" + +#: data/gtk/preferences.blp:272 +msgid "API Key" +msgstr "Klíč API" + +#: data/gtk/preferences.blp:280 +msgid "Use SteamGridDB" +msgstr "Používat SteamGridDB" + +#: data/gtk/preferences.blp:281 +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 +msgid "Prefer Over Official Images" +msgstr "Upřednostnit před oficiálními obrázky" + +#: data/gtk/preferences.blp:299 +msgid "Prefer Animated Images" +msgstr "Upřednostnit animované obrázky" + +#: data/gtk/window.blp:6 data/gtk/window.blp:14 +msgid "No Games Found" +msgstr "Nebyly nalezeny žádné hry" + +#: data/gtk/window.blp:7 data/gtk/window.blp:15 +msgid "Try a different search." +msgstr "Zkuste hledat něco jiného." + +#: data/gtk/window.blp:21 +msgid "No Games" +msgstr "Žádné hry" + +#: data/gtk/window.blp:22 +msgid "Use the + button to add games." +msgstr "Tlačítkem + můžete přidávat hry." + +#: data/gtk/window.blp:40 +msgid "No Hidden Games" +msgstr "Žádné skryté hry" + +#: data/gtk/window.blp:41 +msgid "Games you hide will appear here." +msgstr "Hry, které skryjete, se zobrazí zde." + +#: data/gtk/window.blp:64 data/gtk/window.blp:304 +msgid "Back" +msgstr "Zpět" + +#: data/gtk/window.blp:121 +msgid "Game Title" +msgstr "Název hry" + +#: data/gtk/window.blp:176 +msgid "Play" +msgstr "Hrát" + +#: data/gtk/window.blp:243 data/gtk/window.blp:435 +msgid "Add Game" +msgstr "Přidat hru" + +#: data/gtk/window.blp:250 data/gtk/window.blp:316 +msgid "Main Menu" +msgstr "Hlavní nabídka" + +#: data/gtk/window.blp:311 +msgid "Hidden Games" +msgstr "Skryté hry" + +#: data/gtk/window.blp:374 +msgid "Sort" +msgstr "Třídit" + +#: data/gtk/window.blp:377 +msgid "A-Z" +msgstr "A-Ž" + +#: data/gtk/window.blp:383 +msgid "Z-A" +msgstr "Ž-A" + +#: data/gtk/window.blp:389 +msgid "Newest" +msgstr "Nejnovější" + +#: data/gtk/window.blp:395 +msgid "Oldest" +msgstr "Nejstarší" + +#: data/gtk/window.blp:401 +msgid "Last Played" +msgstr "Naposledy hráno" + +#: data/gtk/window.blp:408 +msgid "Show Hidden" +msgstr "Zobrazit Skryté" + +#: data/gtk/window.blp:421 +msgid "Keyboard Shortcuts" +msgstr "Klávesové zkratky" + +#: data/gtk/window.blp:426 +msgid "About Cartridges" +msgstr "O Kazetách" + +#. Translators: Replace this with your name for it to show up in the about window +#: src/main.py:188 +msgid "translator_credits" +msgstr "ooo.i.love.foo" + +#. The variable is the date when the game was added +#: src/window.py:194 +msgid "Added: {}" +msgstr "Přidáno: {}" + +#: src/window.py:197 +msgid "Never" +msgstr "Nikdy" + +#. The variable is the date when the game was last played +#: src/window.py:201 +msgid "Last played: {}" +msgstr "Naposledy hráno: {}" + +#: src/details_window.py:72 +msgid "Apply" +msgstr "Použít" + +#: src/details_window.py:78 +msgid "Add New Game" +msgstr "Přidat novou hru" + +#: src/details_window.py:79 +msgid "Confirm" +msgstr "Potvrdit" + +#. Translate this string as you would translate "file" +#: src/details_window.py:92 +msgid "file.txt" +msgstr "soubor.txt" + +#. As in software +#: src/details_window.py:94 +msgid "program" +msgstr "program" + +#. Translate this string as you would translate "path to {}" +#: src/details_window.py:99 src/details_window.py:101 +msgid "C:\\path\\to\\{}" +msgstr "C:\\cesta\\k\\{}" + +#. Translate this string as you would translate "path to {}" +#: src/details_window.py:105 src/details_window.py:107 +msgid "/path/to/{}" +msgstr "/cesta/k/{}" + +#: src/details_window.py:112 +msgid "" +"To launch the executable \"{}\", use the command:\n" +"\n" +"\"{}\"\n" +"\n" +"To open the file \"{}\" with the default application, use:\n" +"\n" +"{} \"{}\"\n" +"\n" +"If the path contains spaces, make sure to wrap it in double quotes!" +msgstr "" +"Chcete-li spustit spustitelný soubor \"{}\", použijte příkaz:\n" +"\n" +"\"{}\"\n" +"\n" +"Chcete-li otevřít soubor \"{}\" pomocí výchozí aplikace, použijte příkaz:\n" +"\n" +"{} \"{}\"\n" +"\n" +"Pokud cesta obsahuje mezery, nezapomeňte ji zabalit do dvojitých uvozovek!" + +#: src/details_window.py:147 src/details_window.py:153 +msgid "Couldn't Add Game" +msgstr "Nelze přidat hru" + +#: src/details_window.py:147 src/details_window.py:183 +msgid "Game title cannot be empty." +msgstr "Název hry nemůže být prázdný." + +#: src/details_window.py:153 src/details_window.py:191 +msgid "Executable cannot be empty." +msgstr "Spustitelný soubor nemůže být prázdný." + +#: src/details_window.py:182 src/details_window.py:190 +msgid "Couldn't Apply Preferences" +msgstr "Nelze použít předvolby" + +#. The variable is the title of the game +#: src/game.py:138 +msgid "{} launched" +msgstr "{} spuštěno" + +#. The variable is the title of the game +#: src/game.py:152 +msgid "{} hidden" +msgstr "{} skryto" + +#: src/game.py:152 +msgid "{} unhidden" +msgstr "{} odkryto" + +#: src/game.py:169 +msgid "{} removed" +msgstr "{} odstraněno" + +#: src/preferences.py:112 +msgid "All games removed" +msgstr "Všechny hry odstraněny" + +#: src/preferences.py:160 +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 +msgid "Installation Not Found" +msgstr "Instalace nebyla nalezena" + +#: src/preferences.py:287 +msgid "Select a valid directory." +msgstr "Vyberte platný adresář." + +#: src/preferences.py:349 +msgid "Invalid Directory" +msgstr "Neplatný adresář" + +#. The variable is the name of the source +#: src/preferences.py:353 +msgid "Select the {} cache directory." +msgstr "Vyberte adresář {} mezipaměti." + +#. The variable is the name of the source +#: src/preferences.py:356 +msgid "Select the {} configuration directory." +msgstr "Vyberte konfigurační adresář {}." + +#. The variable is the name of the source +#: src/preferences.py:359 +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 +msgid "Couldn't Authenticate SteamGridDB" +msgstr "Nelze ověřit SteamGridDB" + +#: src/store/managers/sgdb_manager.py:48 +msgid "Verify your API key in preferences" +msgstr "Ověřte váš klíč API v předvolbách" diff --git a/po/de.po b/po/de.po index 15f3df3..bf27566 100644 --- a/po/de.po +++ b/po/de.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\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" "Language-Team: German \n" "Language-Team: Greek \n" +"Last-Translator: Óscar Fernández Díaz \n" "Language-Team: Spanish \n" "Language: es\n" @@ -23,7 +23,7 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" msgstr "Cartuchos" @@ -37,7 +37,9 @@ msgid "Launch all your games" msgstr "Lance todos sus juegos" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "gaming;launcher;steam;lutris;heroic;bottles;itch;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -65,7 +67,7 @@ msgid "Game Details" msgstr "Detalles del juego" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "Preferencias" @@ -148,7 +150,7 @@ msgstr "Mostrar preferencias" msgid "Shortcuts" msgstr "Atajos" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "Deshacer" @@ -176,7 +178,7 @@ msgstr "Mostrar juegos ocultos" msgid "Remove game" msgstr "Eliminar juego" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Comportamiento" @@ -226,9 +228,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Ruta de instalación" @@ -261,54 +263,60 @@ msgid "Import GOG Games" msgstr "Importar juegos de GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Importar juegos de Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Importar juegos descargados" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "Legendario" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Importar lanzadores de juegos" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Autenticación" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "Clave API" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Usar SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Descargar las imágenes al añadir o importar juegos" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Preferir las imágenes oficiales" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Prefiero las imágenes animadas" @@ -397,7 +405,7 @@ msgid "About Cartridges" msgstr "Acerca de Cartuchos" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "Óscar Fernández Díaz " @@ -473,15 +481,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "No se puede añadir el juego" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "El título del juego no puede estar vacío." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "El ejecutable no puede estar vacío." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "No se pudieron aplicar las preferencias" @@ -503,45 +511,45 @@ msgstr "{} visible" msgid "{} removed" msgstr "{} eliminado" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Todos los juegos eliminados" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Se necesita una clave API para utilizar SteamGridDB. Puedes generar una {}" "aquí{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Instalación no encontrada" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Selecciona un directorio válido." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Directorio incorrecto" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Seleccione el directorio de la caché {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Seleccione el directorio de configuración {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Seleccione el directorio de datos {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Escoger la ubicación" diff --git a/po/fa.po b/po/fa.po index da0494e..3ee2a79 100644 --- a/po/fa.po +++ b/po/fa.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-04-22 10:48+0000\n" "Last-Translator: سید حسین موسوی فرد \n" "Language-Team: Persian \n" "Language-Team: Finnish \n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" +"PO-Revision-Date: 2023-07-24 13:05+0000\n" +"Last-Translator: rene-coty \n" "Language-Team: French \n" "Language: fr\n" @@ -25,9 +25,9 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" -msgstr "Cartridges" +msgstr "Cartouches" #: data/hu.kramo.Cartridges.desktop.in:4 msgid "Game Launcher" @@ -39,7 +39,9 @@ msgid "Launch all your games" msgstr "Lancez tous vos jeux" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "gaming;jeux;lanceur;steam;lutris;heroic;bouteilles;itch;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -49,7 +51,7 @@ msgid "" "necessary. You can sort and hide games or download cover art from " "SteamGridDB." msgstr "" -"Cartridges est un lanceur de jeux simple pour tous vos jeux. Il prend en " +"Cartouches est un lanceur de jeux simple pour tous vos jeux. Il prend en " "charge l’importation des jeux depuis Steam, Lutris, Heroic et d’autres " "encore, sans nécessiter de connexion. Vous pouvez trier et masquer les jeux " "ou télécharger la pochette depuis SteamGridDB." @@ -67,7 +69,7 @@ msgid "Game Details" msgstr "Détails du jeu" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "Préférences" @@ -150,7 +152,7 @@ msgstr "Afficher les préférences" msgid "Shortcuts" msgstr "Raccourcis" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "Annuler" @@ -178,7 +180,7 @@ msgstr "Afficher les jeux masqués" msgid "Remove game" msgstr "Supprimer le jeu" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Comportement" @@ -230,9 +232,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Emplacement d'installation" @@ -265,54 +267,60 @@ msgid "Import GOG Games" msgstr "Importer les jeux de GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Importer les jeux de Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Importer des jeux Sideloaded" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "Légendaire" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Importer des lanceurs de jeux" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Authentification" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "Clé API" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Utiliser SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Télécharger les images lors de l’ajout ou de l’importation de jeux" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Préférer à la place des images officielles" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Préférer les images animées" @@ -398,10 +406,10 @@ msgstr "Raccourcis clavier" #: data/gtk/window.blp:426 msgid "About Cartridges" -msgstr "À propos de Cartridges" +msgstr "À propos de Cartouches" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "Irénée Thirion" @@ -479,15 +487,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Impossible d’ajouter le jeu" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Le titre du jeu ne peut pas être vide." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "L’exécutable ne peut pas être vide." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Impossible d’appliquer les préférences" @@ -509,45 +517,45 @@ msgstr "{} affiché" msgid "{} removed" msgstr "{} retiré" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Tous les jeux ont été supprimés" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Une clé API est requise pour utiliser SteamGridDB. Vous pouvez en générer " "une {}ici{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Installation introuvable" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Sélectionnez un répertoire valide." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Répertoire invalide" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Sélectionnez le répertoire de cache de {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Sélectionnez le répertoire de configuration de {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Sélectionnez le répertoire de données de {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Définir l’emplacement" diff --git a/po/hu.po b/po/hu.po index ce46c6a..33cda13 100644 --- a/po/hu.po +++ b/po/hu.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-05 13:13+0000\n" "Last-Translator: kramo \n" "Language-Team: Hungarian , 2023. # albanobattistella , 2023. # kramo , 2023. +# Giasko , 2023. msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" -"PO-Revision-Date: 2023-07-08 14:52+0000\n" -"Last-Translator: Alessandro Iepure \n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" +"PO-Revision-Date: 2023-07-21 12:16+0000\n" +"Last-Translator: Giasko \n" "Language-Team: Italian \n" "Language: it\n" @@ -22,7 +23,7 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" msgstr "Cartucce" @@ -36,7 +37,9 @@ msgid "Launch all your games" msgstr "Avvia tutti i tuoi giochi" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "gioco;launcher;steam;lutris;heroic;bottles;itch;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -64,7 +67,7 @@ msgid "Game Details" msgstr "Dettagli del gioco" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "Preferenze" @@ -132,7 +135,7 @@ msgstr "Generale" #: data/gtk/help-overlay.blp:14 msgid "Quit" -msgstr "Chiudi" +msgstr "Esci" #: data/gtk/help-overlay.blp:19 data/gtk/window.blp:217 data/gtk/window.blp:257 #: data/gtk/window.blp:323 @@ -147,7 +150,7 @@ msgstr "Mostra preferenze" msgid "Shortcuts" msgstr "Scorciatoie da tastiera" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "Annulla" @@ -175,7 +178,7 @@ msgstr "Mostra giochi nascosti" msgid "Remove game" msgstr "Rimuovi gioco" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Comportamento" @@ -225,9 +228,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Posizione di installazione" @@ -260,54 +263,60 @@ msgid "Import GOG Games" msgstr "Importa giochi da GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Importa giochi da Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Importa giochi da aggiunti manualmente" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" -msgstr "Leggendario" +msgstr "Legendary" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Importa launcher di giochi" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Autenticazione" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "Chiave API" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Usa SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Scarica immagini durante l'aggiunta o l'import di giochi" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Preferisci alle immagini ufficiali" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Preferisci immagini animate" @@ -396,7 +405,7 @@ msgid "About Cartridges" msgstr "Informazioni su Cartucce" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "Alessandro Iepure https://ale.iepure.me" @@ -472,15 +481,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Impossibile aggiungere il gioco" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Il titolo del gioco non può essere vuoto." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "L'eseguibile non può essere vuoto." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Impossibile applicare le preferenze" @@ -502,45 +511,45 @@ msgstr "{} visibile" msgid "{} removed" msgstr "{} rimosso" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Tutti i giochi sono stati rimossi" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Per utilizzare SteamGridDB è necessaria una chiave API. Puoi generarne una {}" "qui{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Installazione non trovata" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Seleziona una directory valida." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Directory non valida" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Seleziona la directory della cache per {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Selezionare la directory di configurazione per {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Seleziona la directory dati per {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Imposta percorso" diff --git a/po/ko.po b/po/ko.po index a6c170e..f97e553 100644 --- a/po/ko.po +++ b/po/ko.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-03-28 22:23+0000\n" "Last-Translator: MJKim \n" "Language-Team: Korean \n" "Language-Team: Norwegian Bokmål " @@ -491,15 +497,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Kunne ikke legge til spill" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Spillnavnet kan ikke være tomt." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Kjørbar fil må angis." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Kunne ikke ta i bruk endringer" @@ -523,54 +529,54 @@ msgstr "{} synlig" msgid "{} removed" msgstr "{} fjernet" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Alle spill fjernet" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "En API-nøkkel kreves for å bruke SteamGridDB. Du kan generere en {}her{}." -#: src/preferences.py:284 +#: src/preferences.py:285 #, fuzzy #| msgid "Installation Not Found" msgid "Installation Not Found" msgstr "Fant ikke installasjonen" -#: src/preferences.py:286 +#: src/preferences.py:287 #, fuzzy #| msgid "Select the {} data directory." msgid "Select a valid directory." msgstr "Velg {}-datamappen." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 #, fuzzy #| msgid "Select the {} data directory." msgid "Select the {} cache directory." msgstr "Velg {}-datamappen." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 #, fuzzy #| msgid "Select the {} configuration directory." msgid "Select the {} configuration directory." msgstr "Velg {}-oppsettsmappen." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 #, fuzzy #| msgid "Select the {} data directory." msgid "Select the {} data directory." msgstr "Velg {}-datamappen." -#: src/preferences.py:364 +#: src/preferences.py:365 #, fuzzy #| msgid "Set Steam Location" msgid "Set Location" diff --git a/po/nl.po b/po/nl.po index b85ed43..3d8b8f6 100644 --- a/po/nl.po +++ b/po/nl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-08 14:52+0000\n" "Last-Translator: Philip Goto \n" "Language-Team: Dutch , 2023. # Kshyso , 2023. # Eryk Michalak , 2023. +# Michaks , 2023. msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" -"PO-Revision-Date: 2023-07-14 15:51+0000\n" -"Last-Translator: Eryk Michalak \n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" +"PO-Revision-Date: 2023-07-24 13:05+0000\n" +"Last-Translator: Michaks \n" "Language-Team: Polish \n" "Language: pl\n" @@ -23,9 +24,9 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" -msgstr "Cartridges" +msgstr "Kartridże" #: data/hu.kramo.Cartridges.desktop.in:4 msgid "Game Launcher" @@ -37,7 +38,9 @@ msgid "Launch all your games" msgstr "Uruchom wszystkie swoje gry" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "gry;gaming;launcher;steam;lutris;heroic;bottles;itch;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -57,16 +60,16 @@ msgstr "Biblioteka" #: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67 msgid "Edit Game Details" -msgstr "Edytuj detale gry" +msgstr "Edycja szczegółów gry" #: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71 msgid "Game Details" -msgstr "Detale gry" +msgstr "Szczegóły gry" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" -msgstr "Ustawienia" +msgstr "Preferencje" #: data/gtk/details-window.blp:25 msgid "Cancel" @@ -74,11 +77,11 @@ msgstr "Anuluj" #: data/gtk/details-window.blp:57 msgid "New Cover" -msgstr "Nowa Okładka" +msgstr "Nowa okładka" #: data/gtk/details-window.blp:75 msgid "Delete Cover" -msgstr "Usuń Okładkę" +msgstr "Usuń osłonę" #: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106 #: data/gtk/game.blp:80 @@ -87,7 +90,7 @@ msgstr "Tytuł" #: data/gtk/details-window.blp:102 msgid "The title of the game" -msgstr "Tytuł gry" +msgstr "Tytuł Gry" #: data/gtk/details-window.blp:112 data/gtk/details-window.blp:117 msgid "Developer" @@ -148,9 +151,9 @@ msgstr "Pokaż preferencje" msgid "Shortcuts" msgstr "Skróty" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" -msgstr "Cofnij" +msgstr "Wróć" #: data/gtk/help-overlay.blp:39 msgid "Open menu" @@ -162,7 +165,7 @@ msgstr "Gry" #: data/gtk/help-overlay.blp:48 msgid "Add new game" -msgstr "Dodaj nową grę" +msgstr "Dodaj nową gre" #: data/gtk/help-overlay.blp:53 msgid "Import games" @@ -176,7 +179,7 @@ msgstr "Pokaż ukryte gry" msgid "Remove game" msgstr "Usuń grę" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Zachowanie" @@ -186,7 +189,7 @@ msgstr "Wyjdź po uruchomieniu gry" #: data/gtk/preferences.blp:25 msgid "Cover Image Launches Game" -msgstr "Obraz okładki startera gier" +msgstr "Obraz okładki uruchamia grę" #: data/gtk/preferences.blp:26 msgid "Swaps the behavior of the cover image and the play button" @@ -225,9 +228,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Lokalizacja instalacji" @@ -260,54 +263,60 @@ msgid "Import GOG Games" msgstr "Importuj gry z GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Importuj gry Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Importuj gry w wersji Sideloaded" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Butelki" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "Legendarne" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Importuj programy uruchamiające gry" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Uwierzytelnianie" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "Klucz API" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Użyj SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Pobieranie obrazów podczas dodawania lub importowania gier" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Preferuj ponad Oficjalne zdjęcia" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Preferuj animowane obrazy" @@ -337,7 +346,7 @@ msgstr "Gry, które ukryjesz, pojawią się tutaj." #: data/gtk/window.blp:64 data/gtk/window.blp:304 msgid "Back" -msgstr "Cofnij" +msgstr "Powrót" #: data/gtk/window.blp:121 msgid "Game Title" @@ -345,7 +354,7 @@ msgstr "Tytuł gry" #: data/gtk/window.blp:176 msgid "Play" -msgstr "Uruchom" +msgstr "Graj" #: data/gtk/window.blp:243 data/gtk/window.blp:435 msgid "Add Game" @@ -393,10 +402,10 @@ msgstr "Skróty klawiaturowe" #: data/gtk/window.blp:426 msgid "About Cartridges" -msgstr "O Cartridges" +msgstr "O Kartridżach" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "kredyty tłumacza" @@ -420,7 +429,7 @@ msgstr "Zastosuj" #: src/details_window.py:78 msgid "Add New Game" -msgstr "Dodaj nową grę" +msgstr "Dodaj nową Grę" #: src/details_window.py:79 msgid "Confirm" @@ -472,15 +481,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Nie można było dodać gry" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Tytuł gry nie może być pusty." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Plik wykonywalny nie może być pusty." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Nie można zastosować preferencji" @@ -502,47 +511,47 @@ msgstr "{} nieukryty" msgid "{} removed" msgstr "{} usunięty" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Wszystkie gry usunięte" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Do korzystania z SteamGridDB wymagany jest klucz API. Możesz go wygenerować " "{} tutaj{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Nie znaleziono instalacji" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Wybierz prawidłowy katalog." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Nieprawidłowy katalog" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Wybierz katalog pamięci podręcznej {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Wybierz katalog konfiguracyjny {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Wybierz katalog z danymi {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" -msgstr "Ustaw lokacje" +msgstr "Ustaw położenie" #: src/utils/create_dialog.py:25 msgid "Dismiss" diff --git a/po/pt.po b/po/pt.po index 8cd5d77..35e7cfb 100644 --- a/po/pt.po +++ b/po/pt.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-06-04 22:47+0000\n" "Last-Translator: João Alves \n" "Language-Team: Portuguese \n" "Language-Team: Portuguese (Brazil) \n" "Language-Team: Romanian \n" "Language-Team: Russian =2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\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:162 +#: src/main.py:170 msgid "Cartridges" msgstr "Картриджи" @@ -36,7 +36,9 @@ msgid "Launch all your games" msgstr "Запустите все свои игры" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "gaming;launcher;steam;lutris;heroic;bottles;itch;игры;стим;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -64,7 +66,7 @@ msgid "Game Details" msgstr "Подробности об игре" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "Параметры" @@ -147,7 +149,7 @@ msgstr "Показать параметры" msgid "Shortcuts" msgstr "Комбинации клавиш" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "Вернуть" @@ -175,7 +177,7 @@ msgstr "Показать скрытые игры" msgid "Remove game" msgstr "Удалить игру" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Поведение" @@ -224,9 +226,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Место установки" @@ -259,54 +261,60 @@ msgid "Import GOG Games" msgstr "Импорт игр GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Импорт игр Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Импорт сторонних игр" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "Legendary" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Импорт средств запуска игр" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Аутентификация" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "API-ключ" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Использовать SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Загрузка изображений при добавлении или импорте игр" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Отдавать предпочтение официальным изображениям" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Отдавать предпочтение анимированным изображениям" @@ -395,7 +403,7 @@ msgid "About Cartridges" msgstr "О приложении" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "Ser82-png" @@ -471,15 +479,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Не удалось добавить игру" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Название игры не может быть пустым." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Исполняемый файл не может быть пустым." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Не удалось применить параметры" @@ -501,45 +509,45 @@ msgstr "{} - не скрыта" msgid "{} removed" msgstr "{} удалена" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Все игры удалены" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Для использования SteamGridDB требуется ключ API. Вы можете сгенерировать " "его {}здесь{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Установка не найдена" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Выберите действующий каталог." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Неверный каталог" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Выберите каталог кэша {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Выберите каталог конфигурации {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Выберите каталог данных {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Установить расположение" diff --git a/po/sv.po b/po/sv.po index d6f1122..1adaa49 100644 --- a/po/sv.po +++ b/po/sv.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-08 14:52+0000\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish \n" "Language-Team: Tamil " @@ -473,15 +480,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "விளையாட்டைச் சேர்க்க முடியவில்லை" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "விளையாட்டு தலைப்பு காலியாக இருக்கக்கூடாது." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "இயங்கக்கூடியது காலியாக இருக்க முடியாது." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "விருப்பங்களைப் பயன்படுத்த முடியவில்லை" @@ -503,45 +510,43 @@ msgstr "{} மறைக்கப்படாதது" msgid "{} removed" msgstr "{} அகற்றப்பட்டது" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "அனைத்து விளையாட்டுகளும் அகற்றப்பட்டன" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." -msgstr "" -"SteamGridDB ஐப் பயன்படுத்த API விசை தேவை. நீங்கள் ஒன்றை {}இங்கே{} " -"உருவாக்கலாம்." +msgstr "SteamGridDB ஐப் பயன்படுத்த API விசை தேவை. நீங்கள் ஒன்றை {}இங்கே{} உருவாக்கலாம்." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "நிறுவல் கிடைக்கவில்லை" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "சரியான கோப்பகத்தைத் தேர்ந்தெடுக்கவும்." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "தவறான கோப்பகம்" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "{} கேச் கோப்பகத்தைத் தேர்ந்தெடுக்கவும்." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "{} கட்டமைப்பு கோப்பகத்தைத் தேர்ந்தெடுக்கவும்." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "{} தரவு கோப்பகத்தைத் தேர்ந்தெடுக்கவும்." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "இருப்பிடத்தை அமைக்கவும்" diff --git a/po/tr.po b/po/tr.po index e7acdd1..121fd78 100644 --- a/po/tr.po +++ b/po/tr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-15 22:51+0000\n" "Last-Translator: Sabri Ünal \n" "Language-Team: Turkish " @@ -471,15 +479,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Oyun Eklenemedi" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Oyun başlığı boş olamaz." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Çalıştırılabilir boş olamaz." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Tercihler Uygulanamadı" @@ -501,45 +509,45 @@ msgstr "{} görünür" msgid "{} removed" msgstr "{} kaldırıldı" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Tüm oyunlar kaldırıldı" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "SteamGridDBʼyi kullanmak için API anahtarı gereklidir. {}Buradan{} bir tane " "oluşturabilirsiniz." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Kurulum Bulunamadı" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Geçerli bir dizin seçin." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Geçersiz Dizin" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "{} önbellek dizinini seç." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "{} yapılandırma dizinini seç." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "{} veri dizinini seç." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Konum Ayarla" diff --git a/po/uk.po b/po/uk.po index 2ac3ec9..034db8d 100644 --- a/po/uk.po +++ b/po/uk.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-08 14:52+0000\n" "Last-Translator: Dan \n" "Language-Team: Ukrainian =2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\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:162 +#: src/main.py:170 msgid "Cartridges" msgstr "Картриджі" @@ -38,7 +38,9 @@ msgid "Launch all your games" msgstr "Запустіть усі свої ігри" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "ігри;лаунчер;steam;lutris;heroic;bottles;itch;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -66,7 +68,7 @@ msgid "Game Details" msgstr "Подробиці гри" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "Параметри" @@ -149,7 +151,7 @@ msgstr "Показати параметри" msgid "Shortcuts" msgstr "Ярлики" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "Відмінити" @@ -177,7 +179,7 @@ msgstr "Показати приховані ігри" msgid "Remove game" msgstr "Видалити гру" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Поведінка" @@ -226,9 +228,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Місце встановлення" @@ -261,54 +263,60 @@ msgid "Import GOG Games" msgstr "Імпорт ігор GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Імпорт ігор Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Імпорт сторонніх ігор" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "Легендарний" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Імпортувати ігрові лаунчери" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Аутентифікація" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "Ключ API" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Використовувати SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Завантаження зображень під час додавання або імпорту ігор" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Надавати перевагу офіційним зображенням" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Надавати перевагу анімованим зображенням" @@ -397,7 +405,7 @@ msgid "About Cartridges" msgstr "Про Картриджі" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "kefir2105" @@ -474,15 +482,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Не вдалося додати гру" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Назва гри не може бути порожньою." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Виконуваний файл не може бути порожнім." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Не вдалося застосувати параметри" @@ -504,45 +512,45 @@ msgstr "{} показано" msgid "{} removed" msgstr "{} видалено" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Всі ігри видалено" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Для використання SteamGridDB потрібен ключ API. Ви можете згенерувати його {}" "тут{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Встановлення не знайдено" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Виберіть правильний каталог." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Неправильний каталог" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Виберіть каталог кешу {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Виберіть каталог конфігурації {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Виберіть каталог даних {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Встановити місцезнаходження" diff --git a/src/importer/importer.py b/src/importer/importer.py index 8b30c6b..756ea3b 100644 --- a/src/importer/importer.py +++ b/src/importer/importer.py @@ -105,7 +105,7 @@ class Importer(ErrorProducer): manager.reset_cancellable() for source in self.sources: - logging.debug("Importing games from source %s", source.id) + logging.debug("Importing games from source %s", source.source_id) task = Task.new(None, None, self.source_callback, (source,)) self.n_source_tasks_created += 1 task.set_task_data((source,)) @@ -138,16 +138,16 @@ class Importer(ErrorProducer): # Early exit if not available or not installed if not source.is_available: - logging.info("Source %s skipped, not available", source.id) + logging.info("Source %s skipped, not available", source.source_id) return try: iterator = iter(source) except UnresolvableLocationError: - logging.info("Source %s skipped, bad location", source.id) + logging.info("Source %s skipped, bad location", source.source_id) return # Get games from source - logging.info("Scanning source %s", source.id) + logging.info("Scanning source %s", source.source_id) while True: # Handle exceptions raised when iterating try: @@ -155,7 +155,7 @@ class Importer(ErrorProducer): except StopIteration: break except Exception as error: # pylint: disable=broad-exception-caught - logging.exception("%s in %s", type(error).__name__, source.id) + logging.exception("%s in %s", type(error).__name__, source.source_id) self.report_error(error) continue @@ -172,7 +172,7 @@ class Importer(ErrorProducer): # Should not happen on production code logging.warning( "%s produced an invalid iteration return type %s", - source.id, + source.source_id, type(iteration_result), ) continue @@ -194,7 +194,7 @@ class Importer(ErrorProducer): def source_callback(self, _obj, _result, data): """Callback executed when a source is fully scanned""" source, *_rest = data - logging.debug("Import done for source %s", source.id) + logging.debug("Import done for source %s", source.source_id) self.n_source_tasks_done += 1 self.progress_changed_callback() diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index d993598..041247b 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -20,33 +20,30 @@ from pathlib import Path from time import time +from typing import NamedTuple import yaml from src import shared from src.game import Game -from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.location import Location, LocationSubPath +from src.importer.sources.source import SourceIterable, URLExecutableSource -class BottlesSourceIterator(SourceIterator): +class BottlesSourceIterable(SourceIterable): source: "BottlesSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" - data = self.source.data_location["library.yml"].read_text("utf-8") + data = self.source.locations.data["library.yml"].read_text("utf-8") library: dict = yaml.safe_load(data) added_time = int(time()) for entry in library.values(): # Build game values = { - "source": self.source.id, + "source": self.source.source_id, "added": added_time, "name": entry["name"], "game_id": self.source.game_id_format.format(game_id=entry["id"]), @@ -62,11 +59,11 @@ class BottlesSourceIterator(SourceIterator): # as Cartridges can't access directories picked via Bottles' file picker portal bottles_location = Path( yaml.safe_load( - self.source.data_location["data.yml"].read_text("utf-8") + self.source.locations.data["data.yml"].read_text("utf-8") )["custom_bottles_path"] ) except (FileNotFoundError, KeyError): - bottles_location = self.source.data_location.root / "bottles" + bottles_location = self.source.locations.data.root / "bottles" bottle_path = entry["bottle"]["path"] @@ -80,23 +77,31 @@ class BottlesSourceIterator(SourceIterator): yield (game, additional_data) +class BottlesLocations(NamedTuple): + data: Location + + class BottlesSource(URLExecutableSource): """Generic Bottles source""" + source_id = "bottles" name = _("Bottles") - iterator_class = BottlesSourceIterator + iterable_class = BottlesSourceIterable url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' available_on = {"linux"} - data_location = Location( - schema_key="bottles-location", - candidates=( - shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles", - shared.data_dir / "bottles/", - shared.home / ".local" / "share" / "bottles", - ), - paths={ - "library.yml": (False, "library.yml"), - "data.yml": (False, "data.yml"), - }, + locations = BottlesLocations( + Location( + schema_key="bottles-location", + candidates=( + shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles", + shared.data_dir / "bottles/", + shared.home / ".local" / "share" / "bottles", + ), + paths={ + "library.yml": LocationSubPath("library.yml"), + "data.yml": LocationSubPath("data.yml"), + }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + ) ) diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index 6ec6262..ee4ebbe 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -19,25 +19,26 @@ from pathlib import Path from time import time +from typing import NamedTuple from gi.repository import GLib, Gtk from src import shared from src.game import Game -from src.importer.sources.location import Location -from src.importer.sources.source import Source, SourceIterationResult, SourceIterator +from src.importer.sources.location import Location, LocationSubPath +from src.importer.sources.source import Source, SourceIterable -class FlatpakSourceIterator(SourceIterator): +class FlatpakSourceIterable(SourceIterable): source: "FlatpakSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" added_time = int(time()) icon_theme = Gtk.IconTheme.new() - icon_theme.add_search_path(str(self.source.data_location["icons"])) + icon_theme.add_search_path(str(self.source.locations.data["icons"])) blacklist = ( {"hu.kramo.Cartridges", "hu.kramo.Cartridges.Devel"} @@ -53,7 +54,7 @@ class FlatpakSourceIterator(SourceIterator): } ) - for entry in (self.source.data_location["applications"]).iterdir(): + for entry in (self.source.locations.data["applications"]).iterdir(): if entry.suffix != ".desktop": continue @@ -76,7 +77,7 @@ class FlatpakSourceIterator(SourceIterator): continue values = { - "source": self.source.id, + "source": self.source.source_id, "added": added_time, "name": name, "game_id": self.source.game_id_format.format(game_id=flatpak_id), @@ -111,22 +112,30 @@ class FlatpakSourceIterator(SourceIterator): yield (game, additional_data) +class FlatpakLocations(NamedTuple): + data: Location + + class FlatpakSource(Source): """Generic Flatpak source""" + source_id = "flatpak" name = _("Flatpak") - iterator_class = FlatpakSourceIterator + iterable_class = FlatpakSourceIterable executable_format = "flatpak run {flatpak_id}" available_on = {"linux"} - data_location = Location( - schema_key="flatpak-location", - candidates=( - "/var/lib/flatpak/", - shared.data_dir / "flatpak", - ), - paths={ - "applications": (True, "exports/share/applications"), - "icons": (True, "exports/share/icons"), - }, + locations = FlatpakLocations( + Location( + schema_key="flatpak-location", + candidates=( + "/var/lib/flatpak/", + shared.data_dir / "flatpak", + ), + paths={ + "applications": LocationSubPath("exports/share/applications", True), + "icons": LocationSubPath("exports/share/icons", True), + }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + ) ) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 499803b..c06a266 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -20,21 +20,47 @@ import json import logging +from abc import abstractmethod from hashlib import sha256 from json import JSONDecodeError +from pathlib import Path from time import time -from typing import Optional, TypedDict +from typing import Iterable, NamedTuple, Optional, TypedDict +from functools import cached_property from src import shared from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, LocationSubPath from src.importer.sources.source import ( - URLExecutableSource, + SourceIterable, SourceIterationResult, - SourceIterator, + URLExecutableSource, ) +def path_json_load(path: Path): + """ + Load JSON from the file at the given path + + :raises OSError: if the file can't be opened + :raises JSONDecodeError: if the file isn't valid JSON + """ + with path.open("r", encoding="utf-8") as open_file: + return json.load(open_file) + + +class InvalidLibraryFileError(Exception): + pass + + +class InvalidInstalledFileError(Exception): + pass + + +class InvalidStoreFileError(Exception): + pass + + class HeroicLibraryEntry(TypedDict): app_name: str installed: Optional[bool] @@ -44,119 +70,320 @@ class HeroicLibraryEntry(TypedDict): art_square: str -class HeroicSubSource(TypedDict): - service: str - path: tuple[str] +class SubSourceIterable(Iterable): + """Class representing a Heroic sub-source""" - -class HeroicSourceIterator(SourceIterator): source: "HeroicSource" + source_iterable: "HeroicSourceIterable" + name: str + service: str + image_uri_params: str = "" + relative_library_path: Path + library_json_entries_key: str = "library" - sub_sources: dict[str, HeroicSubSource] = { - "sideload": { - "service": "sideload", - "path": ("sideload_apps", "library.json"), - }, - "legendary": { - "service": "epic", - "path": ("store_cache", "legendary_library.json"), - }, - "gog": { - "service": "gog", - "path": ("store_cache", "gog_library.json"), - }, - } + def __init__(self, source, source_iterable) -> None: + self.source = source + self.source_iterable = source_iterable - def game_from_library_entry( + @cached_property + def library_path(self) -> Path: + path = self.source.locations.config.root / self.relative_library_path + logging.debug("Using Heroic %s library.json path %s", self.name, path) + return path + + def process_library_entry( self, entry: HeroicLibraryEntry, added_time: int ) -> SourceIterationResult: - """Helper method used to build a Game from a Heroic library entry""" + """Build a Game from a Heroic library entry""" - # Skip games that are not installed - if not entry["is_installed"]: - return None - - # Build game app_name = entry["app_name"] runner = entry["runner"] - service = self.sub_sources[runner]["service"] + + # Build game values = { - "source": f"{self.source.id}_{service}", + "source": f"{self.source.source_id}_{self.service}", "added": added_time, "name": entry["title"], "developer": entry.get("developer", None), "game_id": self.source.game_id_format.format( - service=service, game_id=app_name + service=self.service, game_id=app_name ), - "executable": self.source.executable_format.format(app_name=app_name), + "executable": self.source.executable_format.format(runner=runner, app_name=app_name), + "hidden": self.source_iterable.is_hidden(app_name), } game = Game(values) # 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"] - if service == "epic": - uri += "?h=400&resize=1&w=300" + uri: str = entry["art_square"] + self.image_uri_params digest = sha256(uri.encode()).hexdigest() - image_path = self.source.config_location.root / "images-cache" / digest + image_path = self.source.locations.config.root / "images-cache" / digest additional_data = {"local_image_path": image_path} return (game, additional_data) - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): + """ + Iterate through the games with a generator + :raises InvalidLibraryFileError: on initial call if the library file is bad + """ + added_time = int(time()) + try: + iterator = iter( + path_json_load(self.library_path)[self.library_json_entries_key] + ) + except (OSError, JSONDecodeError, TypeError, KeyError) as error: + raise InvalidLibraryFileError( + f"Invalid {self.library_path.name}" + ) from error + for entry in iterator: + try: + yield self.process_library_entry(entry, added_time) + except KeyError as error: + logging.warning( + "Skipped invalid %s game %s", + self.name, + entry.get("app_name", "UNKNOWN"), + exc_info=error, + ) + continue + + +class StoreSubSourceIterable(SubSourceIterable): + """ + Class representing a "store" sub source. + Games can be installed or not, this class does the check accordingly. + """ + + relative_installed_path: Path + installed_app_names: set[str] + + @cached_property + def installed_path(self) -> Path: + path = self.source.locations.config.root / self.relative_installed_path + logging.debug("Using Heroic %s installed.json path %s", self.name, path) + return path + + @abstractmethod + def get_installed_app_names(self) -> set[str]: + """ + Get the sub source's installed app names as a set. + + :raises InvalidInstalledFileError: if the installed file data cannot be read + Whenever possible, `__cause__` is set with the original exception + """ + + def is_installed(self, app_name: str) -> bool: + return app_name in self.installed_app_names + + def process_library_entry(self, entry, added_time): + # Skip games that are not installed + app_name = entry["app_name"] + if not self.is_installed(app_name): + logging.warning( + "Skipped %s game %s (%s): not installed", + self.service, + entry["title"], + app_name, + ) + return None + # Process entry as normal + return super().process_library_entry(entry, added_time) + + def __iter__(self): + """ + Iterate through the installed games with a generator + :raises InvalidLibraryFileError: on initial call if the library file is bad + :raises InvalidInstalledFileError: on initial call if the installed file is bad + """ + self.installed_app_names = self.get_installed_app_names() + yield from super().__iter__() + + +class SideloadIterable(SubSourceIterable): + name = "sideload" + service = "sideload" + relative_library_path = Path("sideload_apps") / "library.json" + library_json_entries_key = "games" + + +class LegendaryIterable(StoreSubSourceIterable): + name = "legendary" + service = "epic" + image_uri_params = "?h=400&resize=1&w=300" + relative_library_path = Path("store_cache") / "legendary_library.json" + + # relative_installed_path = ( + # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" + # ) + + @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. + """ + + heroic_config_path = self.source.locations.config.root + # Heroic >= 2.9 + if (path := heroic_config_path / "legendaryConfig").is_dir(): + logging.debug("Using Heroic >= 2.9 legendary file") + # Heroic <= 2.8 + elif heroic_config_path.is_relative_to(shared.flatpak_dir): + # Heroic flatpak + path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + logging.debug("Using Heroic flatpak <= 2.8 legendary file") + else: + # Heroic native + logging.debug("Using Heroic native <= 2.8 legendary file") + path = Path.home() / ".config" + + path = path / "legendary" / "installed.json" + logging.debug("Using Heroic %s installed.json path %s", self.name, path) + return path + + def get_installed_app_names(self): + try: + return set(path_json_load(self.installed_path).keys()) + except (OSError, JSONDecodeError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class GogIterable(StoreSubSourceIterable): + name = "gog" + service = "gog" + library_json_entries_key = "games" + relative_library_path = Path("store_cache") / "gog_library.json" + relative_installed_path = Path("gog_store") / "installed.json" + + def get_installed_app_names(self): + try: + return { + app_name + for entry in path_json_load(self.installed_path)["installed"] + if (app_name := entry.get("appName")) is not None + } + except (OSError, JSONDecodeError, KeyError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class NileIterable(StoreSubSourceIterable): + name = "nile" + service = "amazon" + relative_library_path = Path("store_cache") / "nile_library.json" + relative_installed_path = Path("nile_config") / "nile" / "installed.json" + + def get_installed_app_names(self): + try: + installed_json = path_json_load(self.installed_path) + return { + app_name + for entry in installed_json + if (app_name := entry.get("id")) is not None + } + except (OSError, JSONDecodeError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class HeroicSourceIterable(SourceIterable): + source: "HeroicSource" + + hidden_app_names: set[str] = set() + + def is_hidden(self, app_name: str) -> bool: + return app_name in self.hidden_app_names + + def get_hidden_app_names(self) -> set[str]: + """Get the hidden app names from store/config.json + + :raises InvalidStoreFileError: if the store is invalid for some reason + """ + + try: + store = path_json_load(self.source.locations.config["store_config.json"]) + self.hidden_app_names = { + app_name + for game in store["games"]["hidden"] + if (app_name := game.get("appName")) is not None + } + except KeyError: + logging.warning('No ["games"]["hidden"] key in Heroic store file') + except (OSError, JSONDecodeError, TypeError) as error: + logging.error("Invalid Heroic store file", exc_info=error) + raise InvalidStoreFileError() from error + + def __iter__(self): """Generator method producing games from all the Heroic sub-sources""" - for sub_source_name, sub_source in self.sub_sources.items(): - # Skip disabled sub-sources - if not shared.schema.get_boolean("heroic-import-" + sub_source["service"]): + self.get_hidden_app_names() + + # Get games from the sub sources + for sub_source_class in ( + SideloadIterable, + LegendaryIterable, + GogIterable, + NileIterable, + ): + sub_source = sub_source_class(self.source, self) + + if not shared.schema.get_boolean("heroic-import-" + sub_source.service): + logging.debug("Skipping Heroic %s: disabled", sub_source.service) continue - # Load games from JSON - file = self.source.config_location.root.joinpath(*sub_source["path"]) try: - contents = json.load(file.open()) - key = "library" if sub_source_name == "legendary" else "games" - library = contents[key] - except (JSONDecodeError, OSError, KeyError): - # Invalid library.json file, skip it - logging.warning("Couldn't open Heroic file: %s", str(file)) + sub_source_iterable = iter(sub_source) + yield from sub_source_iterable + except (InvalidLibraryFileError, InvalidInstalledFileError) as error: + logging.error( + "Skipping bad Heroic sub-source %s", + sub_source.service, + exc_info=error, + ) continue - added_time = int(time()) - for entry in library: - try: - result = self.game_from_library_entry(entry, added_time) - except KeyError as error: - # Skip invalid games - logging.warning( - "Invalid Heroic game skipped in %s", str(file), exc_info=error - ) - continue - yield result +class HeroicLocations(NamedTuple): + config: Location class HeroicSource(URLExecutableSource): """Generic Heroic Games Launcher source""" + source_id = "heroic" name = _("Heroic") - iterator_class = HeroicSourceIterator - url_format = "heroic://launch/{app_name}" + iterable_class = HeroicSourceIterable + url_format = "heroic://launch/{runner}/{app_name}" available_on = {"linux", "win32"} - config_location = Location( - schema_key="heroic-location", - candidates=( - shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic", - shared.config_dir / "heroic", - shared.home / ".config" / "heroic", - shared.appdata_dir / "heroic", - ), - paths={ - "config.json": (False, "config.json"), - }, + locations = HeroicLocations( + Location( + schema_key="heroic-location", + candidates=( + shared.config_dir / "heroic", + shared.home / ".config" / "heroic", + shared.flatpak_dir + / "com.heroicgameslauncher.hgl" + / "config" + / "heroic", + shared.appdata_dir / "heroic", + ), + paths={ + "config.json": LocationSubPath("config.json"), + "store_config.json": LocationSubPath("store/config.json"), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) ) @property def game_id_format(self) -> str: """The string format used to construct game IDs""" - return self.id + "_{service}_{game_id}" + return self.source_id + "_{service}_{game_id}" diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 141c9f9..36e02e0 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -21,22 +21,19 @@ from shutil import rmtree from sqlite3 import connect from time import time +from typing import NamedTuple from src import shared from src.game import Game -from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.location import Location, LocationSubPath +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.sqlite import copy_db -class ItchSourceIterator(SourceIterator): +class ItchSourceIterable(SourceIterable): source: "ItchSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" # Query the database @@ -55,7 +52,7 @@ class ItchSourceIterator(SourceIterator): caves.game_id = games.id ; """ - db_path = copy_db(self.source.config_location["butler.db"]) + db_path = copy_db(self.source.locations.config["butler.db"]) connection = connect(db_path) cursor = connection.execute(db_request) @@ -65,7 +62,7 @@ class ItchSourceIterator(SourceIterator): for row in cursor: values = { "added": added_time, - "source": self.source.id, + "source": self.source.source_id, "name": row[1], "game_id": self.source.game_id_format.format(game_id=row[0]), "executable": self.source.executable_format.format(cave_id=row[4]), @@ -78,19 +75,29 @@ class ItchSourceIterator(SourceIterator): rmtree(str(db_path.parent)) +class ItchLocations(NamedTuple): + config: Location + + class ItchSource(URLExecutableSource): + source_id = "itch" name = _("itch") - iterator_class = ItchSourceIterator + iterable_class = ItchSourceIterable url_format = "itch://caves/{cave_id}/launch" available_on = {"linux", "win32"} - config_location = Location( - schema_key="itch-location", - candidates=( - shared.flatpak_dir / "io.itch.itch" / "config" / "itch", - shared.config_dir / "itch", - shared.home / ".config" / "itch", - shared.appdata_dir / "itch", - ), - paths={"butler.db": (False, "db/butler.db")}, + locations = ItchLocations( + Location( + schema_key="itch-location", + candidates=( + shared.flatpak_dir / "io.itch.itch" / "config" / "itch", + shared.config_dir / "itch", + shared.home / ".config" / "itch", + shared.appdata_dir / "itch", + ), + paths={ + "butler.db": LocationSubPath("db/butler.db"), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) ) diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 22392be..03bbdd2 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -21,15 +21,15 @@ import json import logging from json import JSONDecodeError from time import time -from typing import Generator +from typing import NamedTuple from src import shared from src.game import Game -from src.importer.sources.location import Location -from src.importer.sources.source import Source, SourceIterationResult, SourceIterator +from src.importer.sources.location import Location, LocationSubPath +from src.importer.sources.source import Source, SourceIterationResult, SourceIterable -class LegendarySourceIterator(SourceIterator): +class LegendarySourceIterable(SourceIterable): source: "LegendarySource" def game_from_library_entry( @@ -43,7 +43,7 @@ class LegendarySourceIterator(SourceIterator): app_name = entry["app_name"] values = { "added": added_time, - "source": self.source.id, + "source": self.source.source_id, "name": entry["title"], "game_id": self.source.game_id_format.format(game_id=app_name), "executable": self.source.executable_format.format(app_name=app_name), @@ -51,7 +51,7 @@ class LegendarySourceIterator(SourceIterator): data = {} # Get additional metadata from file (optional) - metadata_file = self.source.config_location["metadata"] / f"{app_name}.json" + metadata_file = self.source.locations.config["metadata"] / f"{app_name}.json" try: metadata = json.load(metadata_file.open()) values["developer"] = metadata["metadata"]["developer"] @@ -65,9 +65,9 @@ class LegendarySourceIterator(SourceIterator): game = Game(values) return (game, data) - def generator_builder(self) -> Generator[SourceIterationResult, None, None]: + def __iter__(self): # Open library - file = self.source.config_location["installed.json"] + file = self.source.locations.config["installed.json"] try: library: dict = json.load(file.open()) except (JSONDecodeError, OSError): @@ -89,20 +89,28 @@ class LegendarySourceIterator(SourceIterator): yield result +class LegendaryLocations(NamedTuple): + config: Location + + class LegendarySource(Source): + source_id = "legendary" name = _("Legendary") executable_format = "legendary launch {app_name}" available_on = {"linux"} + iterable_class = LegendarySourceIterable - iterator_class = LegendarySourceIterator - config_location: Location = Location( - schema_key="legendary-location", - candidates=( - shared.config_dir / "legendary", - shared.home / ".config" / "legendary", - ), - paths={ - "installed.json": (False, "installed.json"), - "metadata": (True, "metadata"), - }, + locations = LegendaryLocations( + Location( + schema_key="legendary-location", + candidates=( + shared.config_dir / "legendary", + shared.home / ".config" / "legendary", + ), + paths={ + "installed.json": LocationSubPath("installed.json"), + "metadata": LocationSubPath("metadata", True), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) ) diff --git a/src/importer/sources/location.py b/src/importer/sources/location.py index 8374a20..55684b4 100644 --- a/src/importer/sources/location.py +++ b/src/importer/sources/location.py @@ -1,13 +1,18 @@ import logging from pathlib import Path -from typing import Callable, Mapping, Iterable +from typing import Mapping, Iterable, NamedTuple from os import PathLike from src import shared PathSegment = str | PathLike | Path PathSegments = Iterable[PathSegment] -Candidate = PathSegments | Callable[[], PathSegments] +Candidate = PathSegments + + +class LocationSubPath(NamedTuple): + segment: PathSegment + is_directory: bool = False class UnresolvableLocationError(Exception): @@ -24,31 +29,42 @@ class Location: * When resolved, the schema is updated with the picked chosen """ + # The variable is the name of the source + CACHE_INVALID_SUBTITLE = _("Select the {} cache directory.") + # The variable is the name of the source + CONFIG_INVALID_SUBTITLE = _("Select the {} configuration directory.") + # The variable is the name of the source + DATA_INVALID_SUBTITLE = _("Select the {} data directory.") + schema_key: str candidates: Iterable[Candidate] - paths: Mapping[str, tuple[bool, PathSegments]] + paths: Mapping[str, LocationSubPath] + invalid_subtitle: str + root: Path = None def __init__( self, schema_key: str, candidates: Iterable[Candidate], - paths: Mapping[str, tuple[bool, PathSegments]], + paths: Mapping[str, LocationSubPath], + invalid_subtitle: str, ) -> None: super().__init__() self.schema_key = schema_key self.candidates = candidates self.paths = paths + self.invalid_subtitle = invalid_subtitle def check_candidate(self, candidate: Path) -> bool: """Check if a candidate root has the necessary files and directories""" - for type_is_dir, subpath in self.paths.values(): - subpath = Path(candidate) / Path(subpath) - if type_is_dir: - if not subpath.is_dir(): + for segment, is_directory in self.paths.values(): + path = Path(candidate) / segment + if is_directory: + if not path.is_dir(): return False else: - if not subpath.is_file(): + if not path.is_file(): return False return True @@ -81,4 +97,4 @@ class Location: def __getitem__(self, key: str): """Get the computed path from its key for the location""" self.resolve() - return self.root / self.paths[key][1] + return self.root / self.paths[key].segment diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index ec4b066..023d3be 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -20,22 +20,19 @@ from shutil import rmtree from sqlite3 import connect from time import time +from typing import NamedTuple from src import shared from src.game import Game -from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.location import Location, LocationSubPath +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.sqlite import copy_db -class LutrisSourceIterator(SourceIterator): +class LutrisSourceIterable(SourceIterable): source: "LutrisSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" # Query the database @@ -55,7 +52,7 @@ class LutrisSourceIterator(SourceIterator): "import_steam": shared.schema.get_boolean("lutris-import-steam"), "import_flatpak": shared.schema.get_boolean("lutris-import-flatpak"), } - db_path = copy_db(self.source.data_location["pga.db"]) + db_path = copy_db(self.source.locations.config["pga.db"]) connection = connect(db_path) cursor = connection.execute(request, params) @@ -68,7 +65,7 @@ class LutrisSourceIterator(SourceIterator): "added": added_time, "hidden": row[4], "name": row[1], - "source": f"{self.source.id}_{row[3]}", + "source": f"{self.source.source_id}_{row[3]}", "game_id": self.source.game_id_format.format( runner=row[3], game_id=row[0] ), @@ -77,7 +74,7 @@ class LutrisSourceIterator(SourceIterator): game = Game(values) # Get official image path - image_path = self.source.cache_location["coverart"] / f"{row[2]}.jpg" + image_path = self.source.locations.cache["coverart"] / f"{row[2]}.jpg" additional_data = {"local_image_path": image_path} # Produce game @@ -87,40 +84,49 @@ class LutrisSourceIterator(SourceIterator): rmtree(str(db_path.parent)) +class LutrisLocations(NamedTuple): + config: Location + cache: Location + + class LutrisSource(URLExecutableSource): """Generic Lutris source""" + source_id = "lutris" name = _("Lutris") - iterator_class = LutrisSourceIterator + iterable_class = LutrisSourceIterable url_format = "lutris:rungameid/{game_id}" available_on = {"linux"} - # FIXME possible bug: location picks ~/.var... and cache_lcoation picks ~/.local... + # FIXME possible bug: config picks ~/.var... and cache picks ~/.local... - data_location = Location( - schema_key="lutris-location", - candidates=( - shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris", - shared.data_dir / "lutris", - shared.home / ".local" / "share" / "lutris", + locations = LutrisLocations( + Location( + schema_key="lutris-location", + candidates=( + shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris", + shared.data_dir / "lutris", + shared.home / ".local" / "share" / "lutris", + ), + paths={ + "pga.db": LocationSubPath("pga.db"), + }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, ), - paths={ - "pga.db": (False, "pga.db"), - }, - ) - - cache_location = Location( - schema_key="lutris-cache-location", - candidates=( - shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris", - shared.cache_dir / "lutris", - shared.home / ".cache" / "lutris", + Location( + schema_key="lutris-cache-location", + candidates=( + shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris", + shared.cache_dir / "lutris", + shared.home / ".cache" / "lutris", + ), + paths={ + "coverart": LocationSubPath("coverart", True), + }, + invalid_subtitle=Location.CACHE_INVALID_SUBTITLE, ), - paths={ - "coverart": (True, "coverart"), - }, ) @property def game_id_format(self): - return self.id + "_{runner}_{game_id}" + return self.source_id + "_{runner}_{game_id}" diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index cb9eb0c..26b2a84 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -19,8 +19,8 @@ import sys from abc import abstractmethod -from collections.abc import Iterable, Iterator -from typing import Any, Generator, Optional +from collections.abc import Iterable +from typing import Any, Generator, Collection from src.game import Game from src.importer.sources.location import Location @@ -29,25 +29,16 @@ from src.importer.sources.location import Location SourceIterationResult = None | Game | tuple[Game, tuple[Any]] -class SourceIterator(Iterator): +class SourceIterable(Iterable): """Data producer for a source of games""" source: "Source" = None - generator: Generator = None def __init__(self, source: "Source") -> None: - super().__init__() self.source = source - self.generator = self.generator_builder() - - def __iter__(self) -> "SourceIterator": - return self - - def __next__(self) -> SourceIterationResult: - return next(self.generator) @abstractmethod - def generator_builder(self) -> Generator[SourceIterationResult, None, None]: + def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ Method that returns a generator that produces games * Should be implemented as a generator method @@ -60,13 +51,12 @@ class SourceIterator(Iterator): class Source(Iterable): """Source of games. E.g an installed app with a config file that lists game directories""" + source_id: str name: str variant: str = None available_on: set[str] = set() - data_location: Optional[Location] = None - cache_location: Optional[Location] = None - config_location: Optional[Location] = None - iterator_class: type[SourceIterator] + iterable_class: type[SourceIterable] + locations: Collection[Location] @property def full_name(self) -> str: @@ -76,18 +66,10 @@ class Source(Iterable): full_name_ += f" ({self.variant})" return full_name_ - @property - def id(self) -> str: # pylint: disable=invalid-name - """The source's identifier""" - id_ = self.name.lower() - if self.variant is not None: - id_ += f"_{self.variant.lower()}" - return id_ - @property def game_id_format(self) -> str: """The string format used to construct game IDs""" - return self.id + "_{game_id}" + return self.source_id + "_{game_id}" @property def is_available(self): @@ -98,7 +80,7 @@ class Source(Iterable): def executable_format(self) -> str: """The executable format used to construct game executables""" - def __iter__(self) -> SourceIterator: + def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ Get an iterator for the source :raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable @@ -108,7 +90,7 @@ class Source(Iterable): if location is None: continue location.resolve() - return self.iterator_class(self) + return iter(self.iterable_class(self)) # pylint: disable=abstract-method diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 561d843..904fb0b 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -18,28 +18,25 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import logging import re from pathlib import Path from time import time -from typing import Iterable +from typing import Iterable, NamedTuple from src import shared from src.game import Game -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.location import Location, LocationSubPath +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.steam import SteamFileHelper, SteamInvalidManifestError -from src.importer.sources.location import Location -class SteamSourceIterator(SourceIterator): +class SteamSourceIterable(SourceIterable): source: "SteamSource" def get_manifest_dirs(self) -> Iterable[Path]: """Get dirs that contain steam app manifests""" - libraryfolders_path = self.source.data_location["libraryfolders.vdf"] + libraryfolders_path = self.source.locations.data["libraryfolders.vdf"] with open(libraryfolders_path, "r", encoding="utf-8") as file: contents = file.read() return [ @@ -62,7 +59,7 @@ class SteamSourceIterator(SourceIterator): ) return manifests - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" appid_cache = set() manifests = self.get_manifests() @@ -74,17 +71,20 @@ class SteamSourceIterator(SourceIterator): steam = SteamFileHelper() try: local_data = steam.get_manifest_data(manifest) - except (OSError, SteamInvalidManifestError): + except (OSError, SteamInvalidManifestError) as error: + logging.debug("Couldn't load appmanifest %s", manifest, exc_info=error) continue # Skip non installed games installed_mask = 4 if not int(local_data["stateflags"]) & installed_mask: + logging.debug("Skipped %s: not installed", manifest) continue # Skip duplicate appids appid = local_data["appid"] if appid in appid_cache: + logging.debug("Skipped %s: appid already seen during import", manifest) continue appid_cache.add(appid) @@ -92,7 +92,7 @@ class SteamSourceIterator(SourceIterator): values = { "added": added_time, "name": local_data["name"], - "source": self.source.id, + "source": self.source.source_id, "game_id": self.source.game_id_format.format(game_id=appid), "executable": self.source.executable_format.format(game_id=appid), } @@ -100,7 +100,7 @@ class SteamSourceIterator(SourceIterator): # Add official cover image image_path = ( - self.source.data_location["librarycache"] + self.source.locations.data["librarycache"] / f"{appid}_library_600x900.jpg" ) additional_data = {"local_image_path": image_path, "steam_appid": appid} @@ -109,22 +109,30 @@ class SteamSourceIterator(SourceIterator): yield (game, additional_data) +class SteamLocations(NamedTuple): + data: Location + + class SteamSource(URLExecutableSource): + source_id = "steam" name = _("Steam") available_on = {"linux", "win32"} - iterator_class = SteamSourceIterator + iterable_class = SteamSourceIterable url_format = "steam://rungameid/{game_id}" - data_location = Location( - schema_key="steam-location", - candidates=( - shared.home / ".steam" / "steam", - shared.data_dir / "Steam", - shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam", - shared.programfiles32_dir / "Steam", - ), - paths={ - "libraryfolders.vdf": (False, "steamapps/libraryfolders.vdf"), - "librarycache": (True, "appcache/librarycache"), - }, + locations = SteamLocations( + Location( + schema_key="steam-location", + candidates=( + shared.home / ".steam" / "steam", + shared.data_dir / "Steam", + shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam", + shared.programfiles32_dir / "Steam", + ), + paths={ + "libraryfolders.vdf": LocationSubPath("steamapps/libraryfolders.vdf"), + "librarycache": LocationSubPath("appcache/librarycache", True), + }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + ) ) diff --git a/src/logging/setup.py b/src/logging/setup.py index 2c0e484..e9737cd 100644 --- a/src/logging/setup.py +++ b/src/logging/setup.py @@ -73,7 +73,7 @@ def setup_logging(): "PIL": { "handlers": ["lib_console_handler", "file_handler"], "propagate": False, - "level": "NOTSET", + "level": "WARNING", }, "urllib3": { "handlers": ["lib_console_handler", "file_handler"], diff --git a/src/main.py b/src/main.py index c52de20..6f4b565 100644 --- a/src/main.py +++ b/src/main.py @@ -46,8 +46,7 @@ from src.logging.setup import log_system_info, setup_logging from src.preferences import PreferencesWindow from src.store.managers.display_manager import DisplayManager from src.store.managers.file_manager import FileManager -from src.store.managers.local_cover_manager import LocalCoverManager -from src.store.managers.online_cover_manager import OnlineCoverManager +from src.store.managers.cover_manager import CoverManager from src.store.managers.sgdb_manager import SGDBManager from src.store.managers.steam_api_manager import SteamAPIManager from src.store.store import Store @@ -98,9 +97,8 @@ class CartridgesApplication(Adw.Application): self.load_games_from_disk() # Add rest of the managers for game imports - shared.store.add_manager(LocalCoverManager()) + shared.store.add_manager(CoverManager()) shared.store.add_manager(SteamAPIManager()) - shared.store.add_manager(OnlineCoverManager()) shared.store.add_manager(SGDBManager()) shared.store.toggle_manager_in_pipelines(FileManager, True) diff --git a/src/preferences.py b/src/preferences.py index fceb7d3..7dd9b95 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -69,6 +69,7 @@ class PreferencesWindow(Adw.PreferencesWindow): heroic_config_file_chooser_button = Gtk.Template.Child() heroic_import_epic_switch = Gtk.Template.Child() heroic_import_gog_switch = Gtk.Template.Child() + heroic_import_amazon_switch = Gtk.Template.Child() heroic_import_sideload_switch = Gtk.Template.Child() bottles_expander_row = Gtk.Template.Child() @@ -148,7 +149,7 @@ class PreferencesWindow(Adw.PreferencesWindow): ): source = source_class() if not source.is_available: - expander_row = getattr(self, f"{source.id}_expander_row") + expander_row = getattr(self, f"{source.source_id}_expander_row") expander_row.set_visible(False) else: self.init_source_row(source) @@ -187,6 +188,7 @@ class PreferencesWindow(Adw.PreferencesWindow): "lutris-import-flatpak", "heroic-import-epic", "heroic-import-gog", + "heroic-import-amazon", "heroic-import-sideload", "flatpak-import-launchers", "sgdb", @@ -254,31 +256,35 @@ class PreferencesWindow(Adw.PreferencesWindow): """Set the dir subtitle for a source's action rows""" for location in ("data", "config", "cache"): # Get the action row to subtitle - action_row = getattr(self, f"{source.id}_{location}_action_row", None) + action_row = getattr( + self, f"{source.source_id}_{location}_action_row", None + ) if not action_row: continue infix = "-cache" if location == "cache" else "" - key = f"{source.id}{infix}-location" + key = f"{source.source_id}{infix}-location" path = Path(shared.schema.get_string(key)).expanduser() # Remove the path prefix if picked via Flatpak portal subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path)) action_row.set_subtitle(subtitle) - def resolve_locations(self, source): + def resolve_locations(self, source: Source): """Resolve locations and add a warning if location cannot be found""" def clear_warning_selection(_widget, label): label.select_region(-1, -1) - for location_name in ("data", "config", "cache"): - action_row = getattr(self, f"{source.id}_{location_name}_action_row", None) + for location_name, location in source.locations._asdict().items(): + action_row = getattr( + self, f"{source.source_id}_{location_name}_action_row", None + ) if not action_row: continue try: - getattr(source, f"{location_name}_location", None).resolve() + location.resolve() except UnresolvableLocationError: popover = Gtk.Popover( @@ -315,7 +321,7 @@ class PreferencesWindow(Adw.PreferencesWindow): menu_button.add_css_class("warning") action_row.add_prefix(menu_button) - self.warning_menu_buttons[source.id] = menu_button + self.warning_menu_buttons[source.source_id] = menu_button def init_source_row(self, source: Source): """Initialize a preference row for a source class""" @@ -329,42 +335,36 @@ class PreferencesWindow(Adw.PreferencesWindow): return # Good picked location - location = getattr(source, f"{location_name}_location") + location = getattr(source.locations, location_name) if location.check_candidate(path): # Set the schema - infix = "-cache" if location_name == "cache" else "" - key = f"{source.id}{infix}-location" + match location_name: + case "config" | "data": + infix = "" + case _: + infix = f"-{location_name}" + key = f"{source.source_id}{infix}-location" value = str(path) shared.schema.set_string(key, value) # Update the row self.update_source_action_row_paths(source) - if self.warning_menu_buttons.get(source.id): + if self.warning_menu_buttons.get(source.source_id): action_row = getattr( - self, f"{source.id}_{location_name}_action_row", None + self, f"{source.source_id}_{location_name}_action_row", None ) - action_row.remove(self.warning_menu_buttons[source.id]) - self.warning_menu_buttons.pop(source.id) + action_row.remove(self.warning_menu_buttons[source.source_id]) + self.warning_menu_buttons.pop(source.source_id) logging.debug("User-set value for schema key %s: %s", key, value) # Bad picked location, inform user else: title = _("Invalid Directory") - match location_name: - case "cache": - # The variable is the name of the source - subtitle_format = _("Select the {} cache directory.") - case "config": - # The variable is the name of the source - subtitle_format = _("Select the {} configuration directory.") - case "data": - # The variable is the name of the source - subtitle_format = _("Select the {} data directory.") dialog = create_dialog( self, title, - subtitle_format.format(source.name), + location.invalid_subtitle.format(source.name), "choose_folder", _("Set Location"), ) @@ -376,19 +376,21 @@ class PreferencesWindow(Adw.PreferencesWindow): dialog.connect("response", on_response) # Bind expander row activation to source being enabled - expander_row = getattr(self, f"{source.id}_expander_row") + expander_row = getattr(self, f"{source.source_id}_expander_row") shared.schema.bind( - source.id, + source.source_id, expander_row, "enable-expansion", Gio.SettingsBindFlags.DEFAULT, ) # Connect dir picker buttons - for location in ("data", "config", "cache"): - button = getattr(self, f"{source.id}_{location}_file_chooser_button", None) + for location_name in source.locations._asdict(): + button = getattr( + self, f"{source.source_id}_{location_name}_file_chooser_button", None + ) if button is not None: - button.connect("clicked", self.choose_folder, set_dir, location) + button.connect("clicked", self.choose_folder, set_dir, location_name) # Set the source row subtitles self.resolve_locations(source) diff --git a/src/store/managers/async_manager.py b/src/store/managers/async_manager.py index 153ce82..c7f2ff8 100644 --- a/src/store/managers/async_manager.py +++ b/src/store/managers/async_manager.py @@ -56,7 +56,7 @@ class AsyncManager(Manager): def _task_thread_func(self, _task, _source_object, data, _cancellable): """Task thread entry point""" game, additional_data, *_rest = data - self.execute_resilient_manager_logic(game, additional_data) + self.run(game, additional_data) def _task_callback(self, _source_object, _result, data): """Method run after the task is done""" diff --git a/src/store/managers/cover_manager.py b/src/store/managers/cover_manager.py new file mode 100644 index 0000000..4496e80 --- /dev/null +++ b/src/store/managers/cover_manager.py @@ -0,0 +1,197 @@ +# local_cover_manager.py +# +# Copyright 2023 Geoffrey Coulaud +# Copyright 2023 kramo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from pathlib import Path +from typing import NamedTuple + +import requests +from gi.repository import Gio, GdkPixbuf +from requests.exceptions import HTTPError, SSLError + +from src import shared +from src.game import Game +from src.store.managers.manager import Manager +from src.store.managers.steam_api_manager import SteamAPIManager +from src.utils.save_cover import resize_cover, save_cover + + +class ImageSize(NamedTuple): + width: float = 0 + height: float = 0 + + @property + def aspect_ratio(self) -> float: + return self.width / self.height + + def __str__(self): + return f"{self.width}x{self.height}" + + def __mul__(self, scale: float | int) -> "ImageSize": + return ImageSize( + self.width * scale, + self.height * scale, + ) + + def __truediv__(self, divisor: float | int) -> "ImageSize": + return self * (1 / divisor) + + def __add__(self, other_size: "ImageSize") -> "ImageSize": + return ImageSize( + self.width + other_size.width, + self.height + other_size.height, + ) + + def __sub__(self, other_size: "ImageSize") -> "ImageSize": + return self + (other_size * -1) + + def element_wise_div(self, other_size: "ImageSize") -> "ImageSize": + """Divide every element of self by the equivalent in the other size""" + return ImageSize( + self.width / other_size.width, + self.height / other_size.height, + ) + + def element_wise_mul(self, other_size: "ImageSize") -> "ImageSize": + """Multiply every element of self by the equivalent in the other size""" + return ImageSize( + self.width * other_size.width, + self.height * other_size.height, + ) + + def invert(self) -> "ImageSize": + """Invert the element of self""" + return ImageSize(1, 1).element_wise_div(self) + + +class CoverManager(Manager): + """ + Manager in charge of adding the cover image of the game + + Order of priority is: + 1. local cover + 2. icon cover + 3. online cover + """ + + run_after = (SteamAPIManager,) + retryable_on = (HTTPError, SSLError, ConnectionError) + + def download_image(self, url: str) -> Path: + image_file = Gio.File.new_tmp()[0] + path = Path(image_file.get_path()) + with requests.get(url, timeout=5) as cover: + cover.raise_for_status() + path.write_bytes(cover.content) + return path + + def is_stretchable(self, source_size: ImageSize, cover_size: ImageSize) -> bool: + is_taller = source_size.aspect_ratio < cover_size.aspect_ratio + if is_taller: + return True + max_stretch = 0.12 + resized_height = (1 / source_size.aspect_ratio) * cover_size.width + stretch = 1 - (resized_height / cover_size.height) + return stretch <= max_stretch + + def save_composited_cover( + self, + game: Game, + image_path: Path, + scale: float = 1, + blur_size: ImageSize = ImageSize(2, 2), + ) -> None: + """ + Save the image composited with a background blur. + If the image is stretchable, just stretch it. + + :param game: The game to save the cover for + :param path: Path where the source image is located + :param scale: + Scale of the smalled image side + compared to the corresponding side in the cover + :param blur_size: Size of the downscaled image used for the blur + """ + + # Load source image + source = GdkPixbuf.Pixbuf.new_from_file(str(image_path)) + source_size = ImageSize(source.get_width(), source.get_height()) + cover_size = ImageSize._make(shared.image_size) + + # Stretch if possible + if scale == 1 and self.is_stretchable(source_size, cover_size): + save_cover(game.game_id, resize_cover(pixbuf=source)) + return + + # Create the blurred cover background + # fmt: off + cover = ( + source + .scale_simple(*blur_size, GdkPixbuf.InterpType.BILINEAR) + .scale_simple(*cover_size, GdkPixbuf.InterpType.BILINEAR) + ) + # fmt: on + + # Scale to fit, apply scaling, then center + uniform_scale = scale * min(cover_size.element_wise_div(source_size)) + source_in_cover_size = source_size * uniform_scale + source_in_cover_position = (cover_size - source_in_cover_size) / 2 + + # Center the scaled source image in the cover + source.composite( + cover, + *source_in_cover_position, + *source_in_cover_size, + *source_in_cover_position, + uniform_scale, + uniform_scale, + GdkPixbuf.InterpType.BILINEAR, + 255, + ) + save_cover(game.game_id, resize_cover(pixbuf=cover)) + + def main(self, game: Game, additional_data: dict) -> None: + if game.blacklisted: + return + for key in ( + "local_image_path", + "local_icon_path", + "online_cover_url", + ): + # Get an image path + if not (value := additional_data.get(key)): + continue + if key == "online_cover_url": + image_path = self.download_image(value) + else: + image_path = Path(value) + if not image_path.is_file(): + continue + + # Icon cover + if key == "local_icon_path": + self.save_composited_cover( + game, + image_path, + scale=0.7, + blur_size=ImageSize(1, 2), + ) + return + + self.save_composited_cover(game, image_path) diff --git a/src/store/managers/display_manager.py b/src/store/managers/display_manager.py index 7d3a8f0..a5005a4 100644 --- a/src/store/managers/display_manager.py +++ b/src/store/managers/display_manager.py @@ -30,7 +30,7 @@ class DisplayManager(Manager): run_after = (SteamAPIManager, SGDBManager) signals = {"update-ready"} - def manager_logic(self, game: Game, _additional_data: dict) -> None: + def main(self, game: Game, _additional_data: dict) -> None: if game.get_parent(): game.get_parent().get_parent().remove(game) if game.get_parent(): diff --git a/src/store/managers/file_manager.py b/src/store/managers/file_manager.py index 4caa3b4..8eee69b 100644 --- a/src/store/managers/file_manager.py +++ b/src/store/managers/file_manager.py @@ -31,7 +31,7 @@ class FileManager(AsyncManager): run_after = (SteamAPIManager,) signals = {"save-ready"} - def manager_logic(self, game: Game, additional_data: dict) -> None: + def main(self, game: Game, additional_data: dict) -> None: if additional_data.get("skip_save"): # Skip saving when loading games from disk return diff --git a/src/store/managers/local_cover_manager.py b/src/store/managers/local_cover_manager.py deleted file mode 100644 index b95c22b..0000000 --- a/src/store/managers/local_cover_manager.py +++ /dev/null @@ -1,71 +0,0 @@ -# local_cover_manager.py -# -# Copyright 2023 Geoffrey Coulaud -# Copyright 2023 kramo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from gi.repository import GdkPixbuf - -from src import shared -from src.game import Game -from src.store.managers.manager import Manager -from src.store.managers.steam_api_manager import SteamAPIManager -from src.utils.save_cover import resize_cover, save_cover - - -class LocalCoverManager(Manager): - """Manager in charge of adding the local cover image of the game""" - - run_after = (SteamAPIManager,) - - def manager_logic(self, game: Game, additional_data: dict) -> None: - if image_path := additional_data.get("local_image_path"): - if not image_path.is_file(): - return - save_cover(game.game_id, resize_cover(image_path)) - elif icon_path := additional_data.get("local_icon_path"): - cover_width, cover_height = shared.image_size - - dest_width = cover_width * 0.7 - dest_height = cover_width * 0.7 - - dest_x = cover_width * 0.15 - dest_y = (cover_height - dest_height) / 2 - - image = GdkPixbuf.Pixbuf.new_from_file(str(icon_path)).scale_simple( - dest_width, dest_height, GdkPixbuf.InterpType.BILINEAR - ) - - cover = image.scale_simple( - 1, 2, GdkPixbuf.InterpType.BILINEAR - ).scale_simple(cover_width, cover_height, GdkPixbuf.InterpType.BILINEAR) - - image.composite( - cover, - dest_x, - dest_y, - dest_width, - dest_height, - dest_x, - dest_y, - 1, - 1, - GdkPixbuf.InterpType.BILINEAR, - 255, - ) - - save_cover(game.game_id, resize_cover(pixbuf=cover)) diff --git a/src/store/managers/manager.py b/src/store/managers/manager.py index b1aadf6..4ef50d0 100644 --- a/src/store/managers/manager.py +++ b/src/store/managers/manager.py @@ -50,7 +50,7 @@ class Manager(ErrorProducer): return type(self).__name__ @abstractmethod - def manager_logic(self, game: Game, additional_data: dict) -> None: + def main(self, game: Game, additional_data: dict) -> None: """ Manager specific logic triggered by the run method * Implemented by final child classes @@ -59,7 +59,7 @@ class Manager(ErrorProducer): * May raise other exceptions that will be reported """ - def execute_resilient_manager_logic(self, game: Game, additional_data: dict): + def run(self, game: Game, additional_data: dict): """Handle errors (retry, ignore or raise) that occur in the manager logic""" # Keep track of the number of tries @@ -106,7 +106,7 @@ class Manager(ErrorProducer): def try_manager_logic(): try: - self.manager_logic(game, additional_data) + self.main(game, additional_data) except Exception as error: # pylint: disable=broad-exception-caught handle_error(error) @@ -116,5 +116,5 @@ class Manager(ErrorProducer): self, game: Game, additional_data: dict, callback: Callable[["Manager"], Any] ) -> None: """Pass the game through the manager""" - self.execute_resilient_manager_logic(game, additional_data) + self.run(game, additional_data) callback(self) diff --git a/src/store/managers/online_cover_manager.py b/src/store/managers/online_cover_manager.py deleted file mode 100644 index bc6ea1e..0000000 --- a/src/store/managers/online_cover_manager.py +++ /dev/null @@ -1,126 +0,0 @@ -# online_cover_manager.py -# -# Copyright 2023 Geoffrey Coulaud -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# SPDX-License-Identifier: GPL-3.0-or-later - -import logging -from pathlib import Path - -import requests -from gi.repository import Gio, GdkPixbuf -from requests.exceptions import HTTPError, SSLError -from PIL import Image - -from src import shared -from src.game import Game -from src.store.managers.local_cover_manager import LocalCoverManager -from src.store.managers.manager import Manager -from src.utils.save_cover import resize_cover, save_cover - - -class OnlineCoverManager(Manager): - """Manager that downloads game covers from URLs""" - - run_after = (LocalCoverManager,) - retryable_on = (HTTPError, SSLError, ConnectionError) - - def save_composited_cover( - self, - game: Game, - image_file: Gio.File, - original_width: int, - original_height: int, - target_width: int, - target_height: int, - ) -> None: - """Save the image composited with a background blur to fit the cover size""" - - logging.debug( - "Compositing image for %s (%s) %dx%d -> %dx%d", - game.name, - game.game_id, - original_width, - original_height, - target_width, - target_height, - ) - - # Load game image - image = GdkPixbuf.Pixbuf.new_from_stream(image_file.read()) - - # Create background blur of the size of the cover - cover = image.scale_simple(2, 2, GdkPixbuf.InterpType.BILINEAR).scale_simple( - target_width, target_height, GdkPixbuf.InterpType.BILINEAR - ) - - # Center the image above the blurred background - scale = min(target_width / original_width, target_height / original_height) - left_padding = (target_width - original_width * scale) / 2 - top_padding = (target_height - original_height * scale) / 2 - image.composite( - cover, - # Top left of overwritten area on the destination - left_padding, - top_padding, - # Size of the overwritten area on the destination - original_width * scale, - original_height * scale, - # Offset - left_padding, - top_padding, - # Scale to apply to the resized image - scale, - scale, - # Compositing stuff - GdkPixbuf.InterpType.BILINEAR, - 255, - ) - - # Resize and save the cover - save_cover(game.game_id, resize_cover(pixbuf=cover)) - - def manager_logic(self, game: Game, additional_data: dict) -> None: - # Ensure that we have a cover to download - cover_url = additional_data.get("online_cover_url") - if not cover_url: - return - - # Download cover - image_file = Gio.File.new_tmp()[0] - image_path = Path(image_file.get_path()) - with requests.get(cover_url, timeout=5) as cover: - cover.raise_for_status() - image_path.write_bytes(cover.content) - - # Get image size - cover_width, cover_height = shared.image_size - with Image.open(image_path) as pil_image: - width, height = pil_image.size - - # Composite if the image is shorter and the stretch amount is too high - aspect_ratio = width / height - target_aspect_ratio = cover_width / cover_height - is_taller = aspect_ratio < target_aspect_ratio - resized_height = height / width * cover_width - stretch = 1 - (resized_height / cover_height) - max_stretch = 0.12 - if is_taller or stretch <= max_stretch: - save_cover(game.game_id, resize_cover(image_path)) - else: - self.save_composited_cover( - game, image_file, width, height, cover_width, cover_height - ) diff --git a/src/store/managers/sgdb_manager.py b/src/store/managers/sgdb_manager.py index 142495f..5c002cb 100644 --- a/src/store/managers/sgdb_manager.py +++ b/src/store/managers/sgdb_manager.py @@ -24,19 +24,18 @@ 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.local_cover_manager import LocalCoverManager -from src.store.managers.online_cover_manager import OnlineCoverManager from src.store.managers.steam_api_manager import SteamAPIManager +from src.store.managers.cover_manager import CoverManager from src.utils.steamgriddb import SGDBAuthError, SGDBHelper class SGDBManager(AsyncManager): """Manager in charge of downloading a game's cover from steamgriddb""" - run_after = (SteamAPIManager, LocalCoverManager, OnlineCoverManager) + run_after = (SteamAPIManager, CoverManager) retryable_on = (HTTPError, SSLError, ConnectionError, JSONDecodeError) - def manager_logic(self, game: Game, _additional_data: dict) -> None: + def main(self, game: Game, _additional_data: dict) -> None: try: sgdb = SGDBHelper() sgdb.conditionaly_update_cover(game) diff --git a/src/store/managers/steam_api_manager.py b/src/store/managers/steam_api_manager.py index 19b38af..1a62ddd 100644 --- a/src/store/managers/steam_api_manager.py +++ b/src/store/managers/steam_api_manager.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from requests.exceptions import HTTPError, SSLError +from urllib3.exceptions import ConnectionError as Urllib3ConnectionError from src.game import Game from src.store.managers.async_manager import AsyncManager @@ -32,7 +33,7 @@ from src.utils.steam import ( class SteamAPIManager(AsyncManager): """Manager in charge of completing a game's data from the Steam API""" - retryable_on = (HTTPError, SSLError, ConnectionError) + retryable_on = (HTTPError, SSLError, Urllib3ConnectionError) steam_api_helper: SteamAPIHelper = None steam_rate_limiter: SteamRateLimiter = None @@ -42,7 +43,7 @@ class SteamAPIManager(AsyncManager): self.steam_rate_limiter = SteamRateLimiter() self.steam_api_helper = SteamAPIHelper(self.steam_rate_limiter) - def manager_logic(self, game: Game, additional_data: dict) -> None: + def main(self, game: Game, additional_data: dict) -> None: # Skip non-steam games appid = additional_data.get("steam_appid", None) if appid is None: diff --git a/src/store/store.py b/src/store/store.py index 9da9ef0..231bbdc 100644 --- a/src/store/store.py +++ b/src/store/store.py @@ -130,7 +130,7 @@ class Store: # Connect signals for manager in self.managers.values(): for signal in manager.signals: - game.connect(signal, manager.execute_resilient_manager_logic) + game.connect(signal, manager.run) # Add the game to the store if not game.source in self.source_games: diff --git a/src/utils/steamgriddb.py b/src/utils/steamgriddb.py index 9cd9c3c..57dda3e 100644 --- a/src/utils/steamgriddb.py +++ b/src/utils/steamgriddb.py @@ -103,11 +103,11 @@ class SGDBHelper: image_trunk = shared.covers_dir / game.game_id still = image_trunk.with_suffix(".tiff") - uri_kwargs = image_trunk.with_suffix(".gif") + animated = image_trunk.with_suffix(".gif") prefer_sgdb = shared.schema.get_boolean("sgdb-prefer") # Do nothing if file present and not prefer SGDB - if not prefer_sgdb and (still.is_file() or uri_kwargs.is_file()): + if not prefer_sgdb and (still.is_file() or animated.is_file()): return # Get ID for the game