* Avoid exception if trying to delete missing "removed" key `.pop(key)` throws an exception if that `key` is missing. Using a default value (in this case None) means we don't throw any errors when deleting a missing key. Which is what we want here, for safety. We just want to delete the key. We don't care if it's existing or not. There are other places in the codebase that also use `.pop(key)`, but all of those first check the validity of the key before popping, so this was the only one that needed fixing. * Improve performance by removing keys() calls Every `.keys()` call create a new `[list...]` of all keys from the given dictionary. It's a total waste of performance, since we can already check if a key exists in a dictionary by just using `if "key" in the_dict`. * Use more pythonic "not in" syntax The syntax is supposed to be `if "thing" not in other_thing`. We already use this proper `not in` syntax everywhere else in the codebase. Just fixing this location.
435 lines
16 KiB
Python
435 lines
16 KiB
Python
# window.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
|
|
|
|
import datetime
|
|
import os
|
|
|
|
from gi.repository import Adw, GdkPixbuf, Gio, GLib, Gtk
|
|
|
|
from .game import game
|
|
from .get_games import get_games
|
|
from .save_game import save_game
|
|
|
|
|
|
@Gtk.Template(resource_path="/hu/kramo/Cartridges/gtk/window.ui")
|
|
class CartridgesWindow(Adw.ApplicationWindow):
|
|
__gtype_name__ = "CartridgesWindow"
|
|
|
|
toast_overlay = Gtk.Template.Child()
|
|
primary_menu_button = Gtk.Template.Child()
|
|
stack = Gtk.Template.Child()
|
|
overview = Gtk.Template.Child()
|
|
library_view = Gtk.Template.Child()
|
|
library = Gtk.Template.Child()
|
|
scrolledwindow = Gtk.Template.Child()
|
|
library_bin = Gtk.Template.Child()
|
|
notice_empty = Gtk.Template.Child()
|
|
notice_no_results = Gtk.Template.Child()
|
|
search_bar = Gtk.Template.Child()
|
|
search_entry = Gtk.Template.Child()
|
|
search_button = Gtk.Template.Child()
|
|
|
|
overview_box = Gtk.Template.Child()
|
|
overview_cover = Gtk.Template.Child()
|
|
overview_title = Gtk.Template.Child()
|
|
overview_header_bar_title = Gtk.Template.Child()
|
|
overview_launch = Gtk.Template.Child()
|
|
overview_blurred_cover = Gtk.Template.Child()
|
|
overview_menu_button = Gtk.Template.Child()
|
|
overview_developer = Gtk.Template.Child()
|
|
overview_added = Gtk.Template.Child()
|
|
overview_last_played = Gtk.Template.Child()
|
|
|
|
hidden_library = Gtk.Template.Child()
|
|
hidden_library_view = Gtk.Template.Child()
|
|
hidden_scrolledwindow = Gtk.Template.Child()
|
|
hidden_library_bin = Gtk.Template.Child()
|
|
hidden_notice_empty = Gtk.Template.Child()
|
|
hidden_search_bar = Gtk.Template.Child()
|
|
hidden_search_entry = Gtk.Template.Child()
|
|
hidden_search_button = Gtk.Template.Child()
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
|
|
self.games = {}
|
|
self.visible_widgets = {}
|
|
self.hidden_widgets = {}
|
|
self.filtered = {}
|
|
self.hidden_filtered = {}
|
|
self.previous_page = self.library_view
|
|
self.toasts = {}
|
|
self.pixbufs = {}
|
|
self.active_game_id = None
|
|
|
|
self.overview.set_measure_overlay(self.overview_box, True)
|
|
self.overview.set_clip_overlay(self.overview_box, False)
|
|
|
|
self.schema = Gio.Settings.new("hu.kramo.Cartridges")
|
|
self.placeholder_pixbuf = GdkPixbuf.Pixbuf.new_from_resource_at_scale(
|
|
"/hu/kramo/Cartridges/library_placeholder.svg", 400, 600, False
|
|
)
|
|
current_games = get_games()
|
|
for current_game in current_games:
|
|
if "removed" in current_games[current_game]:
|
|
os.remove(
|
|
os.path.join(
|
|
os.getenv("XDG_DATA_HOME")
|
|
or os.path.expanduser(os.path.join("~", ".local", "share")),
|
|
"cartridges",
|
|
"games",
|
|
f"{current_game}.json",
|
|
)
|
|
)
|
|
try:
|
|
os.remove(
|
|
os.path.join(
|
|
os.getenv("XDG_DATA_HOME")
|
|
or os.path.expanduser(os.path.join("~", ".local", "share")),
|
|
"cartridges",
|
|
"covers",
|
|
f"{current_game}.tiff",
|
|
)
|
|
)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
self.library.set_filter_func(self.search_filter)
|
|
self.hidden_library.set_filter_func(self.hidden_search_filter)
|
|
|
|
self.update_games(get_games())
|
|
|
|
# Connect signals
|
|
self.search_entry.connect("search-changed", self.search_changed, False)
|
|
self.hidden_search_entry.connect("search-changed", self.search_changed, True)
|
|
|
|
back_mouse_button = Gtk.GestureClick(button=8)
|
|
back_mouse_button.connect("pressed", self.on_go_back_action)
|
|
self.add_controller(back_mouse_button)
|
|
|
|
def update_games(self, games):
|
|
current_games = get_games()
|
|
|
|
for game_id in games:
|
|
if game_id in self.visible_widgets:
|
|
self.library.remove(self.visible_widgets[game_id])
|
|
self.filtered.pop(self.visible_widgets[game_id])
|
|
self.visible_widgets.pop(game_id)
|
|
elif game_id in self.hidden_widgets:
|
|
self.hidden_library.remove(self.hidden_widgets[game_id])
|
|
self.hidden_filtered.pop(self.hidden_widgets[game_id])
|
|
self.hidden_widgets.pop(game_id)
|
|
|
|
current_game = current_games[game_id]
|
|
|
|
entry = game(self, current_game)
|
|
self.games[current_game["game_id"]] = entry
|
|
|
|
if entry.removed:
|
|
continue
|
|
if entry.blacklisted:
|
|
continue
|
|
|
|
if not self.games[game_id].hidden:
|
|
self.visible_widgets[game_id] = entry
|
|
self.library.append(entry)
|
|
else:
|
|
self.hidden_widgets[game_id] = entry
|
|
self.hidden_library.append(entry)
|
|
|
|
entry.menu_button.get_popover().connect(
|
|
"notify::visible", self.set_active_game, game_id
|
|
)
|
|
entry.get_parent().set_focusable(False)
|
|
|
|
if not self.visible_widgets:
|
|
self.library_bin.set_child(self.notice_empty)
|
|
else:
|
|
self.library_bin.set_child(self.scrolledwindow)
|
|
|
|
if not self.hidden_widgets:
|
|
self.hidden_library_bin.set_child(self.hidden_notice_empty)
|
|
else:
|
|
self.hidden_library_bin.set_child(self.hidden_scrolledwindow)
|
|
|
|
self.library.invalidate_filter()
|
|
self.hidden_library.invalidate_filter()
|
|
|
|
def search_changed(self, _widget, hidden):
|
|
# Refresh search filter on keystroke in search box
|
|
if not hidden:
|
|
self.library.invalidate_filter()
|
|
else:
|
|
self.hidden_library.invalidate_filter()
|
|
|
|
def search_filter(self, child):
|
|
# Only show games matching the contents of the search box
|
|
text = self.search_entry.get_text().lower()
|
|
if text == "":
|
|
filtered = True
|
|
elif (
|
|
text in child.get_first_child().name.lower()
|
|
or text in child.get_first_child().developer.lower()
|
|
if child.get_first_child().developer
|
|
else None
|
|
):
|
|
filtered = True
|
|
else:
|
|
filtered = False
|
|
|
|
# Add filtered entry to dict of filtered widgets
|
|
self.filtered[child.get_first_child()] = filtered
|
|
|
|
if True not in self.filtered.values():
|
|
self.library_bin.set_child(self.notice_no_results)
|
|
else:
|
|
self.library_bin.set_child(self.scrolledwindow)
|
|
return filtered
|
|
|
|
def hidden_search_filter(self, child):
|
|
text = self.hidden_search_entry.get_text().lower()
|
|
if text == "":
|
|
filtered = True
|
|
elif (
|
|
text in child.get_first_child().name.lower()
|
|
or text in child.get_first_child().developer.lower()
|
|
if child.get_first_child().developer
|
|
else None
|
|
):
|
|
filtered = True
|
|
else:
|
|
filtered = False
|
|
|
|
self.hidden_filtered[child.get_first_child()] = filtered
|
|
|
|
if True not in self.hidden_filtered.values():
|
|
self.hidden_library_bin.set_child(self.notice_no_results)
|
|
else:
|
|
self.hidden_library_bin.set_child(self.hidden_scrolledwindow)
|
|
return filtered
|
|
|
|
def set_active_game(self, _widget, _unused, game_id):
|
|
self.active_game_id = game_id
|
|
|
|
def get_time(self, timestamp):
|
|
date = datetime.datetime.fromtimestamp(timestamp)
|
|
|
|
if (datetime.datetime.today() - date).days == 0:
|
|
return _("Today")
|
|
if (datetime.datetime.today() - date).days == 1:
|
|
return _("Yesterday")
|
|
if (datetime.datetime.today() - date).days < 8:
|
|
return GLib.DateTime.new_from_unix_utc(timestamp).format("%A")
|
|
return GLib.DateTime.new_from_unix_utc(timestamp).format("%x")
|
|
|
|
def show_overview(self, _widget, game_id):
|
|
current_game = self.games[game_id]
|
|
|
|
if current_game.developer:
|
|
self.overview_developer.set_label(current_game.developer)
|
|
self.overview_developer.set_visible(True)
|
|
else:
|
|
self.overview_developer.set_visible(False)
|
|
|
|
self.overview_menu_button.set_menu_model(
|
|
current_game.menu_button.get_menu_model()
|
|
)
|
|
|
|
if self.stack.get_visible_child() != self.overview:
|
|
self.stack.set_transition_type(Gtk.StackTransitionType.OVER_LEFT)
|
|
self.stack.set_visible_child(self.overview)
|
|
|
|
self.active_game_id = game_id
|
|
|
|
pixbuf = current_game.pixbuf
|
|
self.overview_cover.set_pixbuf(pixbuf)
|
|
self.overview_blurred_cover.set_pixbuf(
|
|
pixbuf.scale_simple(2, 3, GdkPixbuf.InterpType.BILINEAR)
|
|
)
|
|
self.overview_title.set_label(current_game.name)
|
|
self.overview_header_bar_title.set_title(current_game.name)
|
|
date = self.get_time(current_game.added)
|
|
self.overview_added.set_label(
|
|
# The variable is the date when the game was added
|
|
_(f"Added: {date}")
|
|
)
|
|
last_played_date = (
|
|
self.get_time(current_game.last_played)
|
|
if current_game.last_played != 0
|
|
else _("Never")
|
|
)
|
|
self.overview_last_played.set_label(
|
|
# The variable is the date when the game was last played
|
|
_(f"Last played: {last_played_date}")
|
|
)
|
|
|
|
def a_z_sort(self, child1, child2):
|
|
name1 = child1.get_first_child().name.lower()
|
|
name2 = child2.get_first_child().name.lower()
|
|
if name1 > name2:
|
|
return 1
|
|
if name1 < name2:
|
|
return -1
|
|
if child1.get_first_child().game_id > child2.get_first_child().game_id:
|
|
return 1
|
|
return -1
|
|
|
|
def z_a_sort(self, child1, child2):
|
|
name1 = child1.get_first_child().name.lower()
|
|
name2 = child2.get_first_child().name.lower()
|
|
if name1 > name2:
|
|
return -1
|
|
return 1 if name1 < name2 else self.a_z_sort(child1, child2)
|
|
|
|
def newest_sort(self, child1, child2):
|
|
time1 = self.games[child1.get_first_child().game_id].added
|
|
time2 = self.games[child2.get_first_child().game_id].added
|
|
if time1 > time2:
|
|
return -1
|
|
return 1 if time1 < time2 else self.a_z_sort(child1, child2)
|
|
|
|
def oldest_sort(self, child1, child2):
|
|
time1 = self.games[child1.get_first_child().game_id].added
|
|
time2 = self.games[child2.get_first_child().game_id].added
|
|
if time1 > time2:
|
|
return 1
|
|
return -1 if time1 < time2 else self.a_z_sort(child1, child2)
|
|
|
|
def last_played_sort(self, child1, child2):
|
|
time1 = self.games[child1.get_first_child().game_id].last_played
|
|
time2 = self.games[child2.get_first_child().game_id].last_played
|
|
if time1 > time2:
|
|
return -1
|
|
return 1 if time1 < time2 else self.a_z_sort(child1, child2)
|
|
|
|
def on_go_back_action(self, _widget, _unused, _x=None, _y=None):
|
|
if self.stack.get_visible_child() == self.hidden_library_view:
|
|
self.on_show_library_action(None, None)
|
|
elif self.stack.get_visible_child() == self.overview:
|
|
self.on_go_to_parent_action(None, None)
|
|
|
|
def on_go_to_parent_action(self, _widget, _unused):
|
|
if self.stack.get_visible_child() == self.overview:
|
|
if self.previous_page == self.library_view:
|
|
self.on_show_library_action(None, None)
|
|
else:
|
|
self.on_show_hidden_action(None, None)
|
|
|
|
def on_show_library_action(self, _widget, _unused):
|
|
self.stack.set_transition_type(Gtk.StackTransitionType.UNDER_RIGHT)
|
|
self.stack.set_visible_child(self.library_view)
|
|
self.lookup_action("show_hidden").set_enabled(True)
|
|
self.previous_page = self.library_view
|
|
|
|
def on_show_hidden_action(self, _widget, _unused):
|
|
if self.stack.get_visible_child() == self.library_view:
|
|
self.stack.set_transition_type(Gtk.StackTransitionType.OVER_LEFT)
|
|
else:
|
|
self.stack.set_transition_type(Gtk.StackTransitionType.UNDER_RIGHT)
|
|
self.lookup_action("show_hidden").set_enabled(False)
|
|
self.stack.set_visible_child(self.hidden_library_view)
|
|
self.previous_page = self.hidden_library_view
|
|
|
|
def on_sort_action(self, action, state):
|
|
action.set_state(state)
|
|
state = str(state).strip("'")
|
|
|
|
if state == "a-z":
|
|
sort_func = self.a_z_sort
|
|
|
|
elif state == "z-a":
|
|
sort_func = self.z_a_sort
|
|
|
|
elif state == "newest":
|
|
sort_func = self.newest_sort
|
|
|
|
elif state == "oldest":
|
|
sort_func = self.oldest_sort
|
|
|
|
else:
|
|
sort_func = self.last_played_sort
|
|
|
|
Gio.Settings(schema_id="hu.kramo.Cartridge.State").set_string(
|
|
"sort-mode", state
|
|
)
|
|
self.library.set_sort_func(sort_func)
|
|
self.hidden_library.set_sort_func(sort_func)
|
|
|
|
def on_toggle_search_action(self, _widget, _unused):
|
|
if self.stack.get_visible_child() == self.library_view:
|
|
search_bar = self.search_bar
|
|
search_entry = self.search_entry
|
|
search_button = self.search_button
|
|
elif self.stack.get_visible_child() == self.hidden_library_view:
|
|
search_bar = self.hidden_search_bar
|
|
search_entry = self.hidden_search_entry
|
|
search_button = self.hidden_search_button
|
|
else:
|
|
return
|
|
|
|
search_mode = search_bar.get_search_mode()
|
|
search_bar.set_search_mode(not search_mode)
|
|
search_button.set_active(not search_button.get_active())
|
|
|
|
if not search_mode:
|
|
self.set_focus(search_entry)
|
|
else:
|
|
search_entry.set_text("")
|
|
|
|
def on_escape_action(self, _widget, _unused):
|
|
if self.stack.get_visible_child() == self.overview:
|
|
self.on_go_back_action(None, None)
|
|
return
|
|
if self.stack.get_visible_child() == self.library_view:
|
|
search_bar = self.search_bar
|
|
search_entry = self.search_entry
|
|
search_button = self.search_button
|
|
elif self.stack.get_visible_child() == self.hidden_library_view:
|
|
search_bar = self.hidden_search_bar
|
|
search_entry = self.hidden_search_entry
|
|
search_button = self.hidden_search_button
|
|
else:
|
|
return
|
|
|
|
if self.get_focus() == search_entry.get_focus_child():
|
|
search_bar.set_search_mode(False)
|
|
search_button.set_active(False)
|
|
search_entry.set_text("")
|
|
|
|
def on_undo_remove_action(self, _widget, game_id=None):
|
|
# Remove the "removed=True" property from the game and dismiss the toast
|
|
|
|
if not game_id:
|
|
try:
|
|
game_id = list(self.toasts)[-1]
|
|
except IndexError:
|
|
return
|
|
data = get_games([game_id])[game_id]
|
|
data.pop("removed", None)
|
|
save_game(data)
|
|
self.update_games([game_id])
|
|
self.toasts[game_id].dismiss()
|
|
self.toasts.pop(game_id)
|
|
|
|
def on_open_menu_action(self, _widget, _unused):
|
|
if self.stack.get_visible_child() != self.overview:
|
|
self.primary_menu_button.set_active(True)
|
|
else:
|
|
self.overview_menu_button.set_active(True)
|