# details_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 . # # SPDX-License-Identifier: GPL-3.0-or-later import os from time import time from typing import Any from gi.repository import Adw, Gio, GLib, Gtk from PIL import Image from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game from src.game_cover import GameCover from src.store.managers.sgdb_manager import SGDBManager from src.utils.create_dialog import create_dialog from src.utils.save_cover import resize_cover, save_cover @Gtk.Template(resource_path=shared.PREFIX + "/gtk/details-window.ui") class DetailsWindow(Adw.Window): __gtype_name__ = "DetailsWindow" cover_overlay = Gtk.Template.Child() cover = Gtk.Template.Child() cover_button_edit = Gtk.Template.Child() cover_button_delete_revealer = Gtk.Template.Child() cover_button_delete = Gtk.Template.Child() spinner = Gtk.Template.Child() name = Gtk.Template.Child() developer = Gtk.Template.Child() executable = Gtk.Template.Child() exec_info_label = Gtk.Template.Child() exec_info_popover = Gtk.Template.Child() apply_button = Gtk.Template.Child() cover_changed: bool = False def __init__(self, game: Game | None = None, **kwargs: Any): super().__init__(**kwargs) self.game: Game = game self.game_cover: GameCover = GameCover({self.cover}) self.set_transient_for(shared.win) if self.game: self.set_title(_("Game Details")) self.name.set_text(self.game.name) if self.game.developer: self.developer.set_text(self.game.developer) self.executable.set_text(self.game.executable) self.apply_button.set_label(_("Apply")) self.game_cover.new_cover(self.game.get_cover_path()) if self.game_cover.get_texture(): self.cover_button_delete_revealer.set_reveal_child(True) else: self.set_title(_("Add New Game")) self.apply_button.set_label(_("Add")) image_filter = Gtk.FileFilter(name=_("Images")) for extension in Image.registered_extensions(): image_filter.add_suffix(extension[1:]) image_filter.add_suffix("svg") # Gdk.Texture supports .svg but PIL doesn't file_filters = Gio.ListStore.new(Gtk.FileFilter) file_filters.append(image_filter) self.file_dialog = Gtk.FileDialog() self.file_dialog.set_filters(file_filters) # Translate this string as you would translate "file" file_name = _("file.txt") # As in software exe_name = _("program") if os.name == "nt": exe_name += ".exe" # Translate this string as you would translate "path to {}" exe_path = _("C:\\path\\to\\{}").format(exe_name) # Translate this string as you would translate "path to {}" file_path = _("C:\\path\\to\\{}").format(file_name) command = "start" else: # Translate this string as you would translate "path to {}" exe_path = _("/path/to/{}").format(exe_name) # Translate this string as you would translate "path to {}" file_path = _("/path/to/{}").format(file_name) command = "xdg-open" # pylint: disable=line-too-long exec_info_text = _( 'To launch the executable "{}", use the command:\n\n"{}"\n\nTo open the file "{}" with the default application, use:\n\n{} "{}"\n\nIf the path contains spaces, make sure to wrap it in double quotes!' ).format(exe_name, exe_path, file_name, command, file_path) self.exec_info_label.set_label(exec_info_text) def clear_info_selection(*_args: Any) -> None: self.exec_info_label.select_region(-1, -1) self.exec_info_popover.connect("show", clear_info_selection) self.cover_button_delete.connect("clicked", self.delete_pixbuf) self.cover_button_edit.connect("clicked", self.choose_cover) self.apply_button.connect("clicked", self.apply_preferences) self.name.connect("entry-activated", self.focus_executable) self.developer.connect("entry-activated", self.focus_executable) self.executable.connect("entry-activated", self.apply_preferences) self.set_focus(self.name) self.present() 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: Any) -> None: final_name = self.name.get_text() final_developer = self.developer.get_text() final_executable = self.executable.get_text() if not self.game: if final_name == "": create_dialog( self, _("Couldn't Add Game"), _("Game title cannot be empty.") ) return if final_executable == "": create_dialog( self, _("Couldn't Add Game"), _("Executable cannot be empty.") ) return # Increment the number after the game id (eg. imported_1, imported_2) source_id = "imported" numbers = [0] game_id: str for game_id in shared.store.source_games.get(source_id, set()): prefix = "imported_" if not game_id.startswith(prefix): continue numbers.append(int(game_id.replace(prefix, "", 1))) game_number = max(numbers) + 1 self.game = Game( { "game_id": f"imported_{game_number}", "hidden": False, "source": source_id, "added": int(time()), } ) if self.win.sidebar.get_selected_row().get_child() not in ( self.win.all_games_row_box, self.win.added_row_box, ): self.win.sidebar.select_row(self.win.added_row_box.get_parent()) else: if final_name == "": create_dialog( self, _("Couldn't Apply Preferences"), _("Game title cannot be empty."), ) return if final_executable == "": create_dialog( self, _("Couldn't Apply Preferences"), _("Executable cannot be empty."), ) return self.game.name = final_name self.game.developer = final_developer or None self.game.executable = final_executable if self.game.game_id in shared.win.game_covers.keys(): shared.win.game_covers[self.game.game_id].animation = None shared.win.game_covers[self.game.game_id] = self.game_cover if self.cover_changed: save_cover( self.game.game_id, self.game_cover.path, ) shared.store.add_game(self.game, {}, run_pipeline=False) self.game.save() self.game.update() # TODO: this is fucked up (less than before) # Get a cover from SGDB if none is present if not self.game_cover.get_texture(): self.game.set_loading(1) sgdb_manager: SGDBManager = shared.store.managers[SGDBManager] sgdb_manager.reset_cancellable() sgdb_manager.process_game(self.game, {}, self.update_cover_callback) self.game_cover.pictures.remove(self.cover) self.close() shared.win.show_details_page(self.game) def update_cover_callback(self, manager: SGDBManager) -> None: # Set the game as not loading self.game.set_loading(-1) self.game.update() # Handle errors that occured for error in manager.collect_errors(): # On auth error, inform the user if isinstance(error, FriendlyError): create_dialog( shared.win, error.title, error.subtitle, "open_preferences", _("Preferences"), ).connect("response", self.update_cover_error_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: Any) -> None: self.set_focus(self.executable) 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: Any, result: Gio.Task, *_args: Any) -> None: try: path = self.file_dialog.open_finish(result).get_path() except GLib.GError: return def resize() -> None: if cover := resize_cover(path): self.game_cover.new_cover(cover) self.cover_button_delete_revealer.set_reveal_child(True) self.cover_changed = True self.toggle_loading() self.toggle_loading() GLib.Thread.new(None, resize) def choose_cover(self, *_args: Any) -> None: self.file_dialog.open(self, None, self.set_cover)