514 lines
19 KiB
Python
514 lines
19 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
|
|
import struct
|
|
from pathlib import Path
|
|
from shutil import rmtree
|
|
|
|
from gi.repository import Adw, Gdk, GdkPixbuf, Gio, GLib, Gtk
|
|
|
|
from .game import game
|
|
from .game_cover import GameCover
|
|
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_spinner = Gtk.Template.Child()
|
|
overview_title = Gtk.Template.Child()
|
|
overview_header_bar_title = Gtk.Template.Child()
|
|
overview_play_button = Gtk.Template.Child()
|
|
overview_blurred_cover = Gtk.Template.Child()
|
|
overview_developer = Gtk.Template.Child()
|
|
overview_added = Gtk.Template.Child()
|
|
overview_last_played = Gtk.Template.Child()
|
|
overview_hide_button = 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.data_dir = (
|
|
Path(os.getenv("XDG_DATA_HOME"))
|
|
if "XDG_DATA_HOME" in os.environ
|
|
else Path.home() / ".local" / "share"
|
|
)
|
|
self.config_dir = (
|
|
Path(os.getenv("XDG_CONFIG_HOME"))
|
|
if "XDG_CONFIG_HOME" in os.environ
|
|
else Path.home() / ".config"
|
|
)
|
|
self.cache_dir = (
|
|
Path(os.getenv("XDG_CACHE_HOME"))
|
|
if "XDG_CACHE_HOME" in os.environ
|
|
else Path.home() / ".cache"
|
|
)
|
|
|
|
self.games_dir = self.data_dir / "cartridges" / "games"
|
|
self.covers_dir = self.data_dir / "cartridges" / "covers"
|
|
|
|
self.games = {}
|
|
self.visible_widgets = {}
|
|
self.hidden_widgets = {}
|
|
self.filtered = {}
|
|
self.hidden_filtered = {}
|
|
self.previous_page = self.library_view
|
|
self.toasts = {}
|
|
self.active_game_id = None
|
|
self.loading = None
|
|
self.scaled_pixbuf = 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")
|
|
scale_factor = max(
|
|
monitor.get_scale_factor()
|
|
for monitor in Gdk.Display.get_default().get_monitors()
|
|
)
|
|
self.image_size = (200 * scale_factor, 300 * scale_factor)
|
|
|
|
games = get_games(self)
|
|
for game_id in games:
|
|
if "removed" in games[game_id]:
|
|
(self.games_dir / f"{game_id}.json").unlink(missing_ok=True)
|
|
(self.covers_dir / f"{game_id}.tiff").unlink(missing_ok=True)
|
|
(self.covers_dir / f"{game_id}.gif").unlink(missing_ok=True)
|
|
|
|
rmtree(self.cache_dir / "cartridges" / "deleted_covers", True)
|
|
|
|
self.library.set_filter_func(self.search_filter)
|
|
self.hidden_library.set_filter_func(self.hidden_search_filter)
|
|
|
|
self.update_games(get_games(self))
|
|
|
|
self.overview_game_cover = GameCover(self.overview_cover)
|
|
|
|
# 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)
|
|
|
|
Adw.StyleManager.get_default().connect(
|
|
"notify::dark", self.set_overview_opacity
|
|
)
|
|
Adw.StyleManager.get_default().connect(
|
|
"notify::high-contrast", self.set_overview_opacity
|
|
)
|
|
|
|
def update_games(self, games):
|
|
current_games = get_games(self)
|
|
|
|
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)
|
|
|
|
if self.stack.get_visible_child() == self.overview:
|
|
self.show_overview(None, self.active_game_id)
|
|
|
|
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):
|
|
loading = game_id == self.loading
|
|
self.overview_cover.set_visible(not loading)
|
|
self.overview_spinner.set_spinning(loading)
|
|
|
|
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)
|
|
|
|
if current_game.hidden:
|
|
self.overview_hide_button.set_icon_name("view-reveal-symbolic")
|
|
self.overview_hide_button.set_tooltip_text(_("Unhide"))
|
|
else:
|
|
self.overview_hide_button.set_icon_name("view-conceal-symbolic")
|
|
self.overview_hide_button.set_tooltip_text(_("Hide"))
|
|
|
|
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
|
|
|
|
self.overview_game_cover.new_pixbuf(path=current_game.get_cover_path())
|
|
pixbuf = (
|
|
self.overview_game_cover.get_pixbuf()
|
|
or self.overview_game_cover.placeholder_pixbuf
|
|
)
|
|
|
|
self.scaled_pixbuf = pixbuf.scale_simple(2, 3, GdkPixbuf.InterpType.BILINEAR)
|
|
self.overview_blurred_cover.set_pixbuf(self.scaled_pixbuf)
|
|
self.set_overview_opacity()
|
|
|
|
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
|
|
_("Added: {}").format(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
|
|
_("Last played: {}").format(last_played_date)
|
|
)
|
|
|
|
def set_overview_opacity(self, _widget=None, _unused=None):
|
|
if self.stack.get_visible_child() == self.overview:
|
|
style_manager = Adw.StyleManager.get_default()
|
|
|
|
if (
|
|
style_manager.get_high_contrast()
|
|
or not style_manager.get_system_supports_color_schemes()
|
|
):
|
|
self.overview_blurred_cover.set_opacity(0.2)
|
|
return
|
|
|
|
pixels = self.scaled_pixbuf.get_pixels()
|
|
channels = self.scaled_pixbuf.get_n_channels()
|
|
colors = set()
|
|
|
|
for index in range(6):
|
|
colors.add(struct.unpack_from("BBBB", pixels, offset=index * channels))
|
|
|
|
dark_theme = style_manager.get_dark()
|
|
|
|
luminances = []
|
|
|
|
for red, green, blue, alpha in colors:
|
|
# https://en.wikipedia.org/wiki/Relative_luminance
|
|
luminance = red * 0.2126 + green * 0.7152 + blue * 0.0722
|
|
|
|
if dark_theme:
|
|
luminances.append((luminance * alpha) / 255**2)
|
|
else:
|
|
luminances.append((alpha * (luminance - 255)) / 255**2 + 1)
|
|
|
|
if dark_theme:
|
|
self.overview_blurred_cover.set_opacity(
|
|
1.3 - (sum(luminances) / len(luminances) + max(luminances)) / 2
|
|
)
|
|
else:
|
|
self.overview_blurred_cover.set_opacity(
|
|
0.1 + (sum(luminances) / len(luminances) + min(luminances)) / 2
|
|
)
|
|
|
|
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_action(self, _widget, game_id=None, undo=None):
|
|
if not game_id: # If the action was activated via Ctrl + Z
|
|
try:
|
|
game_id = tuple(self.toasts.keys())[-1][0]
|
|
undo = tuple(self.toasts.keys())[-1][1]
|
|
except IndexError:
|
|
return
|
|
|
|
if undo == "hide":
|
|
self.get_application().on_hide_game_action(None, game_id=game_id)
|
|
|
|
elif undo == "remove":
|
|
data = get_games(self, [game_id])[game_id]
|
|
data.pop("removed", None)
|
|
save_game(self, data)
|
|
self.update_games([game_id])
|
|
|
|
self.toasts[(game_id, undo)].dismiss()
|
|
self.toasts.pop((game_id, undo))
|
|
|
|
def on_open_menu_action(self, _widget, _unused):
|
|
if self.stack.get_visible_child() != self.overview:
|
|
self.primary_menu_button.set_active(True)
|