diff --git a/data/gtk/details-window.blp b/data/gtk/details-window.blp
index ecddb13..d84b4f8 100644
--- a/data/gtk/details-window.blp
+++ b/data/gtk/details-window.blp
@@ -2,7 +2,7 @@ using Gtk 4.0;
using Adw 1;
template $DetailsWindow : Adw.Window {
- default-width: 500;
+ default-width: 480; // Same as Nautilus' properties window
default-height: -1;
modal: true;
@@ -97,34 +97,20 @@ template $DetailsWindow : Adw.Window {
}
}
- Adw.PreferencesGroup title_group {
- title: _("Title");
- description: _("The title of the game");
-
- Entry name {
- accessibility {
- label: _("Title");
- }
+ Adw.PreferencesGroup {
+ Adw.EntryRow name {
+ title: _("Title");
+ }
+ Adw.EntryRow developer {
+ title: _("Developer (optional)");
}
}
+ Adw.PreferencesGroup {
+ Adw.EntryRow executable {
+ title: _("Executable");
- Adw.PreferencesGroup developer_group {
- title: _("Developer");
- description: _("The developer or publisher (optional)");
-
- Entry developer {
- accessibility {
- label: _("Developer");
- }
- }
- }
-
- Adw.PreferencesGroup exec_group {
- title: _("Executable");
- description: _("File to open or command to run when launching the game");
-
- [header-suffix]
- Gtk.MenuButton exec_info_button {
+ [suffix]
+ Gtk.MenuButton exec_info_button {
valign: center;
icon-name: "help-about-symbolic";
tooltip-text: _("More Info");
@@ -150,10 +136,6 @@ template $DetailsWindow : Adw.Window {
]
}
- Entry executable {
- accessibility {
- label: _("Executable");
- }
}
}
}
diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp
index 5b46167..e838cb4 100644
--- a/data/gtk/preferences.blp
+++ b/data/gtk/preferences.blp
@@ -138,6 +138,10 @@ template $PreferencesWindow : Adw.PreferencesWindow {
title: _("Import GOG Games");
}
+ Adw.SwitchRow heroic_import_amazon_switch {
+ title: _("Import Amazon Games");
+ }
+
Adw.SwitchRow heroic_import_sideload_switch {
title: _("Import Sideloaded Games");
}
@@ -185,6 +189,20 @@ template $PreferencesWindow : Adw.PreferencesWindow {
}
}
+ Adw.ExpanderRow retroarch_expander_row {
+ title: _("RetroArch");
+ show-enable-switch: true;
+
+ Adw.ActionRow retroarch_config_action_row {
+ title: _("Install Location");
+
+ Button retroarch_config_file_chooser_button {
+ icon-name: "folder-symbolic";
+ valign: center;
+ }
+ }
+ }
+
Adw.ExpanderRow flatpak_expander_row {
title: _("Flatpak");
show-enable-switch: true;
diff --git a/data/hu.kramo.Cartridges.desktop.in b/data/hu.kramo.Cartridges.desktop.in
index 7341def..0c0c75a 100644
--- a/data/hu.kramo.Cartridges.desktop.in
+++ b/data/hu.kramo.Cartridges.desktop.in
@@ -7,5 +7,5 @@ Icon=@APP_ID@
Terminal=false
Type=Application
Categories=GNOME;GTK;Game;
-Keywords=gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;
+Keywords=gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;retroarch;
StartupNotify=true
diff --git a/data/hu.kramo.Cartridges.gschema.xml.in b/data/hu.kramo.Cartridges.gschema.xml.in
index b18fc12..01d625d 100644
--- a/data/hu.kramo.Cartridges.gschema.xml.in
+++ b/data/hu.kramo.Cartridges.gschema.xml.in
@@ -43,6 +43,9 @@
true
+
+ true
+
true
@@ -64,6 +67,12 @@
"~/.config/legendary/"
+
+ true
+
+
+ "~/.var/app/org.libretro.RetroArch/config/retroarch/"
+
true
@@ -113,4 +122,4 @@
"[]"
-
\ No newline at end of file
+
diff --git a/data/hu.kramo.Cartridges.metainfo.xml.in b/data/hu.kramo.Cartridges.metainfo.xml.in
index dd4695c..cc709a9 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 07a51b4..e14f256 100644
--- a/flatpak/hu.kramo.Cartridges.Devel.json
+++ b/flatpak/hu.kramo.Cartridges.Devel.json
@@ -15,8 +15,10 @@
"--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/app/org.libretro.RetroArch/config/retroarch/:ro",
"--filesystem=/var/lib/flatpak:ro"
],
"cleanup" : [
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/POTFILES b/po/POTFILES
index 6b25bb0..d48bf15 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -16,4 +16,5 @@ src/preferences.py
src/utils/create_dialog.py
src/importer/sources/source.py
+src/importer/sources/location.py
src/store/managers/sgdb_manager.py
\ No newline at end of file
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..2308909 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-08-13 12:49+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:169
msgid "Cartridges"
msgstr ""
@@ -33,7 +33,8 @@ 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;retroarch;"
msgstr ""
#: data/hu.kramo.Cartridges.metainfo.xml.in:9
@@ -48,16 +49,17 @@ msgstr ""
msgid "Library"
msgstr ""
-#: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67
+#: data/hu.kramo.Cartridges.metainfo.xml.in:34
msgid "Edit Game Details"
msgstr ""
#: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71
+#: src/details_window.py:67
msgid "Game Details"
msgstr ""
#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416
-#: src/details_window.py:239
+#: src/details_window.py:241
msgid "Preferences"
msgstr ""
@@ -73,32 +75,19 @@ msgstr ""
msgid "Delete Cover"
msgstr ""
-#: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106
-#: data/gtk/game.blp:80
+#: data/gtk/details-window.blp:102 data/gtk/game.blp:80
msgid "Title"
msgstr ""
-#: data/gtk/details-window.blp:102
-msgid "The title of the game"
+#: data/gtk/details-window.blp:105
+msgid "Developer (optional)"
msgstr ""
-#: data/gtk/details-window.blp:112 data/gtk/details-window.blp:117
-msgid "Developer"
-msgstr ""
-
-#: data/gtk/details-window.blp:113
-msgid "The developer or publisher (optional)"
-msgstr ""
-
-#: data/gtk/details-window.blp:123 data/gtk/details-window.blp:155
+#: data/gtk/details-window.blp:110
msgid "Executable"
msgstr ""
-#: data/gtk/details-window.blp:124
-msgid "File to open or command to run when launching the game"
-msgstr ""
-
-#: data/gtk/details-window.blp:130
+#: data/gtk/details-window.blp:116
msgid "More Info"
msgstr ""
@@ -140,7 +129,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:118
msgid "Undo"
msgstr ""
@@ -168,7 +157,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:291
msgid "Behavior"
msgstr ""
@@ -217,9 +206,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 data/gtk/preferences.blp:257
msgid "Install Location"
msgstr ""
@@ -252,54 +241,62 @@ 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 "RetroArch"
+msgstr ""
+
+#: data/gtk/preferences.blp:253
msgid "Flatpak"
msgstr ""
-#: data/gtk/preferences.blp:243
+#: data/gtk/preferences.blp:266
msgid "Import Game Launchers"
msgstr ""
-#: data/gtk/preferences.blp:256
+#: data/gtk/preferences.blp:279
msgid "SteamGridDB"
msgstr ""
-#: data/gtk/preferences.blp:260
+#: data/gtk/preferences.blp:283
msgid "Authentication"
msgstr ""
-#: data/gtk/preferences.blp:263
+#: data/gtk/preferences.blp:286
msgid "API Key"
msgstr ""
-#: data/gtk/preferences.blp:271
+#: data/gtk/preferences.blp:294
msgid "Use SteamGridDB"
msgstr ""
-#: data/gtk/preferences.blp:272
+#: data/gtk/preferences.blp:295
msgid "Download images when adding or importing games"
msgstr ""
-#: data/gtk/preferences.blp:281
+#: data/gtk/preferences.blp:304
msgid "Prefer Over Official Images"
msgstr ""
-#: data/gtk/preferences.blp:290
+#: data/gtk/preferences.blp:313
msgid "Prefer Animated Images"
msgstr ""
@@ -388,7 +385,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 ""
@@ -415,7 +412,7 @@ msgid "Add New Game"
msgstr ""
#: src/details_window.py:79
-msgid "Confirm"
+msgid "Add"
msgstr ""
#. Translate this string as you would translate "file"
@@ -455,15 +452,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 +482,28 @@ msgstr ""
msgid "{} removed"
msgstr ""
-#: src/preferences.py:111
+#: src/preferences.py:117
msgid "All games removed"
msgstr ""
-#: src/preferences.py:159
+#: src/preferences.py:166
msgid ""
"An API key is required to use SteamGridDB. You can generate one {}here{}."
msgstr ""
-#: src/preferences.py:284
+#: src/preferences.py:295
msgid "Installation Not Found"
msgstr ""
-#: src/preferences.py:286
+#: src/preferences.py:297
msgid "Select a valid directory."
msgstr ""
-#: src/preferences.py:348
+#: src/preferences.py:363
msgid "Invalid Directory"
msgstr ""
-#. The variable is the name of the source
-#: src/preferences.py:352
-msgid "Select the {} cache directory."
-msgstr ""
-
-#. The variable is the name of the source
-#: src/preferences.py:355
-msgid "Select the {} configuration directory."
-msgstr ""
-
-#. The variable is the name of the source
-#: src/preferences.py:358
-msgid "Select the {} data directory."
-msgstr ""
-
-#: src/preferences.py:364
+#: src/preferences.py:369
msgid "Set Location"
msgstr ""
@@ -529,10 +511,25 @@ msgstr ""
msgid "Dismiss"
msgstr ""
-#: src/store/managers/sgdb_manager.py:47
+#. The variable is the name of the source
+#: src/importer/sources/location.py:33
+msgid "Select the {} cache directory."
+msgstr ""
+
+#. The variable is the name of the source
+#: src/importer/sources/location.py:35
+msgid "Select the {} configuration directory."
+msgstr ""
+
+#. The variable is the name of the source
+#: src/importer/sources/location.py:37
+msgid "Select the {} data directory."
+msgstr ""
+
+#: src/store/managers/sgdb_manager.py:46
msgid "Couldn't Authenticate SteamGridDB"
msgstr ""
-#: src/store/managers/sgdb_manager.py:48
+#: src/store/managers/sgdb_manager.py:47
msgid "Verify your API key in preferences"
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/details_window.py b/src/details_window.py
index 8d60980..fee029a 100644
--- a/src/details_window.py
+++ b/src/details_window.py
@@ -64,7 +64,7 @@ class DetailsWindow(Adw.Window):
self.set_transient_for(self.win)
if self.game:
- self.set_title(_("Edit Game Details"))
+ self.set_title(_("Game Details"))
self.name.set_text(self.game.name)
if self.game.developer:
self.developer.set_text(self.game.developer)
@@ -76,7 +76,7 @@ class DetailsWindow(Adw.Window):
self.cover_button_delete_revealer.set_reveal_child(True)
else:
self.set_title(_("Add New Game"))
- self.apply_button.set_label(_("Confirm"))
+ self.apply_button.set_label(_("Add"))
image_filter = Gtk.FileFilter(name=_("Images"))
for extension in Image.registered_extensions():
@@ -123,9 +123,9 @@ class DetailsWindow(Adw.Window):
self.cover_button_edit.connect("clicked", self.choose_cover)
self.apply_button.connect("clicked", self.apply_preferences)
- self.name.connect("activate", self.focus_executable)
- self.developer.connect("activate", self.focus_executable)
- self.executable.connect("activate", self.apply_preferences)
+ self.name.connect("entry-activated", self.focus_executable)
+ self.developer.connect("entry-activated", self.focus_executable)
+ self.executable.connect("entry-activated", self.apply_preferences)
self.set_focus(self.name)
self.present()
diff --git a/src/importer/importer.py b/src/importer/importer.py
index fba0483..db76522 100644
--- a/src/importer/importer.py
+++ b/src/importer/importer.py
@@ -106,7 +106,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,))
@@ -139,16 +139,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:
@@ -156,7 +156,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
@@ -173,7 +173,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
@@ -195,7 +195,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/retroarch_source.py b/src/importer/sources/retroarch_source.py
new file mode 100644
index 0000000..96b16e1
--- /dev/null
+++ b/src/importer/sources/retroarch_source.py
@@ -0,0 +1,216 @@
+# retroarch_source.py
+#
+# Copyright 2023 Rilic
+#
+# 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 json
+import logging
+import re
+from hashlib import md5
+from json import JSONDecodeError
+from pathlib import Path
+from time import time
+from typing import NamedTuple
+
+from src import shared
+from src.errors.friendly_error import FriendlyError
+from src.game import Game
+from src.importer.sources.location import (
+ Location,
+ LocationSubPath,
+ UnresolvableLocationError,
+)
+from src.importer.sources.source import Source, SourceIterable
+from src.importer.sources.steam_source import SteamSource
+
+
+class RetroarchSourceIterable(SourceIterable):
+ source: "RetroarchSource"
+
+ def get_config_value(self, key: str, config_data: str):
+ for item in re.findall(f'{key}\\s*=\\s*"(.*)"\n', config_data, re.IGNORECASE):
+ if item.startswith(":"):
+ item = item.replace(":", str(self.source.locations.config.root))
+
+ logging.debug(str(item))
+ return item
+
+ raise KeyError(f"Key not found in RetroArch config: {key}")
+
+ def __iter__(self):
+ added_time = int(time())
+ bad_playlists = set()
+
+ config_file = self.source.locations.config["retroarch.cfg"]
+ with config_file.open(encoding="utf-8") as open_file:
+ config_data = open_file.read()
+
+ playlist_folder = Path(
+ self.get_config_value("playlist_directory", config_data)
+ ).expanduser()
+ thumbnail_folder = Path(
+ self.get_config_value("thumbnails_directory", config_data)
+ ).expanduser()
+
+ # Get all playlist files, ending in .lpl
+ playlist_files = playlist_folder.glob("*.lpl")
+
+ for playlist_file in playlist_files:
+ logging.debug(playlist_file)
+ try:
+ with playlist_file.open(
+ encoding="utf-8",
+ ) as open_file:
+ playlist_json = json.load(open_file)
+ except (JSONDecodeError, OSError):
+ logging.warning("Cannot read playlist file: %s", str(playlist_file))
+ continue
+
+ for item in playlist_json["items"]:
+ # Select the core.
+ # Try the content's core first, then the playlist's default core.
+ # If none can be used, warn the user and continue.
+ for core_path in (
+ item["core_path"],
+ playlist_json["default_core_path"],
+ ):
+ if core_path not in ("DETECT", ""):
+ break
+ else:
+ logging.warning("Cannot find core for: %s", str(item["path"]))
+ bad_playlists.add(playlist_file.stem)
+ continue
+
+ # Build game
+ game_id = md5(item["path"].encode("utf-8")).hexdigest()
+
+ values = {
+ "source": self.source.source_id,
+ "added": added_time,
+ "name": item["label"],
+ "game_id": self.source.game_id_format.format(game_id=game_id),
+ "executable": self.source.executable_format.format(
+ rom_path=item["path"],
+ core_path=core_path,
+ ),
+ }
+
+ game = Game(values)
+
+ # Get boxart
+ boxart_image_name = item["label"] + ".png"
+ boxart_image_name = re.sub(r"[&\*\/:`<>\?\\\|]", "_", boxart_image_name)
+ boxart_folder_name = playlist_file.stem
+ image_path = (
+ thumbnail_folder
+ / boxart_folder_name
+ / "Named_Boxarts"
+ / boxart_image_name
+ )
+ additional_data = {"local_image_path": image_path}
+
+ yield (game, additional_data)
+
+ if bad_playlists:
+ raise FriendlyError(
+ _("No RetroArch Core Selected"),
+ # The variable is a newline separated list of playlists
+ _("The following playlists have no default core:")
+ + "\n\n{}\n\n".format("\n".join(bad_playlists))
+ + _("Games with no core selected were not imported"),
+ )
+
+
+class RetroarchLocations(NamedTuple):
+ config: Location
+
+
+class RetroarchSource(Source):
+ name = _("RetroArch")
+ source_id = "retroarch"
+ available_on = {"linux"}
+ iterable_class = RetroarchSourceIterable
+
+ locations = RetroarchLocations(
+ Location(
+ schema_key="retroarch-location",
+ candidates=[
+ shared.flatpak_dir / "org.libretro.RetroArch" / "config" / "retroarch",
+ shared.config_dir / "retroarch",
+ shared.home / ".config" / "retroarch",
+ # TODO: Windows support, waiting for executable path setting improvement
+ # Path("C:\\RetroArch-Win64"),
+ # Path("C:\\RetroArch-Win32"),
+ # TODO: UWP support (URL handler - https://github.com/libretro/RetroArch/pull/13563)
+ # shared.local_appdata_dir
+ # / "Packages"
+ # / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma"
+ # / "LocalState",
+ ],
+ paths={
+ "retroarch.cfg": LocationSubPath("retroarch.cfg"),
+ },
+ invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE,
+ )
+ )
+
+ @property
+ def executable_format(self):
+ self.locations.config.resolve()
+ is_flatpak = self.locations.config.root.is_relative_to(shared.flatpak_dir)
+ base = "flatpak run org.libretro.RetroArch" if is_flatpak else "retroarch"
+ args = '-L "{core_path}" "{rom_path}"'
+ return f"{base} {args}"
+
+ def __init__(self) -> None:
+ super().__init__()
+ try:
+ self.locations.config.candidates.append(self.get_steam_location())
+ except (OSError, KeyError, UnresolvableLocationError):
+ logging.debug("Steam isn't installed")
+ except ValueError as error:
+ logging.debug("RetroArch Steam location candiate not found", exc_info=error)
+
+ def get_steam_location(self) -> str:
+ """
+ Get the RetroArch installed via Steam location
+
+ :raise UnresolvableLocationError: if steam isn't installed
+ :raise KeyError: if there is no libraryfolders.vdf subpath
+ :raise OSError: if libraryfolders.vdf can't be opened
+ :raise ValueError: if RetroArch isn't installed through Steam
+ """
+
+ # Find steam location
+ libraryfolders = SteamSource().locations.data["libraryfolders.vdf"]
+ parse_apps = False
+ with open(libraryfolders, "r", encoding="utf-8") as open_file:
+ # Search each line for a library path and store it each time a new one is found.
+ for line in open_file:
+ if '"path"' in line:
+ library_path = re.findall(
+ '"path"\\s+"(.*)"\n', line, re.IGNORECASE
+ )[0]
+ elif '"apps"' in line:
+ parse_apps = True
+ elif parse_apps and "}" in line:
+ parse_apps = False
+ # Stop searching, as the library path directly above the appid has been found.
+ elif parse_apps and '"1118310"' in line:
+ return Path(f"{library_path}/steamapps/common/RetroArch")
+ # Not found
+ raise ValueError("RetroArch not found in Steam library")
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 48381c8..d199fd0 100644
--- a/src/main.py
+++ b/src/main.py
@@ -40,13 +40,13 @@ from src.importer.sources.heroic_source import HeroicSource
from src.importer.sources.itch_source import ItchSource
from src.importer.sources.legendary_source import LegendarySource
from src.importer.sources.lutris_source import LutrisSource
+from src.importer.sources.retroarch_source import RetroarchSource
from src.importer.sources.steam_source import SteamSource
from src.logging.setup import log_system_info, setup_logging
from src.preferences import PreferencesWindow
+from src.store.managers.cover_manager import CoverManager
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.sgdb_manager import SGDBManager
from src.store.managers.steam_api_manager import SteamAPIManager
from src.store.store import Store
@@ -101,9 +101,8 @@ class CartridgesApplication(Adw.Application):
self.win.create_source_rows()
# 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)
@@ -180,9 +179,10 @@ class CartridgesApplication(Adw.Application):
(
"kramo https://kramo.hu",
"Geoffrey Coulaud https://geoffrey-coulaud.fr",
+ "Rilic https://rilic.red",
"Arcitec https://github.com/Arcitec",
- "Domenico https://github.com/Domefemia",
"Paweł Lidwin https://github.com/imLinguin",
+ "Domenico https://github.com/Domefemia",
"Rafael Mardojai CM https://mardojai.com",
)
)
@@ -248,6 +248,9 @@ class CartridgesApplication(Adw.Application):
if shared.schema.get_boolean("legendary"):
importer.add_source(LegendarySource())
+ if shared.schema.get_boolean("retroarch"):
+ importer.add_source(RetroarchSource())
+
importer.run()
def on_remove_game_action(self, *_args):
diff --git a/src/preferences.py b/src/preferences.py
index 7fa5a0a..57d2c78 100644
--- a/src/preferences.py
+++ b/src/preferences.py
@@ -32,6 +32,7 @@ from src.importer.sources.itch_source import ItchSource
from src.importer.sources.legendary_source import LegendarySource
from src.importer.sources.location import UnresolvableLocationError
from src.importer.sources.lutris_source import LutrisSource
+from src.importer.sources.retroarch_source import RetroarchSource
from src.importer.sources.source import Source
from src.importer.sources.steam_source import SteamSource
from src.utils.create_dialog import create_dialog
@@ -68,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()
@@ -82,6 +84,10 @@ class PreferencesWindow(Adw.PreferencesWindow):
legendary_config_action_row = Gtk.Template.Child()
legendary_config_file_chooser_button = Gtk.Template.Child()
+ retroarch_expander_row = Gtk.Template.Child()
+ retroarch_config_action_row = Gtk.Template.Child()
+ retroarch_config_file_chooser_button = Gtk.Template.Child()
+
flatpak_expander_row = Gtk.Template.Child()
flatpak_data_action_row = Gtk.Template.Child()
flatpak_data_file_chooser_button = Gtk.Template.Child()
@@ -136,11 +142,12 @@ class PreferencesWindow(Adw.PreferencesWindow):
ItchSource,
LegendarySource,
LutrisSource,
+ RetroarchSource,
SteamSource,
):
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)
@@ -170,6 +177,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
"lutris-import-flatpak",
"heroic-import-epic",
"heroic-import-gog",
+ "heroic-import-amazon",
"heroic-import-sideload",
"flatpak-import-launchers",
"sgdb",
@@ -252,31 +260,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(
@@ -313,7 +325,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"""
@@ -327,42 +339,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"),
)
@@ -374,19 +380,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/shared.py.in b/src/shared.py.in
index d5fa386..ff1a29c 100644
--- a/src/shared.py.in
+++ b/src/shared.py.in
@@ -51,6 +51,7 @@ games_dir = data_dir / "cartridges" / "games"
covers_dir = data_dir / "cartridges" / "covers"
appdata_dir = Path(os.getenv("appdata") or "C:\\Users\\Default\\AppData\\Roaming")
+local_appdata_dir = Path(os.getenv("csidl_local_appdata") or "C:\\Users\\Default\\AppData\\Local")
programfiles32_dir = Path(os.getenv("programfiles(x86)") or "C:\\Program Files (x86)")
scale_factor = max(
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 1602d59..0304e94 100644
--- a/src/store/managers/display_manager.py
+++ b/src/store/managers/display_manager.py
@@ -31,7 +31,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