Merge branch 'main' into libadwaita-1.4
This commit is contained in:
@@ -69,6 +69,14 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
||||
title: _("Import");
|
||||
icon-name: "document-save-symbolic";
|
||||
|
||||
Adw.PreferencesGroup import_behavior_group {
|
||||
title: _("Behavior");
|
||||
|
||||
Adw.SwitchRow remove_missing_switch {
|
||||
title: _("Remove Uninstalled Games");
|
||||
}
|
||||
}
|
||||
|
||||
Adw.PreferencesGroup sources_group {
|
||||
title: _("Sources");
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
<key name="high-quality-images" type="b">
|
||||
<default>false</default>
|
||||
</key>
|
||||
<key name="remove-missing" type="b">
|
||||
<default>true</default>
|
||||
</key>
|
||||
<key name="steam" type="b">
|
||||
<default>true</default>
|
||||
</key>
|
||||
|
||||
@@ -15,6 +15,7 @@ src/game.py
|
||||
src/preferences.py
|
||||
|
||||
src/utils/create_dialog.py
|
||||
src/importer/importer.py
|
||||
src/importer/sources/source.py
|
||||
src/importer/sources/location.py
|
||||
src/store/managers/sgdb_manager.py
|
||||
@@ -8,13 +8,13 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Cartridges\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-13 12:49+0200\n"
|
||||
"POT-Creation-Date: 2023-08-16 11:06+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: data/hu.kramo.Cartridges.desktop.in:3
|
||||
@@ -58,8 +58,9 @@ msgstr ""
|
||||
msgid "Game Details"
|
||||
msgstr ""
|
||||
|
||||
#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416
|
||||
#: src/details_window.py:241
|
||||
#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:418
|
||||
#: src/details_window.py:241 src/importer/importer.py:292
|
||||
#: src/importer/importer.py:342
|
||||
msgid "Preferences"
|
||||
msgstr ""
|
||||
|
||||
@@ -117,7 +118,7 @@ msgid "Quit"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/help-overlay.blp:19 data/gtk/window.blp:217 data/gtk/window.blp:257
|
||||
#: data/gtk/window.blp:323
|
||||
#: data/gtk/window.blp:324
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
@@ -129,7 +130,8 @@ msgstr ""
|
||||
msgid "Shortcuts"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:118
|
||||
#: data/gtk/help-overlay.blp:34 src/game.py:103 src/preferences.py:120
|
||||
#: src/importer/importer.py:366
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
|
||||
@@ -157,7 +159,8 @@ msgstr ""
|
||||
msgid "Remove game"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:291
|
||||
#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:89
|
||||
#: data/gtk/preferences.blp:304
|
||||
msgid "Behavior"
|
||||
msgstr ""
|
||||
|
||||
@@ -193,110 +196,114 @@ msgstr ""
|
||||
msgid "Remove All Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:442
|
||||
#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:444
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:89
|
||||
#: data/gtk/preferences.blp:92
|
||||
msgid "Remove Uninstalled Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:102
|
||||
msgid "Sources"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:92
|
||||
#: data/gtk/preferences.blp:105
|
||||
msgid "Steam"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110
|
||||
#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201
|
||||
#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229
|
||||
#: data/gtk/preferences.blp:243 data/gtk/preferences.blp:257
|
||||
#: data/gtk/preferences.blp:109 data/gtk/preferences.blp:123
|
||||
#: data/gtk/preferences.blp:164 data/gtk/preferences.blp:214
|
||||
#: data/gtk/preferences.blp:228 data/gtk/preferences.blp:242
|
||||
#: data/gtk/preferences.blp:256 data/gtk/preferences.blp:270
|
||||
msgid "Install Location"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:106
|
||||
#: data/gtk/preferences.blp:119
|
||||
msgid "Lutris"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:119
|
||||
#: data/gtk/preferences.blp:132
|
||||
msgid "Cache Location"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:128
|
||||
#: data/gtk/preferences.blp:141
|
||||
msgid "Import Steam Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:137
|
||||
#: data/gtk/preferences.blp:150
|
||||
msgid "Import Flatpak Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:147
|
||||
#: data/gtk/preferences.blp:160
|
||||
msgid "Heroic"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:160
|
||||
#: data/gtk/preferences.blp:173
|
||||
msgid "Import Epic Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:169
|
||||
#: data/gtk/preferences.blp:182
|
||||
msgid "Import GOG Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:178
|
||||
#: data/gtk/preferences.blp:191
|
||||
msgid "Import Amazon Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:187
|
||||
#: data/gtk/preferences.blp:200
|
||||
msgid "Import Sideloaded Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:197
|
||||
#: data/gtk/preferences.blp:210
|
||||
msgid "Bottles"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:211
|
||||
#: data/gtk/preferences.blp:224
|
||||
msgid "itch"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:225
|
||||
#: data/gtk/preferences.blp:238
|
||||
msgid "Legendary"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:239
|
||||
#: data/gtk/preferences.blp:252
|
||||
msgid "RetroArch"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:253
|
||||
#: data/gtk/preferences.blp:266
|
||||
msgid "Flatpak"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:266
|
||||
#: data/gtk/preferences.blp:279
|
||||
msgid "Import Game Launchers"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:279
|
||||
#: data/gtk/preferences.blp:292
|
||||
msgid "SteamGridDB"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:283
|
||||
#: data/gtk/preferences.blp:296
|
||||
msgid "Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:286
|
||||
#: data/gtk/preferences.blp:299
|
||||
msgid "API Key"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:294
|
||||
#: data/gtk/preferences.blp:307
|
||||
msgid "Use SteamGridDB"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:295
|
||||
#: data/gtk/preferences.blp:308
|
||||
msgid "Download images when adding or importing games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:304
|
||||
#: data/gtk/preferences.blp:317
|
||||
msgid "Prefer Over Official Images"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/preferences.blp:313
|
||||
#: data/gtk/preferences.blp:326
|
||||
msgid "Prefer Animated Images"
|
||||
msgstr ""
|
||||
|
||||
@@ -324,7 +331,7 @@ msgstr ""
|
||||
msgid "Games you hide will appear here."
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:64 data/gtk/window.blp:304
|
||||
#: data/gtk/window.blp:64 data/gtk/window.blp:305
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
@@ -336,51 +343,59 @@ msgstr ""
|
||||
msgid "Play"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:243 data/gtk/window.blp:435
|
||||
#: data/gtk/window.blp:243 data/gtk/window.blp:437
|
||||
msgid "Add Game"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:250 data/gtk/window.blp:316
|
||||
#: data/gtk/window.blp:250 data/gtk/window.blp:317
|
||||
msgid "Main Menu"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:311
|
||||
#: data/gtk/window.blp:272
|
||||
msgid "Search games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:312
|
||||
msgid "Hidden Games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:374
|
||||
#: data/gtk/window.blp:339
|
||||
msgid "Search hidden games"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:376
|
||||
msgid "Sort"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:377
|
||||
#: data/gtk/window.blp:379
|
||||
msgid "A-Z"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:383
|
||||
#: data/gtk/window.blp:385
|
||||
msgid "Z-A"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:389
|
||||
#: data/gtk/window.blp:391
|
||||
msgid "Newest"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:395
|
||||
#: data/gtk/window.blp:397
|
||||
msgid "Oldest"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:401
|
||||
#: data/gtk/window.blp:403
|
||||
msgid "Last Played"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:408
|
||||
#: data/gtk/window.blp:410
|
||||
msgid "Show Hidden"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:421
|
||||
#: data/gtk/window.blp:423
|
||||
msgid "Keyboard Shortcuts"
|
||||
msgstr ""
|
||||
|
||||
#: data/gtk/window.blp:426
|
||||
#: data/gtk/window.blp:428
|
||||
msgid "About Cartridges"
|
||||
msgstr ""
|
||||
|
||||
@@ -465,52 +480,84 @@ msgid "Couldn't Apply Preferences"
|
||||
msgstr ""
|
||||
|
||||
#. The variable is the title of the game
|
||||
#: src/game.py:138
|
||||
#: src/game.py:139
|
||||
msgid "{} launched"
|
||||
msgstr ""
|
||||
|
||||
#. The variable is the title of the game
|
||||
#: src/game.py:152
|
||||
#: src/game.py:153
|
||||
msgid "{} hidden"
|
||||
msgstr ""
|
||||
|
||||
#: src/game.py:152
|
||||
#: src/game.py:153
|
||||
msgid "{} unhidden"
|
||||
msgstr ""
|
||||
|
||||
#: src/game.py:169
|
||||
#. The variable is the title of the game
|
||||
#. The variable is the number of games removed
|
||||
#: src/game.py:170 src/importer/importer.py:363
|
||||
msgid "{} removed"
|
||||
msgstr ""
|
||||
|
||||
#: src/preferences.py:117
|
||||
#: src/preferences.py:119
|
||||
msgid "All games removed"
|
||||
msgstr ""
|
||||
|
||||
#: src/preferences.py:166
|
||||
#: src/preferences.py:168
|
||||
msgid ""
|
||||
"An API key is required to use SteamGridDB. You can generate one {}here{}."
|
||||
msgstr ""
|
||||
|
||||
#: src/preferences.py:295
|
||||
#: src/preferences.py:294
|
||||
msgid "Installation Not Found"
|
||||
msgstr ""
|
||||
|
||||
#: src/preferences.py:297
|
||||
#: src/preferences.py:296
|
||||
msgid "Select a valid directory."
|
||||
msgstr ""
|
||||
|
||||
#: src/preferences.py:363
|
||||
#: src/preferences.py:351
|
||||
msgid "Invalid Directory"
|
||||
msgstr ""
|
||||
|
||||
#: src/preferences.py:369
|
||||
#: src/preferences.py:357
|
||||
msgid "Set Location"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/create_dialog.py:25
|
||||
#: src/utils/create_dialog.py:25 src/importer/importer.py:291
|
||||
msgid "Dismiss"
|
||||
msgstr ""
|
||||
|
||||
#: src/importer/importer.py:128
|
||||
msgid "Importing Games…"
|
||||
msgstr ""
|
||||
|
||||
#: src/importer/importer.py:290
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
#: src/importer/importer.py:311
|
||||
msgid "The following errors occured during import:"
|
||||
msgstr ""
|
||||
|
||||
#: src/importer/importer.py:339
|
||||
msgid "No new games found"
|
||||
msgstr ""
|
||||
|
||||
#: src/importer/importer.py:351
|
||||
msgid "1 game imported"
|
||||
msgstr ""
|
||||
|
||||
#. The variable is the number of games
|
||||
#: src/importer/importer.py:355
|
||||
msgid "{} games imported"
|
||||
msgstr ""
|
||||
|
||||
#. A single game removed
|
||||
#: src/importer/importer.py:359
|
||||
msgid "1 removed"
|
||||
msgstr ""
|
||||
|
||||
#. The variable is the name of the source
|
||||
#: src/importer/sources/location.py:33
|
||||
msgid "Select the {} cache directory."
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import os
|
||||
from time import time
|
||||
from typing import Any
|
||||
|
||||
from gi.repository import Adw, Gio, GLib, Gtk
|
||||
from PIL import Image
|
||||
@@ -52,16 +53,15 @@ class DetailsWindow(Adw.Window):
|
||||
|
||||
apply_button = Gtk.Template.Child()
|
||||
|
||||
cover_changed = False
|
||||
cover_changed: bool = False
|
||||
|
||||
def __init__(self, game=None, **kwargs):
|
||||
def __init__(self, game: Game | None = None, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.win = shared.win
|
||||
self.game = game
|
||||
self.game_cover = GameCover({self.cover})
|
||||
self.game: Game = game
|
||||
self.game_cover: GameCover = GameCover({self.cover})
|
||||
|
||||
self.set_transient_for(self.win)
|
||||
self.set_transient_for(shared.win)
|
||||
|
||||
if self.game:
|
||||
self.set_title(_("Game Details"))
|
||||
@@ -114,7 +114,7 @@ class DetailsWindow(Adw.Window):
|
||||
|
||||
self.exec_info_label.set_label(exec_info_text)
|
||||
|
||||
def clear_info_selection(*_args):
|
||||
def clear_info_selection(*_args: Any) -> None:
|
||||
self.exec_info_label.select_region(-1, -1)
|
||||
|
||||
self.exec_info_popover.connect("show", clear_info_selection)
|
||||
@@ -130,13 +130,13 @@ class DetailsWindow(Adw.Window):
|
||||
self.set_focus(self.name)
|
||||
self.present()
|
||||
|
||||
def delete_pixbuf(self, *_args):
|
||||
def delete_pixbuf(self, *_args: Any) -> None:
|
||||
self.game_cover.new_cover()
|
||||
|
||||
self.cover_button_delete_revealer.set_reveal_child(False)
|
||||
self.cover_changed = True
|
||||
|
||||
def apply_preferences(self, *_args):
|
||||
def apply_preferences(self, *_args: Any) -> None:
|
||||
final_name = self.name.get_text()
|
||||
final_developer = self.developer.get_text()
|
||||
final_executable = self.executable.get_text()
|
||||
@@ -202,10 +202,10 @@ class DetailsWindow(Adw.Window):
|
||||
self.game.developer = final_developer or None
|
||||
self.game.executable = final_executable
|
||||
|
||||
if self.game.game_id in self.win.game_covers.keys():
|
||||
self.win.game_covers[self.game.game_id].animation = None
|
||||
if self.game.game_id in shared.win.game_covers.keys():
|
||||
shared.win.game_covers[self.game.game_id].animation = None
|
||||
|
||||
self.win.game_covers[self.game.game_id] = self.game_cover
|
||||
shared.win.game_covers[self.game.game_id] = self.game_cover
|
||||
|
||||
if self.cover_changed:
|
||||
save_cover(
|
||||
@@ -228,9 +228,9 @@ class DetailsWindow(Adw.Window):
|
||||
self.game_cover.pictures.remove(self.cover)
|
||||
|
||||
self.close()
|
||||
self.win.show_details_page(self.game)
|
||||
shared.win.show_details_page(self.game)
|
||||
|
||||
def update_cover_callback(self, manager: SGDBManager):
|
||||
def update_cover_callback(self, manager: SGDBManager) -> None:
|
||||
# Set the game as not loading
|
||||
self.game.set_loading(-1)
|
||||
self.game.update()
|
||||
@@ -247,25 +247,25 @@ class DetailsWindow(Adw.Window):
|
||||
_("Preferences"),
|
||||
).connect("response", self.update_cover_error_response)
|
||||
|
||||
def update_cover_error_response(self, _widget, response):
|
||||
def update_cover_error_response(self, _widget: Any, response: str) -> None:
|
||||
if response == "open_preferences":
|
||||
shared.win.get_application().on_preferences_action(page_name="sgdb")
|
||||
|
||||
def focus_executable(self, *_args):
|
||||
def focus_executable(self, *_args: Any) -> None:
|
||||
self.set_focus(self.executable)
|
||||
|
||||
def toggle_loading(self):
|
||||
def toggle_loading(self) -> None:
|
||||
self.apply_button.set_sensitive(not self.apply_button.get_sensitive())
|
||||
self.spinner.set_spinning(not self.spinner.get_spinning())
|
||||
self.cover_overlay.set_opacity(not self.cover_overlay.get_opacity())
|
||||
|
||||
def set_cover(self, _source, result, *_args):
|
||||
def set_cover(self, _source: Any, result: Gio.Task, *_args: Any) -> None:
|
||||
try:
|
||||
path = self.file_dialog.open_finish(result).get_path()
|
||||
except GLib.GError:
|
||||
return
|
||||
|
||||
def resize():
|
||||
def resize() -> None:
|
||||
if cover := resize_cover(path):
|
||||
self.game_cover.new_cover(cover)
|
||||
self.cover_button_delete_revealer.set_reveal_child(True)
|
||||
@@ -275,5 +275,5 @@ class DetailsWindow(Adw.Window):
|
||||
self.toggle_loading()
|
||||
GLib.Thread.new(None, resize)
|
||||
|
||||
def choose_cover(self, *_args):
|
||||
def choose_cover(self, *_args: Any) -> None:
|
||||
self.file_dialog.open(self, None, self.set_cover)
|
||||
|
||||
77
src/game.py
77
src/game.py
@@ -23,10 +23,12 @@ import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import Any
|
||||
|
||||
from gi.repository import Adw, GObject, Gtk
|
||||
|
||||
from src import shared
|
||||
from src.game_cover import GameCover
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
@@ -45,23 +47,23 @@ class Game(Gtk.Box):
|
||||
game_options = Gtk.Template.Child()
|
||||
hidden_game_options = Gtk.Template.Child()
|
||||
|
||||
loading = 0
|
||||
filtered = False
|
||||
loading: int = 0
|
||||
filtered: bool = False
|
||||
|
||||
added = None
|
||||
executable = None
|
||||
game_id = None
|
||||
source = None
|
||||
hidden = False
|
||||
last_played = 0
|
||||
name = None
|
||||
developer = None
|
||||
removed = False
|
||||
blacklisted = False
|
||||
game_cover = None
|
||||
version = 0
|
||||
added: int
|
||||
executable: str
|
||||
game_id: str
|
||||
source: str
|
||||
hidden: bool = False
|
||||
last_played: int = 0
|
||||
name: str
|
||||
developer: str | None = None
|
||||
removed: bool = False
|
||||
blacklisted: bool = False
|
||||
game_cover: GameCover = None
|
||||
version: int = 0
|
||||
|
||||
def __init__(self, data, **kwargs):
|
||||
def __init__(self, data: dict[str, Any], **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.win = shared.win
|
||||
@@ -82,20 +84,20 @@ class Game(Gtk.Box):
|
||||
|
||||
shared.schema.connect("changed", self.schema_changed)
|
||||
|
||||
def update_values(self, data):
|
||||
def update_values(self, data: dict[str, Any]) -> None:
|
||||
for key, value in data.items():
|
||||
# Convert executables to strings
|
||||
if key == "executable" and isinstance(value, list):
|
||||
value = shlex.join(value)
|
||||
setattr(self, key, value)
|
||||
|
||||
def update(self):
|
||||
def update(self) -> None:
|
||||
self.emit("update-ready", {})
|
||||
|
||||
def save(self):
|
||||
def save(self) -> None:
|
||||
self.emit("save-ready", {})
|
||||
|
||||
def create_toast(self, title, action=None):
|
||||
def create_toast(self, title: str, action: str | None = None) -> None:
|
||||
toast = Adw.Toast.new(title.format(self.name))
|
||||
toast.set_priority(Adw.ToastPriority.HIGH)
|
||||
toast.set_use_markup(False)
|
||||
@@ -112,7 +114,7 @@ class Game(Gtk.Box):
|
||||
|
||||
self.win.toast_overlay.add_toast(toast)
|
||||
|
||||
def launch(self):
|
||||
def launch(self) -> None:
|
||||
self.last_played = int(time())
|
||||
self.save()
|
||||
self.update()
|
||||
@@ -130,7 +132,7 @@ class Game(Gtk.Box):
|
||||
cwd=Path.home(),
|
||||
shell=True,
|
||||
start_new_session=True,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0, # type: ignore
|
||||
)
|
||||
|
||||
if shared.schema.get_boolean("exit-after-launch"):
|
||||
@@ -139,7 +141,7 @@ class Game(Gtk.Box):
|
||||
# The variable is the title of the game
|
||||
self.create_toast(_("{} launched"))
|
||||
|
||||
def toggle_hidden(self, toast=True):
|
||||
def toggle_hidden(self, toast: bool = True) -> None:
|
||||
self.hidden = not self.hidden
|
||||
self.save()
|
||||
|
||||
@@ -155,7 +157,7 @@ class Game(Gtk.Box):
|
||||
"hide",
|
||||
)
|
||||
|
||||
def remove_game(self):
|
||||
def remove_game(self) -> None:
|
||||
# Add "removed=True" to the game properties so it can be deleted on next init
|
||||
self.removed = True
|
||||
self.save()
|
||||
@@ -164,53 +166,58 @@ class Game(Gtk.Box):
|
||||
if self.win.navigation_view.get_visible_page() == self.win.details_page:
|
||||
self.win.navigation_view.pop()
|
||||
|
||||
# The variable is the title of the game
|
||||
self.create_toast(_("{} removed").format(self.name), "remove")
|
||||
self.create_toast(
|
||||
# The variable is the title of the game
|
||||
_("{} removed").format(GLib.markup_escape_text(self.name)),
|
||||
"remove",
|
||||
)
|
||||
|
||||
def set_loading(self, state):
|
||||
def set_loading(self, state: int) -> None:
|
||||
self.loading += state
|
||||
loading = self.loading > 0
|
||||
|
||||
self.cover.set_opacity(int(not loading))
|
||||
self.spinner.set_spinning(loading)
|
||||
|
||||
def get_cover_path(self):
|
||||
def get_cover_path(self) -> Path | None:
|
||||
cover_path = shared.covers_dir / f"{self.game_id}.gif"
|
||||
if cover_path.is_file():
|
||||
return cover_path
|
||||
return cover_path # type: ignore
|
||||
|
||||
cover_path = shared.covers_dir / f"{self.game_id}.tiff"
|
||||
if cover_path.is_file():
|
||||
return cover_path
|
||||
return cover_path # type: ignore
|
||||
|
||||
return None
|
||||
|
||||
def toggle_play(self, _widget, _prop1, _prop2, state=True):
|
||||
def toggle_play(
|
||||
self, _widget: Any, _prop1: Any, _prop2: Any, state: bool = True
|
||||
) -> None:
|
||||
if not self.menu_button.get_active():
|
||||
self.play_revealer.set_reveal_child(not state)
|
||||
self.menu_revealer.set_reveal_child(not state)
|
||||
|
||||
def main_button_clicked(self, _widget, button):
|
||||
def main_button_clicked(self, _widget: Any, button: bool) -> None:
|
||||
if shared.schema.get_boolean("cover-launches-game") ^ button:
|
||||
self.launch()
|
||||
else:
|
||||
self.win.show_details_page(self)
|
||||
|
||||
def set_play_icon(self):
|
||||
def set_play_icon(self) -> None:
|
||||
self.play_button.set_icon_name(
|
||||
"help-about-symbolic"
|
||||
if shared.schema.get_boolean("cover-launches-game")
|
||||
else "media-playback-start-symbolic"
|
||||
)
|
||||
|
||||
def schema_changed(self, _settings, key):
|
||||
def schema_changed(self, _settings: Any, key: str) -> None:
|
||||
if key == "cover-launches-game":
|
||||
self.set_play_icon()
|
||||
|
||||
@GObject.Signal(name="update-ready", arg_types=[object])
|
||||
def update_ready(self, _additional_data) -> None:
|
||||
def update_ready(self, _additional_data): # type: ignore
|
||||
"""Signal emitted when the game needs updating"""
|
||||
|
||||
@GObject.Signal(name="save-ready", arg_types=[object])
|
||||
def save_ready(self, _additional_data) -> None:
|
||||
def save_ready(self, _additional_data): # type: ignore
|
||||
"""Signal emitted when the game needs saving"""
|
||||
|
||||
@@ -17,19 +17,22 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk
|
||||
from PIL import Image, ImageFilter, ImageStat
|
||||
|
||||
from src import shared
|
||||
|
||||
|
||||
class GameCover:
|
||||
texture = None
|
||||
blurred = None
|
||||
luminance = None
|
||||
path = None
|
||||
animation = None
|
||||
anim_iter = None
|
||||
texture: Gdk.Texture | None
|
||||
blurred: Gdk.Texture | None
|
||||
luminance: tuple[float, float] | None
|
||||
path: Path | None
|
||||
animation: GdkPixbuf.PixbufAnimation | None
|
||||
anim_iter: GdkPixbuf.PixbufAnimationIter | None
|
||||
|
||||
placeholder = Gdk.Texture.new_from_resource(
|
||||
shared.PREFIX + "/library_placeholder.svg"
|
||||
@@ -38,21 +41,21 @@ class GameCover:
|
||||
shared.PREFIX + "/library_placeholder_small.svg"
|
||||
)
|
||||
|
||||
def __init__(self, pictures, path=None):
|
||||
def __init__(self, pictures: set[Gtk.Picture], path: Path | None = None) -> None:
|
||||
self.pictures = pictures
|
||||
self.new_cover(path)
|
||||
|
||||
# Wrap the function in another one as Gio.Task.run_in_thread does not allow for passing args
|
||||
def create_func(self, path):
|
||||
def create_func(self, path: Path | None) -> Callable:
|
||||
self.animation = GdkPixbuf.PixbufAnimation.new_from_file(str(path))
|
||||
self.anim_iter = self.animation.get_iter()
|
||||
|
||||
def wrapper(task, *_args):
|
||||
def wrapper(task: Gio.Task, *_args: Any) -> None:
|
||||
self.update_animation((task, self.animation))
|
||||
|
||||
return wrapper
|
||||
|
||||
def new_cover(self, path=None):
|
||||
def new_cover(self, path: Path | None = None) -> None:
|
||||
self.animation = None
|
||||
self.texture = None
|
||||
self.blurred = None
|
||||
@@ -69,14 +72,14 @@ class GameCover:
|
||||
if not self.animation:
|
||||
self.set_texture(self.texture)
|
||||
|
||||
def get_texture(self):
|
||||
def get_texture(self) -> Gdk.Texture:
|
||||
return (
|
||||
Gdk.Texture.new_for_pixbuf(self.animation.get_static_image())
|
||||
if self.animation
|
||||
else self.texture
|
||||
)
|
||||
|
||||
def get_blurred(self):
|
||||
def get_blurred(self) -> Gdk.Texture:
|
||||
if not self.blurred:
|
||||
if self.path:
|
||||
with Image.open(self.path) as image:
|
||||
@@ -94,24 +97,24 @@ class GameCover:
|
||||
stat = ImageStat.Stat(image.convert("L"))
|
||||
|
||||
# Luminance values for light and dark mode
|
||||
self.luminance = [
|
||||
self.luminance = (
|
||||
min((stat.mean[0] + stat.extrema[0][0]) / 510, 0.7),
|
||||
max((stat.mean[0] + stat.extrema[0][1]) / 510, 0.3),
|
||||
]
|
||||
)
|
||||
else:
|
||||
self.blurred = self.placeholder_small
|
||||
self.luminance = (0.3, 0.5)
|
||||
|
||||
return self.blurred
|
||||
|
||||
def add_picture(self, picture):
|
||||
def add_picture(self, picture: Gtk.Picture) -> None:
|
||||
self.pictures.add(picture)
|
||||
if not self.animation:
|
||||
self.set_texture(self.texture)
|
||||
else:
|
||||
self.update_animation((self.task, self.animation))
|
||||
|
||||
def set_texture(self, texture):
|
||||
def set_texture(self, texture: Gdk.Texture) -> None:
|
||||
self.pictures.discard(
|
||||
picture for picture in self.pictures if not picture.is_visible()
|
||||
)
|
||||
@@ -121,13 +124,13 @@ class GameCover:
|
||||
for picture in self.pictures:
|
||||
picture.set_paintable(texture or self.placeholder)
|
||||
|
||||
def update_animation(self, data):
|
||||
def update_animation(self, data: GdkPixbuf.PixbufAnimation) -> None:
|
||||
if self.animation == data[1]:
|
||||
self.anim_iter.advance()
|
||||
self.anim_iter.advance() # type: ignore
|
||||
|
||||
self.set_texture(Gdk.Texture.new_for_pixbuf(self.anim_iter.get_pixbuf()))
|
||||
self.set_texture(Gdk.Texture.new_for_pixbuf(self.anim_iter.get_pixbuf())) # type: ignore
|
||||
|
||||
delay_time = self.anim_iter.get_delay_time()
|
||||
delay_time = self.anim_iter.get_delay_time() # type: ignore
|
||||
GLib.timeout_add(
|
||||
20 if delay_time < 20 else delay_time,
|
||||
self.update_animation,
|
||||
|
||||
@@ -49,8 +49,16 @@ class Importer(ErrorProducer):
|
||||
n_pipelines_done: int = 0
|
||||
game_pipelines: set[Pipeline] = None
|
||||
|
||||
removed_game_ids: set[str] = set()
|
||||
imported_game_ids: set[str] = set()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# TODO: make this stateful
|
||||
shared.store.new_game_ids = set()
|
||||
shared.store.duplicate_game_ids = set()
|
||||
|
||||
self.game_pipelines = set()
|
||||
self.sources = set()
|
||||
|
||||
@@ -218,9 +226,37 @@ class Importer(ErrorProducer):
|
||||
if self.finished:
|
||||
self.import_callback()
|
||||
|
||||
def remove_games(self):
|
||||
"""Set removed to True for missing games"""
|
||||
if not shared.schema.get_boolean("remove-missing"):
|
||||
return
|
||||
|
||||
for game in shared.store:
|
||||
if game.removed:
|
||||
continue
|
||||
if game.source == "imported":
|
||||
continue
|
||||
if not shared.schema.get_boolean(game.base_source):
|
||||
continue
|
||||
if game.game_id in shared.store.duplicate_game_ids:
|
||||
continue
|
||||
if game.game_id in shared.store.new_game_ids:
|
||||
continue
|
||||
|
||||
logging.debug("Removing missing game %s (%s)", game.name, game.game_id)
|
||||
|
||||
game.removed = True
|
||||
game.save()
|
||||
game.update()
|
||||
self.removed_game_ids.add(game.game_id)
|
||||
|
||||
def import_callback(self):
|
||||
"""Callback called when importing has finished"""
|
||||
logging.info("Import done")
|
||||
self.remove_games()
|
||||
self.imported_game_ids = shared.store.new_game_ids
|
||||
shared.store.new_game_ids = set()
|
||||
shared.store.duplicate_game_ids = set()
|
||||
self.import_dialog.close()
|
||||
self.summary_toast = self.create_summary_toast()
|
||||
self.create_error_dialog()
|
||||
@@ -280,27 +316,60 @@ class Importer(ErrorProducer):
|
||||
|
||||
dialog.present()
|
||||
|
||||
def undo_import(self, *_args):
|
||||
for game_id in self.imported_game_ids:
|
||||
shared.store[game_id].removed = True
|
||||
shared.store[game_id].update()
|
||||
shared.store[game_id].save()
|
||||
|
||||
for game_id in self.removed_game_ids:
|
||||
shared.store[game_id].removed = False
|
||||
shared.store[game_id].update()
|
||||
shared.store[game_id].save()
|
||||
|
||||
self.imported_game_ids = set()
|
||||
self.removed_game_ids = set()
|
||||
self.summary_toast.dismiss()
|
||||
|
||||
logging.info("Import undone")
|
||||
|
||||
def create_summary_toast(self):
|
||||
"""N games imported toast"""
|
||||
"""N games imported, removed toast"""
|
||||
|
||||
toast = Adw.Toast(timeout=0, priority=Adw.ToastPriority.HIGH)
|
||||
|
||||
if self.n_games_added == 0:
|
||||
toast.set_title(_("No new games found"))
|
||||
toast.set_button_label(_("Preferences"))
|
||||
toast.connect(
|
||||
"button-clicked",
|
||||
self.dialog_response_callback,
|
||||
"open_preferences",
|
||||
"import",
|
||||
)
|
||||
if not self.n_games_added:
|
||||
toast_title = _("No new games found")
|
||||
|
||||
if not self.removed_game_ids:
|
||||
toast.set_button_label(_("Preferences"))
|
||||
toast.connect(
|
||||
"button-clicked",
|
||||
self.dialog_response_callback,
|
||||
"open_preferences",
|
||||
"import",
|
||||
)
|
||||
|
||||
elif self.n_games_added == 1:
|
||||
toast.set_title(_("1 game imported"))
|
||||
toast_title = _("1 game imported")
|
||||
|
||||
elif self.n_games_added > 1:
|
||||
# The variable is the number of games
|
||||
toast.set_title(_("{} games imported").format(self.n_games_added))
|
||||
toast_title = _("{} games imported").format(self.n_games_added)
|
||||
|
||||
if (removed_length := len(self.removed_game_ids)) == 1:
|
||||
# A single game removed
|
||||
toast_title += ", " + _("1 removed")
|
||||
|
||||
elif removed_length > 1:
|
||||
# The variable is the number of games removed
|
||||
toast_title += ", " + _("{} removed").format(removed_length)
|
||||
|
||||
if self.n_games_added or self.removed_game_ids:
|
||||
toast.set_button_label(_("Undo"))
|
||||
toast.connect("button-clicked", self.undo_import)
|
||||
|
||||
toast.set_title(toast_title)
|
||||
|
||||
shared.win.toast_overlay.add_toast(toast)
|
||||
return toast
|
||||
|
||||
@@ -90,18 +90,22 @@ class BottlesSource(URLExecutableSource):
|
||||
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
|
||||
available_on = {"linux"}
|
||||
|
||||
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,
|
||||
locations: BottlesLocations
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.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,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -125,17 +125,21 @@ class FlatpakSource(Source):
|
||||
executable_format = "flatpak run {flatpak_id}"
|
||||
available_on = {"linux"}
|
||||
|
||||
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,
|
||||
locations: FlatpakLocations
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.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,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
import json
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from functools import cached_property
|
||||
from hashlib import sha256
|
||||
from json import JSONDecodeError
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import Iterable, NamedTuple, Optional, TypedDict
|
||||
from functools import cached_property
|
||||
|
||||
from src import shared
|
||||
from src.game import Game
|
||||
@@ -108,7 +108,9 @@ class SubSourceIterable(Iterable):
|
||||
"game_id": self.source.game_id_format.format(
|
||||
service=self.service, game_id=app_name
|
||||
),
|
||||
"executable": self.source.executable_format.format(runner=runner, 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)
|
||||
@@ -363,27 +365,31 @@ class HeroicSource(URLExecutableSource):
|
||||
url_format = "heroic://launch/{runner}/{app_name}"
|
||||
available_on = {"linux", "win32"}
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
locations: HeroicLocations
|
||||
|
||||
@property
|
||||
def game_id_format(self) -> str:
|
||||
"""The string format used to construct game IDs"""
|
||||
return self.source_id + "_{service}_{game_id}"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -86,18 +86,22 @@ class ItchSource(URLExecutableSource):
|
||||
url_format = "itch://caves/{cave_id}/launch"
|
||||
available_on = {"linux", "win32"}
|
||||
|
||||
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,
|
||||
locations: ItchLocations
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.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,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ from typing import NamedTuple
|
||||
from src import shared
|
||||
from src.game import Game
|
||||
from src.importer.sources.location import Location, LocationSubPath
|
||||
from src.importer.sources.source import Source, SourceIterationResult, SourceIterable
|
||||
from src.importer.sources.source import Source, SourceIterable, SourceIterationResult
|
||||
|
||||
|
||||
class LegendarySourceIterable(SourceIterable):
|
||||
@@ -100,17 +100,21 @@ class LegendarySource(Source):
|
||||
available_on = {"linux"}
|
||||
iterable_class = LegendarySourceIterable
|
||||
|
||||
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,
|
||||
locations: LegendaryLocations
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.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,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Mapping, Iterable, NamedTuple
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Mapping, NamedTuple
|
||||
|
||||
from src import shared
|
||||
|
||||
@@ -70,7 +70,7 @@ class Location:
|
||||
|
||||
def resolve(self) -> None:
|
||||
"""Choose a root path from the candidates for the location.
|
||||
If none fits, raise a UnresolvableLocationError"""
|
||||
If none fits, raise an UnresolvableLocationError"""
|
||||
|
||||
if self.root is not None:
|
||||
return
|
||||
|
||||
@@ -100,33 +100,37 @@ class LutrisSource(URLExecutableSource):
|
||||
|
||||
# FIXME possible bug: config picks ~/.var... and cache picks ~/.local...
|
||||
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
)
|
||||
locations: LutrisLocations
|
||||
|
||||
@property
|
||||
def game_id_format(self):
|
||||
return self.source_id + "_{runner}_{game_id}"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -145,28 +145,7 @@ class RetroarchSource(Source):
|
||||
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,
|
||||
)
|
||||
)
|
||||
locations: RetroarchLocations
|
||||
|
||||
@property
|
||||
def executable_format(self):
|
||||
@@ -176,15 +155,6 @@ class RetroarchSource(Source):
|
||||
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
|
||||
@@ -214,3 +184,37 @@ class RetroarchSource(Source):
|
||||
return Path(f"{library_path}/steamapps/common/RetroArch")
|
||||
# Not found
|
||||
raise ValueError("RetroArch not found in Steam library")
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.locations = RetroarchLocations(
|
||||
Location(
|
||||
schema_key="retroarch-location",
|
||||
candidates=[
|
||||
shared.flatpak_dir
|
||||
/ "org.libretro.RetroArch"
|
||||
/ "config"
|
||||
/ "retroarch",
|
||||
shared.config_dir / "retroarch",
|
||||
shared.home / ".config" / "retroarch",
|
||||
# TODO: Windows support, waiting for executable path setting improvement
|
||||
# Path("C:\\RetroArch-Win64"),
|
||||
# Path("C:\\RetroArch-Win32"),
|
||||
# TODO: UWP support (URL handler - https://github.com/libretro/RetroArch/pull/13563)
|
||||
# shared.local_appdata_dir
|
||||
# / "Packages"
|
||||
# / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma"
|
||||
# / "LocalState",
|
||||
],
|
||||
paths={
|
||||
"retroarch.cfg": LocationSubPath("retroarch.cfg"),
|
||||
},
|
||||
invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE,
|
||||
)
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -56,6 +56,9 @@ class Source(Iterable):
|
||||
variant: str = None
|
||||
available_on: set[str] = set()
|
||||
iterable_class: type[SourceIterable]
|
||||
|
||||
# NOTE: Locations must be set at __init__ time, not in the class definition.
|
||||
# They must not be shared between source instances.
|
||||
locations: Collection[Location]
|
||||
|
||||
@property
|
||||
@@ -85,10 +88,7 @@ class Source(Iterable):
|
||||
Get an iterator for the source
|
||||
:raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable
|
||||
"""
|
||||
for location_name in ("data", "cache", "config"):
|
||||
location = getattr(self, f"{location_name}_location", None)
|
||||
if location is None:
|
||||
continue
|
||||
for location in self.locations:
|
||||
location.resolve()
|
||||
return iter(self.iterable_class(self))
|
||||
|
||||
|
||||
@@ -120,19 +120,25 @@ class SteamSource(URLExecutableSource):
|
||||
iterable_class = SteamSourceIterable
|
||||
url_format = "steam://rungameid/{game_id}"
|
||||
|
||||
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,
|
||||
locations: SteamLocations
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.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,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
78
src/main.py
78
src/main.py
@@ -21,6 +21,7 @@ import json
|
||||
import lzma
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import gi
|
||||
|
||||
@@ -56,15 +57,15 @@ from src.window import CartridgesWindow
|
||||
|
||||
class CartridgesApplication(Adw.Application):
|
||||
state = shared.AppState.DEFAULT
|
||||
win = None
|
||||
win: CartridgesWindow
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
shared.store = Store()
|
||||
super().__init__(
|
||||
application_id=shared.APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE
|
||||
)
|
||||
|
||||
def do_activate(self): # pylint: disable=arguments-differ
|
||||
def do_activate(self) -> None: # pylint: disable=arguments-differ
|
||||
"""Called on app creation"""
|
||||
|
||||
setup_logging()
|
||||
@@ -145,14 +146,17 @@ class CartridgesApplication(Adw.Application):
|
||||
|
||||
self.win.present()
|
||||
|
||||
def load_games_from_disk(self):
|
||||
def load_games_from_disk(self) -> None:
|
||||
if shared.games_dir.is_dir():
|
||||
for game_file in shared.games_dir.iterdir():
|
||||
data = json.load(game_file.open())
|
||||
try:
|
||||
data = json.load(game_file.open())
|
||||
except (OSError, json.decoder.JSONDecodeError):
|
||||
continue
|
||||
game = Game(data)
|
||||
shared.store.add_game(game, {"skip_save": True})
|
||||
|
||||
def get_source_name(self, source_id):
|
||||
def get_source_name(self, source_id: str):
|
||||
if source_id == "all":
|
||||
name = _("All Games")
|
||||
elif source_id == "imported":
|
||||
@@ -161,7 +165,7 @@ class CartridgesApplication(Adw.Application):
|
||||
name = globals()[f'{source_id.split("_")[0].title()}Source'].name
|
||||
return name
|
||||
|
||||
def on_about_action(self, *_args):
|
||||
def on_about_action(self, *_args: Any) -> None:
|
||||
# Get the debug info from the log files
|
||||
debug_str = ""
|
||||
for i, path in enumerate(shared.log_files):
|
||||
@@ -207,8 +211,12 @@ class CartridgesApplication(Adw.Application):
|
||||
about.present()
|
||||
|
||||
def on_preferences_action(
|
||||
self, _action=None, _parameter=None, page_name=None, expander_row=None
|
||||
):
|
||||
self,
|
||||
_action: Any = None,
|
||||
_parameter: Any = None,
|
||||
page_name: (str | None) = None,
|
||||
expander_row: (str | None) = None,
|
||||
) -> CartridgesWindow:
|
||||
win = PreferencesWindow()
|
||||
if page_name:
|
||||
win.set_visible_page_name(page_name)
|
||||
@@ -218,76 +226,76 @@ class CartridgesApplication(Adw.Application):
|
||||
|
||||
return win
|
||||
|
||||
def on_launch_game_action(self, *_args):
|
||||
def on_launch_game_action(self, *_args: Any) -> None:
|
||||
self.win.active_game.launch()
|
||||
|
||||
def on_hide_game_action(self, *_args):
|
||||
def on_hide_game_action(self, *_args: Any) -> None:
|
||||
self.win.active_game.toggle_hidden()
|
||||
|
||||
def on_edit_game_action(self, *_args):
|
||||
def on_edit_game_action(self, *_args: Any) -> None:
|
||||
DetailsWindow(self.win.active_game)
|
||||
|
||||
def on_add_game_action(self, *_args):
|
||||
def on_add_game_action(self, *_args: Any) -> None:
|
||||
DetailsWindow()
|
||||
|
||||
def on_import_action(self, *_args):
|
||||
importer = Importer()
|
||||
def on_import_action(self, *_args: Any) -> None:
|
||||
shared.importer = Importer()
|
||||
|
||||
if shared.schema.get_boolean("lutris"):
|
||||
importer.add_source(LutrisSource())
|
||||
shared.importer.add_source(LutrisSource())
|
||||
|
||||
if shared.schema.get_boolean("steam"):
|
||||
importer.add_source(SteamSource())
|
||||
shared.importer.add_source(SteamSource())
|
||||
|
||||
if shared.schema.get_boolean("heroic"):
|
||||
importer.add_source(HeroicSource())
|
||||
shared.importer.add_source(HeroicSource())
|
||||
|
||||
if shared.schema.get_boolean("bottles"):
|
||||
importer.add_source(BottlesSource())
|
||||
shared.importer.add_source(BottlesSource())
|
||||
|
||||
if shared.schema.get_boolean("flatpak"):
|
||||
importer.add_source(FlatpakSource())
|
||||
shared.importer.add_source(FlatpakSource())
|
||||
|
||||
if shared.schema.get_boolean("itch"):
|
||||
importer.add_source(ItchSource())
|
||||
shared.importer.add_source(ItchSource())
|
||||
|
||||
if shared.schema.get_boolean("legendary"):
|
||||
importer.add_source(LegendarySource())
|
||||
shared.importer.add_source(LegendarySource())
|
||||
|
||||
if shared.schema.get_boolean("retroarch"):
|
||||
importer.add_source(RetroarchSource())
|
||||
shared.importer.add_source(RetroarchSource())
|
||||
|
||||
importer.run()
|
||||
shared.importer.run()
|
||||
|
||||
def on_remove_game_action(self, *_args):
|
||||
def on_remove_game_action(self, *_args: Any) -> None:
|
||||
self.win.active_game.remove_game()
|
||||
|
||||
def on_remove_game_details_view_action(self, *_args):
|
||||
def on_remove_game_details_view_action(self, *_args: Any) -> None:
|
||||
if self.win.navigation_view.get_visible_page() == self.win.details_page:
|
||||
self.on_remove_game_action()
|
||||
|
||||
def search(self, uri):
|
||||
def search(self, uri: str) -> None:
|
||||
Gio.AppInfo.launch_default_for_uri(f"{uri}{self.win.active_game.name}")
|
||||
|
||||
def on_igdb_search_action(self, *_args):
|
||||
def on_igdb_search_action(self, *_args: Any) -> None:
|
||||
self.search("https://www.igdb.com/search?type=1&q=")
|
||||
|
||||
def on_sgdb_search_action(self, *_args):
|
||||
def on_sgdb_search_action(self, *_args: Any) -> None:
|
||||
self.search("https://www.steamgriddb.com/search/grids?term=")
|
||||
|
||||
def on_protondb_search_action(self, *_args):
|
||||
def on_protondb_search_action(self, *_args: Any) -> None:
|
||||
self.search("https://www.protondb.com/search?q=")
|
||||
|
||||
def on_lutris_search_action(self, *_args):
|
||||
def on_lutris_search_action(self, *_args: Any) -> None:
|
||||
self.search("https://lutris.net/games?q=")
|
||||
|
||||
def on_hltb_search_action(self, *_args):
|
||||
def on_hltb_search_action(self, *_args: Any) -> None:
|
||||
self.search("https://howlongtobeat.com/?q=")
|
||||
|
||||
def on_quit_action(self, *_args):
|
||||
def on_quit_action(self, *_args: Any) -> None:
|
||||
self.quit()
|
||||
|
||||
def create_actions(self, actions):
|
||||
def create_actions(self, actions: set) -> None:
|
||||
for action in actions:
|
||||
simple_action = Gio.SimpleAction.new(action[0], None)
|
||||
|
||||
@@ -303,7 +311,7 @@ class CartridgesApplication(Adw.Application):
|
||||
scope.add_action(simple_action)
|
||||
|
||||
|
||||
def main(_version):
|
||||
def main(_version: int) -> Any:
|
||||
"""App entry point"""
|
||||
app = CartridgesApplication()
|
||||
return app.run(sys.argv)
|
||||
|
||||
@@ -21,10 +21,12 @@ import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from typing import Any, Callable
|
||||
|
||||
from gi.repository import Adw, Gio, GLib, Gtk
|
||||
|
||||
from src import shared
|
||||
from src.game import Game
|
||||
from src.importer.sources.bottles_source import BottlesSource
|
||||
from src.importer.sources.flatpak_source import FlatpakSource
|
||||
from src.importer.sources.heroic_source import HeroicSource
|
||||
@@ -52,6 +54,8 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
cover_launches_game_switch = Gtk.Template.Child()
|
||||
high_quality_images_switch = Gtk.Template.Child()
|
||||
|
||||
remove_missing_switch = Gtk.Template.Child()
|
||||
|
||||
steam_expander_row = Gtk.Template.Child()
|
||||
steam_data_action_row = Gtk.Template.Child()
|
||||
steam_data_file_chooser_button = Gtk.Template.Child()
|
||||
@@ -104,10 +108,10 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
reset_button = Gtk.Template.Child()
|
||||
remove_all_games_button = Gtk.Template.Child()
|
||||
|
||||
removed_games = set()
|
||||
warning_menu_buttons = {}
|
||||
removed_games: set[Game] = set()
|
||||
warning_menu_buttons: dict = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.win = shared.win
|
||||
self.file_chooser = Gtk.FileDialog()
|
||||
@@ -153,7 +157,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
self.init_source_row(source)
|
||||
|
||||
# SteamGridDB
|
||||
def sgdb_key_changed(*_args):
|
||||
def sgdb_key_changed(*_args: Any) -> None:
|
||||
shared.schema.set_string("sgdb-key", self.sgdb_key_entry_row.get_text())
|
||||
|
||||
self.sgdb_key_entry_row.set_text(shared.schema.get_string("sgdb-key"))
|
||||
@@ -169,10 +173,11 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
|
||||
# Switches
|
||||
self.bind_switches(
|
||||
(
|
||||
{
|
||||
"exit-after-launch",
|
||||
"cover-launches-game",
|
||||
"high-quality-images",
|
||||
"remove-missing",
|
||||
"lutris-import-steam",
|
||||
"lutris-import-flatpak",
|
||||
"heroic-import-epic",
|
||||
@@ -183,10 +188,10 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
"sgdb",
|
||||
"sgdb-prefer",
|
||||
"sgdb-animated",
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
def set_sgdb_sensitive(widget):
|
||||
def set_sgdb_sensitive(widget: Adw.EntryRow) -> None:
|
||||
if not widget.get_text():
|
||||
shared.schema.set_boolean("sgdb", False)
|
||||
|
||||
@@ -195,10 +200,10 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
self.sgdb_key_entry_row.connect("changed", set_sgdb_sensitive)
|
||||
set_sgdb_sensitive(self.sgdb_key_entry_row)
|
||||
|
||||
def get_switch(self, setting):
|
||||
def get_switch(self, setting: str) -> Any:
|
||||
return getattr(self, f'{setting.replace("-", "_")}_switch')
|
||||
|
||||
def bind_switches(self, settings):
|
||||
def bind_switches(self, settings: set[str]) -> None:
|
||||
for setting in settings:
|
||||
shared.schema.bind(
|
||||
setting,
|
||||
@@ -207,10 +212,12 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
Gio.SettingsBindFlags.DEFAULT,
|
||||
)
|
||||
|
||||
def choose_folder(self, _widget, callback, callback_data=None):
|
||||
def choose_folder(
|
||||
self, _widget: Any, callback: Callable, callback_data: str | None = None
|
||||
) -> None:
|
||||
self.file_chooser.select_folder(self.win, None, callback, callback_data)
|
||||
|
||||
def undo_remove_all(self, *_args):
|
||||
def undo_remove_all(self, *_args: Any) -> None:
|
||||
shared.win.get_application().state = shared.AppState.UNDO_REMOVE_ALL_GAMES
|
||||
for game in self.removed_games:
|
||||
game.removed = False
|
||||
@@ -222,7 +229,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
shared.win.get_application().state = shared.AppState.DEFAULT
|
||||
shared.win.create_source_rows()
|
||||
|
||||
def remove_all_games(self, *_args):
|
||||
def remove_all_games(self, *_args: Any) -> None:
|
||||
shared.win.get_application().state = shared.AppState.REMOVE_ALL_GAMES
|
||||
for game in shared.store:
|
||||
if not game.removed:
|
||||
@@ -238,7 +245,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
shared.win.get_application().state = shared.AppState.DEFAULT
|
||||
shared.win.create_source_rows()
|
||||
|
||||
def reset_app(self, *_args):
|
||||
def reset_app(self, *_args: Any) -> None:
|
||||
rmtree(shared.data_dir / "cartridges", True)
|
||||
rmtree(shared.config_dir / "cartridges", True)
|
||||
rmtree(shared.cache_dir / "cartridges", True)
|
||||
@@ -256,28 +263,24 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
|
||||
shared.win.get_application().quit()
|
||||
|
||||
def update_source_action_row_paths(self, source):
|
||||
def update_source_action_row_paths(self, source: Source) -> None:
|
||||
"""Set the dir subtitle for a source's action rows"""
|
||||
for location in ("data", "config", "cache"):
|
||||
for location_name, location in source.locations._asdict().items():
|
||||
# Get the action row to subtitle
|
||||
action_row = getattr(
|
||||
self, f"{source.source_id}_{location}_action_row", None
|
||||
self, f"{source.source_id}_{location_name}_action_row", None
|
||||
)
|
||||
if not action_row:
|
||||
continue
|
||||
|
||||
infix = "-cache" if location == "cache" else ""
|
||||
key = f"{source.source_id}{infix}-location"
|
||||
path = Path(shared.schema.get_string(key)).expanduser()
|
||||
|
||||
path = Path(shared.schema.get_string(location.schema_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: Source):
|
||||
def resolve_locations(self, source: Source) -> None:
|
||||
"""Resolve locations and add a warning if location cannot be found"""
|
||||
|
||||
def clear_warning_selection(_widget, label):
|
||||
def clear_warning_selection(_widget: Any, label: Gtk.Label) -> None:
|
||||
label.select_region(-1, -1)
|
||||
|
||||
for location_name, location in source.locations._asdict().items():
|
||||
@@ -327,40 +330,30 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
action_row.add_prefix(menu_button)
|
||||
self.warning_menu_buttons[source.source_id] = menu_button
|
||||
|
||||
def init_source_row(self, source: Source):
|
||||
def init_source_row(self, source: Source) -> None:
|
||||
"""Initialize a preference row for a source class"""
|
||||
|
||||
def set_dir(_widget, result, location_name):
|
||||
def set_dir(_widget: Any, result: Gio.Task, location_name: str) -> None:
|
||||
"""Callback called when a dir picker button is clicked"""
|
||||
|
||||
try:
|
||||
path = Path(self.file_chooser.select_folder_finish(result).get_path())
|
||||
except GLib.GError:
|
||||
return
|
||||
|
||||
# Good picked location
|
||||
location = getattr(source.locations, location_name)
|
||||
location = source.locations._asdict()[location_name]
|
||||
if location.check_candidate(path):
|
||||
# Set the schema
|
||||
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
|
||||
shared.schema.set_string(location.schema_key, str(path))
|
||||
self.update_source_action_row_paths(source)
|
||||
|
||||
if self.warning_menu_buttons.get(source.source_id):
|
||||
action_row = getattr(
|
||||
self, f"{source.source_id}_{location_name}_action_row", None
|
||||
)
|
||||
action_row.remove(self.warning_menu_buttons[source.source_id])
|
||||
action_row.remove( # type: ignore
|
||||
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)
|
||||
logging.debug("User-set value for %s is %s", location.schema_key, path)
|
||||
|
||||
# Bad picked location, inform user
|
||||
else:
|
||||
@@ -373,7 +366,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
_("Set Location"),
|
||||
)
|
||||
|
||||
def on_response(widget, response):
|
||||
def on_response(widget: Any, response: str) -> None:
|
||||
if response == "choose_folder":
|
||||
self.choose_folder(widget, set_dir, location_name)
|
||||
|
||||
|
||||
@@ -106,5 +106,5 @@ class Pipeline(GObject.Object):
|
||||
self.advance()
|
||||
|
||||
@GObject.Signal(name="advanced")
|
||||
def advanced(self) -> None:
|
||||
def advanced(self): # type: ignore
|
||||
"""Signal emitted when the pipeline has advanced"""
|
||||
|
||||
@@ -33,12 +33,16 @@ class Store:
|
||||
pipeline_managers: set[Manager]
|
||||
pipelines: dict[str, Pipeline]
|
||||
source_games: MutableMapping[str, MutableMapping[str, Game]]
|
||||
new_game_ids: set[str]
|
||||
duplicate_game_ids: set[str]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.managers = {}
|
||||
self.pipeline_managers = set()
|
||||
self.pipelines = {}
|
||||
self.source_games = {}
|
||||
self.new_game_ids = set()
|
||||
self.duplicate_game_ids = set()
|
||||
|
||||
def __contains__(self, obj: object) -> bool:
|
||||
"""Check if the game is present in the store with the `in` keyword"""
|
||||
@@ -87,7 +91,7 @@ class Store:
|
||||
self.pipeline_managers.discard(self.managers[manager_type])
|
||||
|
||||
def cleanup_game(self, game: Game) -> None:
|
||||
"""Remove a game's files"""
|
||||
"""Remove a game's files, dismiss any loose toasts"""
|
||||
for path in (
|
||||
shared.games_dir / f"{game.game_id}.json",
|
||||
shared.covers_dir / f"{game.game_id}.tiff",
|
||||
@@ -95,6 +99,14 @@ class Store:
|
||||
):
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
# TODO: don't run this if the state is startup
|
||||
for undo in ("remove", "hide"):
|
||||
try:
|
||||
shared.win.toasts[(game, undo)].dismiss()
|
||||
shared.win.toasts.pop((game, undo))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def add_game(
|
||||
self, game: Game, additional_data: dict, run_pipeline=True
|
||||
) -> Pipeline | None:
|
||||
@@ -114,6 +126,7 @@ class Store:
|
||||
if not stored_game:
|
||||
# New game, do as normal
|
||||
logging.debug("New store game %s (%s)", game.name, game.game_id)
|
||||
self.new_game_ids.add(game.game_id)
|
||||
elif stored_game.removed:
|
||||
# Will replace a removed game, cleanup its remains
|
||||
logging.debug(
|
||||
@@ -122,9 +135,11 @@ class Store:
|
||||
game.game_id,
|
||||
)
|
||||
self.cleanup_game(stored_game)
|
||||
self.new_game_ids.add(game.game_id)
|
||||
else:
|
||||
# Duplicate game, ignore it
|
||||
logging.debug("Duplicate store game %s (%s)", game.name, game.game_id)
|
||||
self.duplicate_game_ids.add(game.game_id)
|
||||
return None
|
||||
|
||||
# Connect signals
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# check_install.py
|
||||
#
|
||||
# Copyright 2022-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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# TODO delegate to the sources
|
||||
def check_install(check, locations, setting=None, subdirs=(Path(),)):
|
||||
for location in locations:
|
||||
for subdir in (Path(),) + subdirs:
|
||||
if (location / subdir / check).exists():
|
||||
if setting:
|
||||
setting[0].set_string(setting[1], str(location / subdir))
|
||||
return location / subdir
|
||||
|
||||
return False
|
||||
@@ -17,9 +17,13 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from gi.repository import Adw, Gtk
|
||||
from typing import Any
|
||||
|
||||
from gi.repository import Adw, Gio, GLib, Gtk
|
||||
|
||||
from src import shared
|
||||
from src.game import Game
|
||||
from src.game_cover import GameCover
|
||||
from src.utils.relative_date import relative_date
|
||||
|
||||
|
||||
@@ -73,13 +77,13 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
hidden_search_entry = Gtk.Template.Child()
|
||||
hidden_search_button = Gtk.Template.Child()
|
||||
|
||||
game_covers = {}
|
||||
toasts = {}
|
||||
active_game = None
|
||||
details_view_game_cover = None
|
||||
sort_state = "a-z"
|
||||
filter_state = "all"
|
||||
source_rows = {}
|
||||
game_covers: dict = {}
|
||||
toasts: dict = {}
|
||||
active_game: Game
|
||||
details_view_game_cover: GameCover | None = None
|
||||
sort_state: str = "a-z"
|
||||
filter_state: str = "all"
|
||||
source_rows: dict = {}
|
||||
|
||||
def create_source_rows(self):
|
||||
def get_removed(source_id):
|
||||
@@ -204,7 +208,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
if self.overlay_split_view.get_collapsed():
|
||||
self.overlay_split_view.set_show_sidebar(False)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.details_view.set_measure_overlay(self.details_view_toolbar_view, True)
|
||||
@@ -249,11 +253,11 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
style_manager.connect("notify::dark", self.set_details_view_opacity)
|
||||
style_manager.connect("notify::high-contrast", self.set_details_view_opacity)
|
||||
|
||||
def search_changed(self, _widget, hidden):
|
||||
def search_changed(self, _widget: Any, hidden: bool) -> None:
|
||||
# Refresh search filter on keystroke in search box
|
||||
(self.hidden_library if hidden else self.library).invalidate_filter()
|
||||
|
||||
def set_library_child(self):
|
||||
def set_library_child(self) -> None:
|
||||
child, hidden_child = self.notice_empty, self.hidden_notice_empty
|
||||
|
||||
for game in shared.store:
|
||||
@@ -286,7 +290,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
remove_from_overlay(self.hidden_notice_empty)
|
||||
remove_from_overlay(self.hidden_notice_no_results)
|
||||
|
||||
def filter_func(self, child):
|
||||
def filter_func(self, child: Gtk.Widget) -> bool:
|
||||
game = child.get_child()
|
||||
text = (
|
||||
(
|
||||
@@ -314,10 +318,10 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
|
||||
return not filtered
|
||||
|
||||
def set_active_game(self, _widget, _pspec, game):
|
||||
def set_active_game(self, _widget: Any, _pspec: Any, game: Game) -> None:
|
||||
self.active_game = game
|
||||
|
||||
def show_details_page(self, game):
|
||||
def show_details_page(self, game: Game) -> None:
|
||||
self.active_game = game
|
||||
|
||||
self.details_view_cover.set_opacity(int(not game.loading))
|
||||
@@ -365,7 +369,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
|
||||
self.set_details_view_opacity()
|
||||
|
||||
def set_details_view_opacity(self, *_args):
|
||||
def set_details_view_opacity(self, *_args: Any) -> None:
|
||||
if self.navigation_view.get_visible_page() != self.details_page:
|
||||
return
|
||||
|
||||
@@ -376,12 +380,12 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
return
|
||||
|
||||
self.details_view_blurred_cover.set_opacity(
|
||||
1 - self.details_view_game_cover.luminance[0]
|
||||
1 - self.details_view_game_cover.luminance[0] # type: ignore
|
||||
if style_manager.get_dark()
|
||||
else self.details_view_game_cover.luminance[1]
|
||||
else self.details_view_game_cover.luminance[1] # type: ignore
|
||||
)
|
||||
|
||||
def sort_func(self, child1, child2):
|
||||
def sort_func(self, child1: Gtk.Widget, child2: Gtk.Widget) -> int:
|
||||
var, order = "name", True
|
||||
|
||||
if self.sort_state in ("newest", "oldest"):
|
||||
@@ -391,7 +395,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
elif self.sort_state == "a-z":
|
||||
order = False
|
||||
|
||||
def get_value(index):
|
||||
def get_value(index: int) -> str:
|
||||
return str(
|
||||
getattr((child1.get_child(), child2.get_child())[index], var)
|
||||
).lower()
|
||||
@@ -412,24 +416,24 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
)
|
||||
self.overlay_split_view.set_show_sidebar(value)
|
||||
|
||||
def on_go_to_parent_action(self, *_args):
|
||||
def on_go_to_parent_action(self, *_args: Any) -> None:
|
||||
if self.navigation_view.get_visible_page() == self.details_page:
|
||||
self.navigation_view.pop()
|
||||
|
||||
def on_go_home_action(self, *_args):
|
||||
def on_go_home_action(self, *_args: Any) -> None:
|
||||
self.navigation_view.pop_to_page(self.library_page)
|
||||
|
||||
def on_show_hidden_action(self, *_args):
|
||||
def on_show_hidden_action(self, *_args: Any) -> None:
|
||||
self.navigation_view.push(self.hidden_library_page)
|
||||
|
||||
def on_sort_action(self, action, state):
|
||||
def on_sort_action(self, action: Gio.SimpleAction, state: GLib.Variant) -> None:
|
||||
action.set_state(state)
|
||||
self.sort_state = str(state).strip("'")
|
||||
self.library.invalidate_sort()
|
||||
|
||||
shared.state_schema.set_string("sort-mode", self.sort_state)
|
||||
|
||||
def on_toggle_search_action(self, *_args):
|
||||
def on_toggle_search_action(self, *_args: Any) -> None:
|
||||
if self.navigation_view.get_visible_page() == self.library_page:
|
||||
search_bar = self.search_bar
|
||||
search_entry = self.search_entry
|
||||
@@ -446,7 +450,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
|
||||
search_entry.set_text("")
|
||||
|
||||
def on_escape_action(self, *_args):
|
||||
def on_escape_action(self, *_args: Any) -> None:
|
||||
if (
|
||||
self.get_focus() == self.search_entry.get_focus_child()
|
||||
or self.hidden_search_entry.get_focus_child()
|
||||
@@ -455,7 +459,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
else:
|
||||
self.navigation_view.pop()
|
||||
|
||||
def show_details_page_search(self, widget):
|
||||
def show_details_page_search(self, widget: Gtk.Widget) -> None:
|
||||
library = (
|
||||
self.hidden_library if widget == self.hidden_search_entry else self.library
|
||||
)
|
||||
@@ -471,30 +475,39 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
|
||||
index += 1
|
||||
|
||||
def on_undo_action(self, _widget, game=None, undo=None):
|
||||
def on_undo_action(
|
||||
self, _widget: Any, game: Game | None = None, undo: str | None = None
|
||||
) -> None:
|
||||
if not game: # If the action was activated via Ctrl + Z
|
||||
if shared.importer and (
|
||||
shared.importer.imported_game_ids or shared.importer.removed_game_ids
|
||||
):
|
||||
shared.importer.undo_import()
|
||||
return
|
||||
|
||||
try:
|
||||
game = tuple(self.toasts.keys())[-1][0]
|
||||
undo = tuple(self.toasts.keys())[-1][1]
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
if undo == "hide":
|
||||
game.toggle_hidden(False)
|
||||
if game:
|
||||
if undo == "hide":
|
||||
game.toggle_hidden(False)
|
||||
|
||||
elif undo == "remove":
|
||||
game.removed = False
|
||||
game.save()
|
||||
game.update()
|
||||
elif undo == "remove":
|
||||
game.removed = False
|
||||
game.save()
|
||||
game.update()
|
||||
|
||||
self.toasts[(game, undo)].dismiss()
|
||||
self.toasts.pop((game, undo))
|
||||
self.toasts[(game, undo)].dismiss()
|
||||
self.toasts.pop((game, undo))
|
||||
|
||||
def on_open_menu_action(self, *_args):
|
||||
def on_open_menu_action(self, *_args: Any) -> None:
|
||||
if self.navigation_view.get_visible_page() == self.library_page:
|
||||
self.primary_menu_button.popup()
|
||||
elif self.navigation_view.get_visible_page() == self.hidden_library_page:
|
||||
self.hidden_primary_menu_button.popup()
|
||||
|
||||
def on_close_action(self, *_args):
|
||||
def on_close_action(self, *_args: Any) -> None:
|
||||
self.close()
|
||||
|
||||
Reference in New Issue
Block a user