diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..2f71a2e20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/subprojects/blueprint-compiler +/src/__pycache__ diff --git a/data/hu.kramo.GameShelf.desktop.in b/data/hu.kramo.GameShelf.desktop.in new file mode 100644 index 000000000..8e4a51c23 --- /dev/null +++ b/data/hu.kramo.GameShelf.desktop.in @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Game Shelf +Exec=gameshelf +Icon=hu.kramo.GameShelf +Terminal=false +Type=Application +Categories=Game; +StartupNotify=true diff --git a/data/hu.kramo.GameShelf.gschema.xml b/data/hu.kramo.GameShelf.gschema.xml new file mode 100644 index 000000000..8c8ed947c --- /dev/null +++ b/data/hu.kramo.GameShelf.gschema.xml @@ -0,0 +1,21 @@ + + + + + false + + + "~/.steam/" + + + + + + + + + + "a-z" + + + diff --git a/data/hu.kramo.GameShelf.metainfo.xml.in b/data/hu.kramo.GameShelf.metainfo.xml.in new file mode 100644 index 000000000..1d6f16e21 --- /dev/null +++ b/data/hu.kramo.GameShelf.metainfo.xml.in @@ -0,0 +1,13 @@ + + + hu.kramo.GameShelf.desktop + CC0-1.0 + GPL-3.0-or-later + Game Shelf + Launch all your games + + Game Shelf is a simple game launcher. It has support for importing your games from Steam with organizational features such as hiding and sorting by date added or last played. + + hu.kramo.GameShelf.desktop + kramo + diff --git a/data/icons/hicolor/scalable/apps/hu.kramo.GameShelf.svg b/data/icons/hicolor/scalable/apps/hu.kramo.GameShelf.svg new file mode 100644 index 000000000..935e28755 --- /dev/null +++ b/data/icons/hicolor/scalable/apps/hu.kramo.GameShelf.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/hicolor/symbolic/apps/hu.kramo.GameShelf-symbolic.svg b/data/icons/hicolor/symbolic/apps/hu.kramo.GameShelf-symbolic.svg new file mode 100644 index 000000000..cd33c21dd --- /dev/null +++ b/data/icons/hicolor/symbolic/apps/hu.kramo.GameShelf-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/icons/meson.build b/data/icons/meson.build new file mode 100644 index 000000000..efece6a0d --- /dev/null +++ b/data/icons/meson.build @@ -0,0 +1,13 @@ +application_id = 'hu.kramo.GameShelf' + +scalable_dir = join_paths('hicolor', 'scalable', 'apps') +install_data( + join_paths(scalable_dir, ('@0@.svg').format(application_id)), + install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) +) + +symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') +install_data( + join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), + install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir) +) diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 000000000..ddf495aeb --- /dev/null +++ b/data/meson.build @@ -0,0 +1,39 @@ +desktop_file = i18n.merge_file( + input: 'hu.kramo.GameShelf.desktop.in', + output: 'hu.kramo.GameShelf.desktop', + type: 'desktop', + po_dir: '../po', + install: true, + install_dir: join_paths(get_option('datadir'), 'applications') +) + +desktop_utils = find_program('desktop-file-validate', required: false) +if desktop_utils.found() + test('Validate desktop file', desktop_utils, args: [desktop_file]) +endif + +appstream_file = i18n.merge_file( + input: 'hu.kramo.GameShelf.metainfo.xml.in', + output: 'hu.kramo.GameShelf.metainfo.xml', + po_dir: '../po', + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) + +appstream_util = find_program('appstream-util', required: false) +if appstream_util.found() + test('Validate appstream file', appstream_util, args: ['validate', appstream_file]) +endif + +install_data('hu.kramo.GameShelf.gschema.xml', + install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') +) + +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test('Validate schema file', + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()]) +endif + +subdir('icons') diff --git a/hu.kramo.GameShelf.json b/hu.kramo.GameShelf.json new file mode 100644 index 000000000..8c82dfe0f --- /dev/null +++ b/hu.kramo.GameShelf.json @@ -0,0 +1,40 @@ +{ + "app-id" : "hu.kramo.GameShelf", + "runtime" : "org.gnome.Platform", + "runtime-version" : "master", + "sdk" : "org.gnome.Sdk", + "command" : "gameshelf", + "finish-args" : [ + "--share=ipc", + "--socket=fallback-x11", + "--device=dri", + "--socket=wayland", + "--socket=session-bus", + "--filesystem=xdg-run/gvfsd", + "--filesystem=~/.steam/steam/" + ], + "cleanup" : [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules" : [ + { + "name" : "gameshelf", + "builddir" : true, + "buildsystem" : "meson", + "sources" : [ + { + "type" : "git", + "url" : "file:///home/kramo/Projects" + } + ] + } + ] +} diff --git a/meson.build b/meson.build new file mode 100644 index 000000000..acb58b4cc --- /dev/null +++ b/meson.build @@ -0,0 +1,20 @@ +project('gameshelf', + version: '0.1.0', + meson_version: '>= 0.59.0', + default_options: [ 'warning_level=2', 'werror=false', ], +) + +i18n = import('i18n') +gnome = import('gnome') + + + +subdir('data') +subdir('src') +subdir('po') + +gnome.post_install( + glib_compile_schemas: true, + gtk_update_icon_cache: true, + update_desktop_database: true, +) \ No newline at end of file diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 000000000..925eccd2d --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1 @@ +hu diff --git a/po/POTFILES b/po/POTFILES new file mode 100644 index 000000000..a733a6355 --- /dev/null +++ b/po/POTFILES @@ -0,0 +1,14 @@ +data/hu.kramo.GameShelf.desktop.in +data/hu.kramo.GameShelf.metainfo.xml.in + +src/main.py +src/window.py +src/window.blp +src/game.py +src/preferences.py + +src/gtk/game.blp +src/gtk/preferences.blp + +src/utils/create_details_window.py +src/utils/create_dialog.py diff --git a/po/gameshelf.pot b/po/gameshelf.pot new file mode 100644 index 000000000..eb6ad264d --- /dev/null +++ b/po/gameshelf.pot @@ -0,0 +1,259 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-12-26 14:29+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: game.blp:32 ../utils/create_details_window.py:72 +msgid "Title" +msgstr "" + +#: game.blp:62 game.blp:88 ../window.blp:132 +msgid "Play" +msgstr "" + +#: game.blp:69 game.blp:95 ../window.blp:374 ../window.blp:393 +msgid "Edit" +msgstr "" + +#: game.blp:74 ../window.blp:379 +msgid "Hide" +msgstr "" + +#: game.blp:79 game.blp:105 ../window.blp:384 ../window.blp:403 +msgid "Remove" +msgstr "" + +#: game.blp:100 ../window.blp:398 +msgid "Unhide" +msgstr "" + +#: preferences.blp:10 +msgid "General" +msgstr "" + +#: preferences.blp:13 +msgid "Exit after launching a game" +msgstr "" + +#: preferences.blp:25 +msgid "Steam install location" +msgstr "" + +#: preferences.blp:26 +msgid "Directory to use when importing games" +msgstr "" + +#: ../window.blp:6 ../window.blp:14 ../utils/steam_parser.py:98 +msgid "No Games Found" +msgstr "" + +#: ../window.blp:7 +msgid "Try a different search." +msgstr "" + +#: ../window.blp:15 +msgid "Use the + button to add games." +msgstr "" + +#: ../window.blp:22 +msgid "No Hidden Games" +msgstr "" + +#: ../window.blp:23 +msgid "Games you hide will appear here." +msgstr "" + +#: ../window.blp:29 +msgid "Game Shelf" +msgstr "" + +#: ../window.blp:52 +msgid "Game Details" +msgstr "" + +#: ../window.blp:90 +msgid "Game Title" +msgstr "" + +#: ../window.blp:241 +msgid "Hidden Games" +msgstr "" + +#: ../window.blp:299 +msgid "Sort" +msgstr "" + +#: ../window.blp:302 +msgid "A-Z" +msgstr "" + +#: ../window.blp:308 +msgid "Z-A" +msgstr "" + +#: ../window.blp:314 +msgid "Newest" +msgstr "" + +#: ../window.blp:320 +msgid "Oldest" +msgstr "" + +#: ../window.blp:326 +msgid "Last Played" +msgstr "" + +#: ../window.blp:333 +msgid "Show Hidden" +msgstr "" + +#: ../window.blp:341 +msgid "Preferences" +msgstr "" + +#: ../window.blp:346 +msgid "Keyboard Shortcuts" +msgstr "" + +#: ../window.blp:351 +msgid "About Game Shelf" +msgstr "" + +#: ../window.blp:360 +msgid "Add Game" +msgstr "" + +#: ../window.blp:365 +msgid "Import From Steam" +msgstr "" + +#. Create toast for undoing the remove action +#: ../main.py:126 +msgid " removed" +msgstr "" + +#: ../main.py:127 +msgid "Undo" +msgstr "" + +#: ../window.py:196 +msgid "Today" +msgstr "" + +#: ../window.py:198 +msgid "Yesterday" +msgstr "" + +#: ../window.py:222 +msgid "Added: " +msgstr "" + +#: ../window.py:223 +msgid "Last played: " +msgstr "" + +#: ../window.py:223 +msgid "Last played: Never" +msgstr "" + +#: ../utils/create_details_window.py:33 +msgid "Add New Game" +msgstr "" + +#: ../utils/create_details_window.py:37 +msgid "Confirm" +msgstr "" + +#: ../utils/create_details_window.py:39 +msgid "Edit Game Details" +msgstr "" + +#: ../utils/create_details_window.py:43 +msgid "Apply" +msgstr "" + +#: ../utils/create_details_window.py:46 +msgid "Images" +msgstr "" + +#: ../utils/create_details_window.py:73 +msgid "The title of the game" +msgstr "" + +#: ../utils/create_details_window.py:77 +msgid "Executable" +msgstr "" + +#: ../utils/create_details_window.py:78 +msgid "File to open or command to run when launching the game" +msgstr "" + +#: ../utils/create_details_window.py:86 +msgid "Cancel" +msgstr "" + +#: ../utils/create_details_window.py:133 ../utils/create_details_window.py:137 +msgid "Couldn't Add Game" +msgstr "" + +#: ../utils/create_details_window.py:133 ../utils/create_details_window.py:158 +msgid "Game title cannot be empty." +msgstr "" + +#: ../utils/create_details_window.py:137 ../utils/create_details_window.py:162 +msgid "Executable cannot be empty." +msgstr "" + +#: ../utils/create_details_window.py:158 ../utils/create_details_window.py:162 +msgid "Couldn't Apply Preferences" +msgstr "" + +#: ../utils/create_dialog.py:24 +msgid "Dismiss" +msgstr "" + +#: ../utils/steam_parser.py:49 +msgid "Couldn't Import Games" +msgstr "" + +#: ../utils/steam_parser.py:49 +msgid "Steam directory cannot be found." +msgstr "" + +#: ../utils/steam_parser.py:49 +msgid "Set Steam Location" +msgstr "" + +#: ../utils/steam_parser.py:98 +msgid "No new games found in Steam library." +msgstr "" + +#: ../utils/steam_parser.py:100 ../utils/steam_parser.py:102 +msgid "Steam Games Imported" +msgstr "" + +#: ../utils/steam_parser.py:100 +msgid "Successfully imported 1 game." +msgstr "" + +#: ../utils/steam_parser.py:102 +msgid "Successfully imported " +msgstr "" + +#: ../utils/steam_parser.py:102 +msgid " games." +msgstr "" diff --git a/po/hu.mo b/po/hu.mo new file mode 100644 index 000000000..043e27640 Binary files /dev/null and b/po/hu.mo differ diff --git a/po/hu.po b/po/hu.po new file mode 100644 index 000000000..48fe43920 --- /dev/null +++ b/po/hu.po @@ -0,0 +1,260 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-12-26 14:29+0100\n" +"PO-Revision-Date: 2022-12-26 14:33+0100\n" +"Last-Translator: kramo \n" +"Language-Team: \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.2.2\n" + +#: game.blp:32 ../utils/create_details_window.py:72 +msgid "Title" +msgstr "Cím" + +#: game.blp:62 game.blp:88 ../window.blp:132 +msgid "Play" +msgstr "Játék" + +#: game.blp:69 game.blp:95 ../window.blp:374 ../window.blp:393 +msgid "Edit" +msgstr "Szerkeszt" + +#: game.blp:74 ../window.blp:379 +msgid "Hide" +msgstr "Elrejt" + +#: game.blp:79 game.blp:105 ../window.blp:384 ../window.blp:403 +msgid "Remove" +msgstr "Eltávolít" + +#: game.blp:100 ../window.blp:398 +msgid "Unhide" +msgstr "Mutat" + +#: preferences.blp:10 +msgid "General" +msgstr "Általános" + +#: preferences.blp:13 +msgid "Exit after launching a game" +msgstr "Kilépés játék indítása után" + +#: preferences.blp:25 +msgid "Steam install location" +msgstr "Steam telepítés helye" + +#: preferences.blp:26 +msgid "Directory to use when importing games" +msgstr "Az importáláskor használatos mappa" + +#: ../window.blp:6 ../window.blp:14 ../utils/steam_parser.py:98 +msgid "No Games Found" +msgstr "Nem Találhatóak Játékok" + +#: ../window.blp:7 +msgid "Try a different search." +msgstr "Próbálkozz más kereséssel." + +#: ../window.blp:15 +msgid "Use the + button to add games." +msgstr "Használd a + gombot a játékok hozzáadásához." + +#: ../window.blp:22 +msgid "No Hidden Games" +msgstr "Nincsenek Rejtett Játékok" + +#: ../window.blp:23 +msgid "Games you hide will appear here." +msgstr "A rejtett játékaid itt lesznek megtalálhatók." + +#: ../window.blp:29 +msgid "Game Shelf" +msgstr "Játékpolc" + +#: ../window.blp:52 +msgid "Game Details" +msgstr "Játék Részletei" + +#: ../window.blp:90 +msgid "Game Title" +msgstr "Cím" + +#: ../window.blp:241 +msgid "Hidden Games" +msgstr "Rejtett Játékok" + +#: ../window.blp:299 +msgid "Sort" +msgstr "Rendezés" + +#: ../window.blp:302 +msgid "A-Z" +msgstr "A-Z" + +#: ../window.blp:308 +msgid "Z-A" +msgstr "Z-A" + +#: ../window.blp:314 +msgid "Newest" +msgstr "Legújabb" + +#: ../window.blp:320 +msgid "Oldest" +msgstr "Legrégebbi" + +#: ../window.blp:326 +msgid "Last Played" +msgstr "Legutóbb játszott" + +#: ../window.blp:333 +msgid "Show Hidden" +msgstr "Rejtett Játékok" + +#: ../window.blp:341 +msgid "Preferences" +msgstr "Beállítások" + +#: ../window.blp:346 +msgid "Keyboard Shortcuts" +msgstr "Billentyűparancsok" + +#: ../window.blp:351 +msgid "About Game Shelf" +msgstr "A Játékpolcról" + +#: ../window.blp:360 +msgid "Add Game" +msgstr "Játék Hozzáadása" + +#: ../window.blp:365 +msgid "Import From Steam" +msgstr "Importálás Steam-ből" + +#. Create toast for undoing the remove action +#: ../main.py:126 +msgid " removed" +msgstr " eltávolítva" + +#: ../main.py:127 +msgid "Undo" +msgstr "Visszavonás" + +#: ../window.py:196 +msgid "Today" +msgstr "Ma" + +#: ../window.py:198 +msgid "Yesterday" +msgstr "Tegnap" + +#: ../window.py:222 +msgid "Added: " +msgstr "Hozzáadva: " + +#: ../window.py:223 +msgid "Last played: " +msgstr "Legutóbb játszva: " + +#: ../window.py:223 +msgid "Last played: Never" +msgstr "Legutóbb játszva: Soha" + +#: ../utils/create_details_window.py:33 +msgid "Add New Game" +msgstr "Új Játék Hozzáadása" + +#: ../utils/create_details_window.py:37 +msgid "Confirm" +msgstr "Megerősít" + +#: ../utils/create_details_window.py:39 +msgid "Edit Game Details" +msgstr "Játék Részleteinek Szerkesztése" + +#: ../utils/create_details_window.py:43 +msgid "Apply" +msgstr "Alkalmaz" + +#: ../utils/create_details_window.py:46 +msgid "Images" +msgstr "Képek" + +#: ../utils/create_details_window.py:73 +msgid "The title of the game" +msgstr "A játék címe" + +#: ../utils/create_details_window.py:77 +msgid "Executable" +msgstr "Program" + +#: ../utils/create_details_window.py:78 +msgid "File to open or command to run when launching the game" +msgstr "Fájl megnyitása vagy parancs futtatása a játék indításakor" + +#: ../utils/create_details_window.py:86 +msgid "Cancel" +msgstr "Mégse" + +#: ../utils/create_details_window.py:133 ../utils/create_details_window.py:137 +msgid "Couldn't Add Game" +msgstr "Nem Lehet Hozzáadni a Játékot" + +#: ../utils/create_details_window.py:133 ../utils/create_details_window.py:158 +msgid "Game title cannot be empty." +msgstr "A cím nem lehet üres." + +#: ../utils/create_details_window.py:137 ../utils/create_details_window.py:162 +msgid "Executable cannot be empty." +msgstr "A program nem lehet üres." + +#: ../utils/create_details_window.py:158 ../utils/create_details_window.py:162 +msgid "Couldn't Apply Preferences" +msgstr "Nem Lehet Menteni a Beállításokat" + +#: ../utils/create_dialog.py:24 +msgid "Dismiss" +msgstr "Rendben" + +#: ../utils/steam_parser.py:49 +msgid "Couldn't Import Games" +msgstr "Nem Lehet Importálni a Játékot" + +#: ../utils/steam_parser.py:49 +msgid "Steam directory cannot be found." +msgstr "A Steam mappa nem található." + +#: ../utils/steam_parser.py:49 +msgid "Set Steam Location" +msgstr "Steam Mappa Kiválasztása" + +#: ../utils/steam_parser.py:98 +msgid "No new games found in Steam library." +msgstr "Nem találhatók új játékok a Steam könyvtárban." + +#: ../utils/steam_parser.py:100 ../utils/steam_parser.py:102 +msgid "Steam Games Imported" +msgstr "Steam Játékok Importálva" + +#: ../utils/steam_parser.py:100 +msgid "Successfully imported 1 game." +msgstr "1 játék sikeresen importálva." + +#: ../utils/steam_parser.py:102 +msgid "Successfully imported " +msgstr "Sikeresen importálva " + +#: ../utils/steam_parser.py:102 +msgid " games." +msgstr " játék." diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 000000000..601332ea9 --- /dev/null +++ b/po/meson.build @@ -0,0 +1 @@ +i18n.gettext('gameshelf', preset: 'glib') diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/assets/library_placeholder.svg b/src/assets/library_placeholder.svg new file mode 100644 index 000000000..5f6e956e0 --- /dev/null +++ b/src/assets/library_placeholder.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/game.py b/src/game.py new file mode 100644 index 000000000..32fa58203 --- /dev/null +++ b/src/game.py @@ -0,0 +1,40 @@ +# game.py +# +# Copyright 2022 kramo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from gi.repository import Gtk + +@Gtk.Template(resource_path='/hu/kramo/GameShelf/gtk/game.ui') +class game(Gtk.Box): + __gtype_name__ = 'game' + + title = Gtk.Template.Child() + cover = Gtk.Template.Child() + cover_button = Gtk.Template.Child() + menu_button = Gtk.Template.Child() + hidden_game_options = Gtk.Template.Child() + + def __init__(self, title, pixbuf, game_id, **kwargs): + super().__init__(**kwargs) + + self.name = title + self.pixbuf = pixbuf + self.game_id = game_id + + self.title.set_label(title) + self.cover.set_pixbuf(pixbuf) diff --git a/src/gameshelf.gresource.xml b/src/gameshelf.gresource.xml new file mode 100644 index 000000000..e3c73a7eb --- /dev/null +++ b/src/gameshelf.gresource.xml @@ -0,0 +1,10 @@ + + + + window.ui + gtk/help-overlay.ui + gtk/game.ui + gtk/preferences.ui + assets/library_placeholder.svg + + diff --git a/src/gameshelf.in b/src/gameshelf.in new file mode 100755 index 000000000..004cea97c --- /dev/null +++ b/src/gameshelf.in @@ -0,0 +1,46 @@ +#!@PYTHON@ + +# gameshelf.in +# +# Copyright 2022 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 +import sys +import signal +import locale +import gettext + +VERSION = '@VERSION@' +pkgdatadir = '@pkgdatadir@' +localedir = '@localedir@' + +sys.path.insert(1, pkgdatadir) +signal.signal(signal.SIGINT, signal.SIG_DFL) +locale.bindtextdomain('gameshelf', localedir) +locale.textdomain('gameshelf') +gettext.install('gameshelf', localedir) + +if __name__ == '__main__': + import gi + + from gi.repository import Gio + resource = Gio.Resource.load(os.path.join(pkgdatadir, 'gameshelf.gresource')) + resource._register() + + from gameshelf import main + sys.exit(main.main(VERSION)) diff --git a/src/gtk/game.blp b/src/gtk/game.blp new file mode 100644 index 000000000..51f189a12 --- /dev/null +++ b/src/gtk/game.blp @@ -0,0 +1,109 @@ +using Gtk 4.0; +using Adw 1; + +template game : Box { + orientation: vertical; + halign: center; + valign: start; + + Button cover_button { + Picture cover { + width-request: 200; + height-request: 300; + hexpand: true; + vexpand: true; + + styles [ + "card", + ] + } + + styles [ + "card", + "flat", + ] + } + + Adw.Clamp { + maximum-size: 200; + + Box { + Label title { + label: _("Title"); + ellipsize: end; + hexpand: true; + halign: start; + margin-start: 12; + } + + MenuButton menu_button { + icon-name: "view-more-symbolic"; + margin-top: 6; + margin-bottom: 6; + margin-end: 6; + margin-start: 6; + menu-model: game_options; + + styles [ + "flat", + ] + } + } + } + + styles [ + "card", + ] +} + +menu game_options { + section { + item { + label: _("Play"); + action: "app.launch_game"; + } + } + + section { + item { + label: _("Edit"); + action: "app.edit_details"; + } + + item { + label: _("Hide"); + action: "app.hide_game"; + } + + item { + label: _("Remove"); + action: "app.remove_game"; + } + } +} + +menu hidden_game_options { + section { + item { + label: _("Play"); + action: "app.launch_game"; + } + } + + section { + item { + label: _("Edit"); + action: "app.edit_details"; + } + + item { + label: _("Unhide"); + action: "app.hide_game"; + } + + item { + label: _("Remove"); + action: "app.remove_game"; + } + } +} diff --git a/src/gtk/help-overlay.blp b/src/gtk/help-overlay.blp new file mode 100644 index 000000000..2ffe6aede --- /dev/null +++ b/src/gtk/help-overlay.blp @@ -0,0 +1,34 @@ +using Gtk 4.0; + +ShortcutsWindow help_overlay { + modal: true; + + ShortcutsSection { + section-name: "shortcuts"; + max-height: 10; + + ShortcutsGroup { + title: C_("shortcut window", "General"); + + ShortcutsShortcut { + title: C_("shortcut window", "Quit"); + action-name: "app.quit"; + } + + ShortcutsShortcut { + title: C_("shortcut window", "Search"); + action-name: "win.toggle_search"; + } + + ShortcutsShortcut { + title: C_("shortcut window", "Shortcuts"); + action-name: "win.show-help-overlay"; + } + + ShortcutsShortcut { + title: C_("shortcut window", "Undo"); + action-name: "win.undo_remove"; + } + } + } +} diff --git a/src/gtk/preferences.blp b/src/gtk/preferences.blp new file mode 100644 index 000000000..0a019571a --- /dev/null +++ b/src/gtk/preferences.blp @@ -0,0 +1,35 @@ +using Gtk 4.0; +using Adw 1; + +template PreferencesWindow : Adw.PreferencesWindow { + search-enabled: false; + default-height: 400; + + Adw.PreferencesPage { + Adw.PreferencesGroup { + title: _("General"); + + Adw.ActionRow { + title: _("Exit after launching a game"); + + Switch exit_after_launch_switch { + valign: center; + } + } + } + + Adw.PreferencesGroup { + title: "Steam"; + + Adw.ActionRow { + title: _("Steam install location"); + subtitle: _("Directory to use when importing games"); + + Button steam_file_chooser_button { + icon-name: "folder-symbolic"; + valign: center; + } + } + } + } +} diff --git a/src/main.py b/src/main.py new file mode 100644 index 000000000..fe6318046 --- /dev/null +++ b/src/main.py @@ -0,0 +1,151 @@ +# main.py +# +# Copyright 2022 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 gi, sys, os, time, json + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +from gi.repository import Gtk, Gio, GLib, Adw + +from .window import GameShelfWindow +from .preferences import PreferencesWindow +from .toggle_hidden import toggle_hidden +from .save_games import save_games +from .run_command import run_command +from .steam_parser import steam_parser +from .create_details_window import create_details_window + +class GameShelfApplication(Adw.Application): + def __init__(self): + super().__init__(application_id="hu.kramo.GameShelf", flags=Gio.ApplicationFlags.FLAGS_NONE) + self.create_action("quit", self.on_quit_action, ["q"]) + self.create_action("about", self.on_about_action) + self.create_action("preferences", self.on_preferences_action) + self.create_action("steam_import", self.on_steam_import_action) + self.create_action("launch_game", self.on_launch_game_action) + self.create_action("hide_game", self.on_hide_game_action) + self.create_action("edit_details", self.on_edit_details_action) + self.create_action("add_game", self.on_add_game_action) + self.create_action("remove_game", self.on_remove_game_action) + + def do_activate(self): + + # Create the main window + win = self.props.active_window + if not win: + win = GameShelfWindow(application=self) + + win.present() + + # Create actions for the main window + self.create_action("show_hidden", win.on_show_hidden_action, None, win) + self.create_action("go_back", win.on_go_back_action, ["Left"], win) + self.create_action("go_to_parent", win.on_go_to_parent_action, ["Up"], win) + self.create_action("toggle_search", win.on_toggle_search_action, ["f"], win) + self.create_action("escape", win.on_escape_action, ["Escape"], win) + self.create_action("undo_remove", win.on_undo_remove_action, ["z"], win) + win.sort = Gio.SimpleAction.new_stateful("sort_by", GLib.VariantType.new("s"), GLib.Variant("s", "a-z")) + win.add_action(win.sort) + win.sort.connect("activate", win.on_sort_action) + win.on_sort_action(win.sort, win.schema.get_value("sort-mode")) + + def on_about_action(self, widget, callback=None): + about = Adw.AboutWindow(transient_for=self.props.active_window, + application_name="Game Shelf", + application_icon="hu.kramo.GameShelf", + developer_name="kramo", + version="0.1.0", + developers=["kramo"], + copyright="© 2022 kramo", + license_type=Gtk.License.GPL_3_0) + about.present() + + def on_preferences_action(self, widget, callback=None): + PreferencesWindow(self.props.active_window).present() + + def on_steam_import_action(self, widget, callback=None): + games = steam_parser(self.props.active_window, self.on_steam_import_action) + save_games(games) + self.props.active_window.update_games(games.keys()) + + def on_launch_game_action(self, widget, callback=None): + + # Launch the game and update the last played value + self.props.active_window.games[self.props.active_window.active_game_id]["last_played"] = int(time.time()) + save_games({self.props.active_window.active_game_id : self.props.active_window.games[self.props.active_window.active_game_id]}) + self.props.active_window.update_games([self.props.active_window.active_game_id]) + run_command(self.props.active_window, self.props.active_window.games[self.props.active_window.active_game_id]["executable"]) + + if self.props.active_window.stack.get_visible_child() == self.props.active_window.overview: + self.props.active_window.show_overview(None, self.props.active_window.active_game_id) + + def on_hide_game_action(self, widget, callback=None): + if self.props.active_window.stack.get_visible_child() == self.props.active_window.overview: + self.props.active_window.on_go_back_action(None, None) + toggle_hidden(self.props.active_window.active_game_id) + self.props.active_window.update_games([self.props.active_window.active_game_id]) + + def on_edit_details_action(self, widget, callback=None): + create_details_window(self.props.active_window, self.props.active_window.active_game_id) + + def on_add_game_action(self, widget, callback=None): + create_details_window(self.props.active_window) + + def on_remove_game_action(self, widget, callback=None): + + # Add "removed=True" to the game properties so it can be deleted on next init + game_id = self.props.active_window.active_game_id + open_file = open(os.path.join(os.path.join(os.environ.get("XDG_DATA_HOME"), "games", game_id + ".json")), "r") + data = json.loads(open_file.read()) + open_file.close() + data["removed"] = True + save_games({game_id : data}) + + self.props.active_window.update_games([game_id]) + if self.props.active_window.stack.get_visible_child() == self.props.active_window.overview: + self.props.active_window.on_go_back_action(None, None) + + # Create toast for undoing the remove action + toast = Adw.Toast.new(self.props.active_window.games[game_id]["name"] + (_(" removed"))) + toast.set_button_label(_("Undo")) + toast.connect("button-clicked", self.props.active_window.on_undo_remove_action, game_id) + toast.set_priority(Adw.ToastPriority.HIGH) + self.props.active_window.toasts[game_id] = toast + self.props.active_window.toast_overlay.add_toast(toast) + + def on_quit_action(self, widget, callback=None): + self.quit() + + def create_action(self, name, callback, shortcuts=None, win=None): + action = Gio.SimpleAction.new(name, None) + action.connect("activate", callback) + if not win: + self.add_action(action) + if shortcuts: + self.set_accels_for_action(f"app.{name}", shortcuts) + else: + win.add_action(action) + if shortcuts: + self.set_accels_for_action(f"win.{name}", shortcuts) + +def main(version): + app = GameShelfApplication() + return app.run(sys.argv) + diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 000000000..f0721560e --- /dev/null +++ b/src/meson.build @@ -0,0 +1,57 @@ +pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) +moduledir = join_paths(pkgdatadir, 'gameshelf') +gnome = import('gnome') + +blueprints = custom_target('blueprints', + input: files( + 'gtk/help-overlay.blp', + 'window.blp', + 'gtk/game.blp', + 'gtk/preferences.blp' + ), + output: '.', + command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], +) + +gnome.compile_resources('gameshelf', + 'gameshelf.gresource.xml', + gresource_bundle: true, + install: true, + install_dir: pkgdatadir, + dependencies: blueprints, +) + +python = import('python') + +conf = configuration_data() +conf.set('PYTHON', python.find_installation('python3').path()) +conf.set('VERSION', meson.project_version()) +conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) +conf.set('pkgdatadir', pkgdatadir) + +configure_file( + input: 'gameshelf.in', + output: 'gameshelf', + configuration: conf, + install: true, + install_dir: get_option('bindir') +) + +gameshelf_sources = [ + '__init__.py', + 'main.py', + 'window.py', + 'preferences.py', + 'game.py', + 'utils/steam_parser.py', + 'utils/run_command.py', + 'utils/get_games.py', + 'utils/get_cover.py', + 'utils/save_games.py', + 'utils/save_cover.py', + 'utils/toggle_hidden.py', + 'utils/create_dialog.py', + 'utils/create_details_window.py' +] + +install_data(gameshelf_sources, install_dir: moduledir) diff --git a/src/preferences.py b/src/preferences.py new file mode 100644 index 000000000..c3ac23651 --- /dev/null +++ b/src/preferences.py @@ -0,0 +1,47 @@ +# preferences.py +# +# Copyright 2022 kramo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from gi.repository import Adw, Gtk, Gio, GLib + +@Gtk.Template(resource_path='/hu/kramo/GameShelf/gtk/preferences.ui') +class PreferencesWindow(Adw.PreferencesWindow): + __gtype_name__ = 'PreferencesWindow' + + exit_after_launch_switch = Gtk.Template.Child() + steam_file_chooser_button = Gtk.Template.Child() + + def __init__(self, parent_widget, **kwargs): + super().__init__(**kwargs) + + self.set_transient_for(parent_widget) + schema = parent_widget.schema + schema.bind("exit-after-launch", self.exit_after_launch_switch, "active", Gio.SettingsBindFlags.DEFAULT) + + filechooser = Gtk.FileDialog.new() + + def set_steam_dir(source, result, user_data): + try: + schema.set_string("steam-location", filechooser.select_folder_finish(result).get_path()) + except GLib.GError: + pass + + def choose_folder(widget): + filechooser.select_folder(parent_widget, None, None, set_steam_dir, None) + + self.steam_file_chooser_button.connect("clicked", choose_folder) diff --git a/src/utils/create_details_window.py b/src/utils/create_details_window.py new file mode 100644 index 000000000..cf78f9704 --- /dev/null +++ b/src/utils/create_details_window.py @@ -0,0 +1,194 @@ +# create_details_window.py +# +# Copyright 2022 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 + +def create_details_window(parent_widget, game_id = None): + import os, json, time + from gi.repository import Adw, Gtk, GLib, GdkPixbuf + from .create_dialog import create_dialog + from .save_games import save_games + from .save_cover import save_cover + + window = Adw.Window.new() + + games = parent_widget.games + pixbuf = None + + if game_id == None: + window.set_title(_("Add New Game")) + cover = Gtk.Picture.new_for_pixbuf(parent_widget.placeholder_pixbuf) + name = Gtk.Entry.new() + executable = Gtk.Entry.new() + apply_button = Gtk.Button.new_with_label(_("Confirm")) + else: + window.set_title(_("Edit Game Details")) + cover = Gtk.Picture.new_for_pixbuf((parent_widget.visible_widgets | parent_widget.hidden_widgets)[game_id].pixbuf) + name = Gtk.Entry.new_with_buffer(Gtk.EntryBuffer.new(games[game_id]["name"], -1)) + executable = Gtk.Entry.new_with_buffer(Gtk.EntryBuffer.new((games[game_id]["executable"]), -1)) + apply_button = Gtk.Button.new_with_label(_("Apply")) + + file_filter = Gtk.FileFilter.new() + file_filter.set_name(_("Images")) + file_filter.add_pixbuf_formats() + filechooser = Gtk.FileDialog.new() + filechooser.set_current_filter(file_filter) + + cover.add_css_class("card") + cover.set_size_request(200, 300) + + cover_button = Gtk.Button.new_from_icon_name("document-edit-symbolic") + cover_button.set_halign(Gtk.Align.END) + cover_button.set_valign(Gtk.Align.END) + cover_button.set_margin_bottom(6) + cover_button.set_margin_end(6) + cover_button.add_css_class("circular") + cover_button.add_css_class("osd") + + cover_overlay = Gtk.Overlay.new() + cover_overlay.set_child(cover) + cover_overlay.add_overlay(cover_button) + cover_overlay.set_halign(Gtk.Align.CENTER) + cover_overlay.set_valign(Gtk.Align.CENTER) + + cover_group = Adw.PreferencesGroup.new() + cover_group.add(cover_overlay) + + title_group = Adw.PreferencesGroup.new() + title_group.set_title(_("Title")) + title_group.set_description(_("The title of the game")) + title_group.add(name) + + exec_group = Adw.PreferencesGroup.new() + exec_group.set_title(_("Executable")) + exec_group.set_description(_("File to open or command to run when launching the game")) + exec_group.add(executable) + + general_page = Adw.PreferencesPage.new() + general_page.add(cover_group) + general_page.add(title_group) + general_page.add(exec_group) + + cancel_button = Gtk.Button.new_with_label(_("Cancel")) + + apply_button.add_css_class("suggested-action") + + header_bar = Adw.HeaderBar.new() + header_bar.set_show_start_title_buttons(False) + header_bar.set_show_end_title_buttons(False) + header_bar.pack_start(cancel_button) + header_bar.pack_end(apply_button) + + main_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + main_box.append(header_bar) + main_box.append(general_page) + + window.set_modal(True) + window.set_default_size(500, 650) + window.set_content(main_box) + window.set_transient_for(parent_widget) + + def choose_cover(widget): + filechooser.open(window, None, None, set_cover, None) + + def set_cover(source, result, _): + nonlocal pixbuf + + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filechooser.open_finish(result).get_path(), 200, 300, False) + cover.set_pixbuf(pixbuf) + except GLib.GError: + return + + def close_window(widget, callback=None): + window.close() + + def apply_preferences(widget, callback=None): + nonlocal pixbuf + nonlocal game_id + + values = {} + + games_dir = os.path.join(os.environ.get("XDG_DATA_HOME"), "games") + final_name = name.get_buffer().get_text() + final_executable = executable.get_buffer().get_text() + + if game_id == None: + + if final_name == "": + create_dialog(window, _("Couldn't Add Game"), _("Game title cannot be empty.")) + return + + if final_executable == "": + create_dialog(window, _("Couldn't Add Game"), _("Executable cannot be empty.")) + return + + numbers = [0] + + for game in games: + if "imported_" in game: + numbers.append(int(game.replace("imported_", ""))) + + game_id = "imported_" + str(max(numbers)+1) + + games[game_id] = {} + + values["game_id"] = game_id + values["hidden"] = False + values["source"] = "imported" + values["added"] = int(time.time()) + values["last_played"] = 0 + + else: + if final_name == "": + create_dialog(window, _("Couldn't Apply Preferences"), _("Game title cannot be empty.")) + return + + if final_executable == "": + create_dialog(window, _("Couldn't Apply Preferences"), _("Executable cannot be empty.")) + return + + if pixbuf != None: + values["pixbuf_options"] = save_cover(None, parent_widget, None, pixbuf, game_id) + + values["name"] = final_name + values["executable"] = final_executable + + games[game_id].update(values) + save_games(games) + parent_widget.update_games([game_id]) + if parent_widget.stack.get_visible_child() == parent_widget.overview: + parent_widget.show_overview(None, game_id) + window.close() + parent_widget.show_overview(None, game_id) + + def focus_executable(widget): + window.set_focus(executable) + + cover_button.connect("clicked", choose_cover) + cancel_button.connect("clicked", close_window) + apply_button.connect("clicked", apply_preferences) + name.connect("activate", focus_executable) + executable.connect("activate", apply_preferences) + + shortcut_controller = Gtk.ShortcutController.new() + shortcut_controller.add_shortcut(Gtk.Shortcut.new(Gtk.ShortcutTrigger.parse_string('Escape'), Gtk.CallbackAction.new(close_window))) + + window.add_controller(shortcut_controller) + window.set_focus(name) + window.present() + diff --git a/src/utils/create_dialog.py b/src/utils/create_dialog.py new file mode 100644 index 000000000..fc33d11f9 --- /dev/null +++ b/src/utils/create_dialog.py @@ -0,0 +1,30 @@ +# create_dialog.py +# +# Copyright 2022 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 + +def create_dialog(parent_widget, heading, body, extra_option=None, extra_label=None): + from gi.repository import Adw, Gtk + + dialog = Adw.MessageDialog.new(parent_widget, _(heading), body) + dialog.add_response("dismiss", _("Dismiss")) + + if extra_option: + dialog.add_response(extra_option, _(extra_label)) + + Gtk.Window.present(dialog) + return dialog diff --git a/src/utils/get_cover.py b/src/utils/get_cover.py new file mode 100644 index 000000000..fdf0c40af --- /dev/null +++ b/src/utils/get_cover.py @@ -0,0 +1,35 @@ +# get_cover.py +# +# Copyright 2022 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 + +def get_cover(game, parent_widget): + from gi.repository import GdkPixbuf + import os + + cover_path = os.path.join(os.environ.get("XDG_DATA_HOME"), "covers", game["game_id"] + ".dat") + + if os.path.isfile(cover_path) == False: + return parent_widget.placeholder_pixbuf + + open_file = open((cover_path), "rb") + data = open_file.read() + open_file.close() + try: + return GdkPixbuf.Pixbuf.new_from_data(data, *parent_widget.games[game["game_id"]]["pixbuf_options"]) + except KeyError: + return parent_widget.placeholder_pixbuf diff --git a/src/utils/get_games.py b/src/utils/get_games.py new file mode 100644 index 000000000..680cd6a3a --- /dev/null +++ b/src/utils/get_games.py @@ -0,0 +1,34 @@ +# get_games.py +# +# Copyright 2022 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 + +def get_games(): + import os, json + + games_dir = os.path.join(os.environ.get("XDG_DATA_HOME"), "games") + games = {} + + if os.path.exists(games_dir) == False: + return {} + + for game in os.listdir(games_dir): + open_file = open(os.path.join(games_dir, game), "r") + data = json.loads(open_file.read()) + open_file.close() + games[data["game_id"]] = data + return games diff --git a/src/utils/run_command.py b/src/utils/run_command.py new file mode 100644 index 000000000..aa90863ad --- /dev/null +++ b/src/utils/run_command.py @@ -0,0 +1,28 @@ +# run_command.py +# +# Copyright 2022 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 + +def run_command(parent_widget, executable): + import subprocess, sys + + from gi.repository import Gio + + subprocess.Popen(["flatpak-spawn --host " + executable], shell=True, start_new_session=True) + + if Gio.Settings.new("hu.kramo.GameShelf").get_boolean("exit-after-launch") == True: + sys.exit() diff --git a/src/utils/save_cover.py b/src/utils/save_cover.py new file mode 100644 index 000000000..707d89b13 --- /dev/null +++ b/src/utils/save_cover.py @@ -0,0 +1,39 @@ +# save_cover.py +# +# Copyright 2022 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 + +def save_cover(game, parent_widget, file_path, pixbuf = None, game_id = None): + from gi.repository import GdkPixbuf + import os + + covers_dir = os.path.join(os.environ.get("XDG_DATA_HOME"), "covers") + if os.path.exists(covers_dir) == False: + os.makedirs(covers_dir) + + if game_id == None: + game_id = game["game_id"] + + cover_path = os.path.join(covers_dir, game_id + ".dat") + + if pixbuf == None: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(file_path, 200, 300, False) + + open_file = open((cover_path), "wb") + open_file.write(bytes(pixbuf.get_pixels())) + open_file.close() + return [pixbuf.get_colorspace(), pixbuf.get_has_alpha(), pixbuf.get_bits_per_sample(), pixbuf.get_width(), pixbuf.get_height(), pixbuf.get_rowstride()] diff --git a/src/utils/save_games.py b/src/utils/save_games.py new file mode 100644 index 000000000..1f1485f69 --- /dev/null +++ b/src/utils/save_games.py @@ -0,0 +1,31 @@ +# save_games.py +# +# Copyright 2022 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 + +def save_games(games): + import os, json + games_dir = os.path.join(os.environ.get("XDG_DATA_HOME"), "games") + existing = [] + + if os.path.exists(games_dir) == False: + os.makedirs(games_dir) + + for game in games: + open_file = open(os.path.join(games_dir, game + ".json"), "w") + open_file.write(json.dumps(games[game], indent=4)) + open_file.close() diff --git a/src/utils/steam_parser.py b/src/utils/steam_parser.py new file mode 100644 index 000000000..762cda01e --- /dev/null +++ b/src/utils/steam_parser.py @@ -0,0 +1,103 @@ +# steam_parser.py +# +# Copyright 2022 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 + +def steam_parser(parent_widget, action): + import os, re, time + + from gi.repository import Gtk, GLib + + from .create_dialog import create_dialog + from .save_cover import save_cover + + schema = parent_widget.schema + steam_dir = os.path.expanduser(os.path.join(schema.get_string("steam-location"))) + + def steam_not_found(): + filechooser = Gtk.FileDialog.new() + + def set_steam_dir(source, result, _): + try: + schema.set_string("steam-location", filechooser.select_folder_finish(result).get_path()) + steam_dir = steam_dir = os.path.join(schema.get_string("steam-location")) + action(None, None) + except GLib.GError: + return + + def choose_folder(widget): + filechooser.select_folder(parent_widget, None, None, set_steam_dir, None) + + def response(widget, response): + if response == "choose_folder": + choose_folder(widget) + + create_dialog(parent_widget, _("Couldn't Import Games"), _("Steam directory cannot be found."), "choose_folder", _("Set Steam Location")).connect("response", response) + + if os.path.exists(os.path.join(steam_dir, "steamapps")) == True: + pass + elif os.path.exists(os.path.join(steam_dir, "steam", "steamapps")) == True: + schema.set_string("steam-location", os.path.join(steam_dir, "steam")) + elif os.path.exists(os.path.join(steam_dir, "Steam", "steamapps")) == True: + schema.set_string("steam-location", os.path.join(steam_dir, "Steam")) + else: + steam_not_found() + return {} + + steam_dir = os.path.join(schema.get_string("steam-location")) + + appmanifests = [] + datatypes = ["appid", "name"] + steam_games = {} + current_time = int(time.time()) + + for file in os.listdir(os.path.join(steam_dir, "steamapps")): + path = os.path.join(steam_dir, "steamapps", file) + if os.path.isfile(path) and "appmanifest" in file: + appmanifests.append(path) + + for appmanifest in appmanifests: + values = {} + file = open(appmanifest, "r") + data = file.read() + file.close() + for datatype in datatypes: + value = re.findall("\"" + datatype + "\"\t\t\"(.*)\"\n", data) + values [datatype] = value[0] + + values["game_id"] = "steam_" + values["appid"] + + if values["game_id"] in parent_widget.games and "removed" not in parent_widget.games[values["game_id"]].keys(): + continue + + values["executable"] = "xdg-open steam://rungameid/" + values["appid"] + values["hidden"] = False + values["source"] = "steam" + values["added"] = current_time + values["last_played"] = 0 + + if os.path.isfile(os.path.join(steam_dir, "appcache", "librarycache", values["appid"] + "_library_600x900.jpg")) == True: + values["pixbuf_options"] = save_cover(values, parent_widget, os.path.join(steam_dir, "appcache", "librarycache", values["appid"] + "_library_600x900.jpg")) + steam_games[values["game_id"]] = values + + if len(steam_games) == 0: + create_dialog(parent_widget, _("No Games Found"), _("No new games found in Steam library.")) + elif len(steam_games) == 1: + create_dialog(parent_widget, _("Steam Games Imported"), _("Successfully imported 1 game.")) + elif len(steam_games) > 1: + create_dialog(parent_widget, _("Steam Games Imported"), _("Successfully imported ") + str(len(steam_games)) + _(" games.")) + return steam_games diff --git a/src/utils/toggle_hidden.py b/src/utils/toggle_hidden.py new file mode 100644 index 000000000..fc460144b --- /dev/null +++ b/src/utils/toggle_hidden.py @@ -0,0 +1,33 @@ +# toggle_hidden.py +# +# Copyright 2022 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 + +def toggle_hidden(game): + import os, json + games_dir = os.path.join(os.environ.get("XDG_DATA_HOME"), "games") + + if os.path.exists(games_dir) == False: + return + + file = open(os.path.join(games_dir, game + ".json"), "r") + data = json.loads(file.read()) + file.close() + file = open(os.path.join(games_dir, game + ".json"), "w") + data["hidden"] = not data["hidden"] + file.write(json.dumps(data, indent=4)) + file.close() diff --git a/src/window.blp b/src/window.blp new file mode 100644 index 000000000..463670560 --- /dev/null +++ b/src/window.blp @@ -0,0 +1,407 @@ +using Gtk 4.0; +using Adw 1; + +Adw.StatusPage notice_no_results { + icon-name: "system-search-symbolic"; + title: _("No Games Found"); + description: _("Try a different search."); + vexpand: true; + valign: center; +} + +Adw.StatusPage notice_empty { + icon-name: "applications-games-symbolic"; + title: _("No Games Found"); + description: _("Use the + button to add games."); + vexpand: true; + valign: center; +} + +Adw.StatusPage hidden_notice_empty { + icon-name: "view-conceal-symbolic"; + title: _("No Hidden Games"); + description: _("Games you hide will appear here."); + vexpand: true; + valign: center; +} + +template GameShelfWindow : Adw.ApplicationWindow { + title: _("Game Shelf"); + default-width: 1110; + default-height: 820; + + Adw.ToastOverlay toast_overlay { + Stack stack { + visible-child: library_view; + transition-type: over_left; + + Overlay overview { + [overlay] + Box overview_box { + orientation: vertical; + + Adw.HeaderBar { + [start] + Button back_button { + action-name: "win.go_back"; + icon-name: "go-previous-symbolic"; + } + + [title] + Adw.WindowTitle overview_header_bar_title { + title: _("Game Details"); + } + + styles [ + "flat", + ] + } + + Adw.Bin { + hexpand: true; + vexpand: true; + + Box { + halign: center; + valign: center; + margin-start: 24; + margin-end: 24; + margin-top: 24; + margin-bottom: 24; + + Picture overview_cover { + halign: end; + valign: start; + width-request: 200; + height-request: 300; + + styles [ + "card", + ] + } + + Box { + orientation: vertical; + margin-start: 48; + vexpand: true; + valign: center; + + Label overview_title { + label: _("Game Title"); + hexpand: true; + halign: start; + wrap: true; + wrap-mode: word_char; + natural-wrap-mode: word; + + styles [ + "title-1", + ] + } + Label overview_added { + margin-top: 12; + hexpand: true; + halign: start; + wrap: true; + wrap-mode: word_char; + natural-wrap-mode: word; + + styles [ + "dim-label", + ] + } + Label overview_last_played { + margin-top: 6; + hexpand: true; + halign: start; + wrap: true; + wrap-mode: word_char; + natural-wrap-mode: word; + + styles [ + "dim-label", + ] + } + Box { + hexpand: true; + vexpand: true; + valign: center; + + Button overview_launch { + action-name: "app.launch_game"; + label: _("Play"); + halign: start; + margin-top: 24; + + styles [ + "suggested-action", + "pill", + ] + } + + MenuButton overview_menu_button { + icon-name: "view-more-symbolic"; + hexpand: true; + vexpand: true; + halign: start; + valign: center; + margin-top: 24; + margin-start: 6; + + styles [ + "circular", + ] + } + } + } + } + } + } + + styles [ + "background", + ] + + Picture overview_blurred_cover { + opacity: 0.2; + can-shrink: true; + keep-aspect-ratio: false; + hexpand: true; + vexpand: true; + } + } + + Box library_view { + orientation: vertical; + + Adw.HeaderBar header_bar { + [start] + MenuButton { + icon-name: "list-add-symbolic"; + menu-model: add_games; + } + + [end] + MenuButton { + icon-name: "open-menu-symbolic"; + menu-model: primary_menu; + } + + [end] + ToggleButton search_button { + icon-name: "system-search-symbolic"; + action-name: "win.toggle_search"; + } + } + + SearchBar search_bar { + Adw.Clamp { + maximum-size: 500; + tightening-threshold: 500; + + SearchEntry search_entry { + hexpand: true; + } + } + } + + Adw.Bin library_bin { + ScrolledWindow scrolledwindow { + hexpand: true; + vexpand: true; + + FlowBox library { + homogeneous: true; + halign: center; + valign: start; + column-spacing: 12; + row-spacing: 12; + margin-top: 16; + margin-bottom: 16; + margin-start: 16; + margin-end: 16; + selection-mode: none; + } + } + } + } + + Box hidden_library_view { + orientation: vertical; + + Adw.HeaderBar hidden_header_bar { + [start] + Button hidden_back_button { + action-name: "win.go_back"; + icon-name: "go-previous-symbolic"; + } + + [title] + Adw.WindowTitle { + title: _("Hidden Games"); + } + + [end] + MenuButton { + icon-name: "open-menu-symbolic"; + menu-model: primary_menu; + } + + [end] + ToggleButton hidden_search_button { + icon-name: "system-search-symbolic"; + action-name: "win.toggle_search"; + } + } + + SearchBar hidden_search_bar { + Adw.Clamp { + maximum-size: 500; + tightening-threshold: 500; + + SearchEntry hidden_search_entry { + hexpand: true; + } + } + } + + Adw.Bin hidden_library_bin { + ScrolledWindow hidden_scrolledwindow { + hexpand: true; + vexpand: true; + + FlowBox hidden_library { + homogeneous: true; + halign: center; + valign: start; + column-spacing: 12; + row-spacing: 12; + margin-top: 16; + margin-bottom: 16; + margin-start: 16; + margin-end: 16; + selection-mode: none; + } + } + } + + styles [ + "background", + ] + } + } + } +} + +menu primary_menu { + section { + submenu { + label: _("Sort"); + + item { + label: _("A-Z"); + action: "win.sort_by"; + target: "a-z"; + } + + item { + label: _("Z-A"); + action: "win.sort_by"; + target: "z-a"; + } + + item { + label: _("Newest"); + action: "win.sort_by"; + target: "newest"; + } + + item { + label: _("Oldest"); + action: "win.sort_by"; + target: "oldest"; + } + + item { + label: _("Last Played"); + action: "win.sort_by"; + target: "last_played"; + } + } + + item { + label: _("Show Hidden"); + action: "win.show_hidden"; + hidden-when: "action-disabled"; + } + } + + section { + item { + label: _("Preferences"); + action: "app.preferences"; + } + + item { + label: _("Keyboard Shortcuts"); + action: "win.show-help-overlay"; + } + + item { + label: _("About Game Shelf"); + action: "app.about"; + } + } +} + +menu add_games { + section { + item { + label: _("Add Game"); + action: "app.add_game"; + } + + item { + label: _("Import From Steam"); + action: "app.steam_import"; + } + } +} + +menu game_options { + section { + item { + label: _("Edit"); + action: "app.edit_details"; + } + + item { + label: _("Hide"); + action: "app.hide_game"; + } + + item { + label: _("Remove"); + action: "app.remove_game"; + } + } +} + +menu hidden_game_options { + section { + item { + label: _("Edit"); + action: "app.edit_details"; + } + + item { + label: _("Unhide"); + action: "app.hide_game"; + } + + item { + label: _("Remove"); + action: "app.remove_game"; + } + } +} diff --git a/src/window.py b/src/window.py new file mode 100644 index 000000000..87853da81 --- /dev/null +++ b/src/window.py @@ -0,0 +1,385 @@ +# window.py +# +# Copyright 2022 kramo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from gi.repository import Gio, GLib, Adw, GdkPixbuf, Gtk + +import os, json, time, datetime + +from .game import game +from .get_cover import get_cover +from .get_games import get_games +from .save_games import save_games + +@Gtk.Template(resource_path="/hu/kramo/GameShelf/window.ui") +class GameShelfWindow(Adw.ApplicationWindow): + __gtype_name__ = "GameShelfWindow" + + toast_overlay = 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() + game_options = 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_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_game_options = 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.visible_widgets = {} + self.hidden_widgets = {} + self.filtered = {} + self.hidden_filtered = {} + self.previous_page = self.library_view + self.toasts = {} + + 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.GameShelf") + self.placeholder_pixbuf = GdkPixbuf.Pixbuf.new_from_resource_at_scale("/hu/kramo/GameShelf/assets/library_placeholder.svg", 200, 300, False) + games = get_games() + for game in games: + if "removed" in games[game].keys(): + os.remove(os.path.join(os.environ.get("XDG_DATA_HOME"), "games", game + ".json")) + try: + os.remove(os.path.join(os.environ.get("XDG_DATA_HOME"), "covers", game + ".dat")) + 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) + + def update_games(self, games): + # Update the displayed games and the self.games instance variable to reference later + self.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) + if game_id in self.games: + current_game = self.games[game_id] + + if "removed" in current_game.keys(): + continue + + entry = game(current_game["name"], get_cover(current_game, self), game_id) + + if self.games[game_id]["hidden"] == False: + self.visible_widgets[game_id] = entry + self.library.append(entry) + else: + self.hidden_widgets[game_id] = entry + entry.menu_button.set_menu_model(entry.hidden_game_options) + self.hidden_library.append(entry) + + entry.cover_button.connect("clicked", self.show_overview, game_id) + entry.menu_button.connect("state-flags-changed", self.set_active_game, game_id) + + if self.visible_widgets == {}: + self.library_bin.set_child(self.notice_empty) + else: + self.library_bin.set_child(self.scrolledwindow) + + if 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 hidden == False: + 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(): + 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(): + 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, flags, game): + if "GTK_STATE_FLAG_FOCUS_WITHIN" in flags.value_names: + self.active_game_id = game + + def get_time(self, timestamp): + date = datetime.datetime.fromtimestamp(timestamp) + + if (datetime.datetime.today() - date).days == 0: + return _("Today") + elif (datetime.datetime.today() - date).days == 1: + return _("Yesterday") + elif (datetime.datetime.today() - date).days < 8: + return GLib.DateTime.new_from_unix_utc(timestamp).format("%A") + else: + return GLib.DateTime.new_from_unix_utc(timestamp).format("%x") + + def show_overview(self, widget, game_id): + game = self.games[game_id] + + if game["hidden"] == False: + self.overview_menu_button.set_menu_model(self.game_options) + else: + self.overview_menu_button.set_menu_model(self.hidden_game_options) + + 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 = (self.visible_widgets | self.hidden_widgets)[self.active_game_id].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(game["name"]) + self.overview_header_bar_title.set_title(game["name"]) + self.overview_added.set_label(_("Added: ") + self.get_time(game["added"])) + self.overview_last_played.set_label(_("Last played: ") + self.get_time(game["last_played"]) if game["last_played"] != 0 else _("Last played: Never")) + + 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 + elif name1 < name2: + return -1 + else: + if child1.get_first_child().game_id > child2.get_first_child().game_id: + return 1 + else: + 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 + elif name1 < name2: + return 1 + else: + return 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 + elif time1 < time2: + return 1 + else: + return 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 + elif time1 < time2: + return -1 + else: + return 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 + elif time1 < time2: + return 1 + else: + return self.a_z_sort(child1, child2) + + def on_go_back_action(self, widget, _): + 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, _): + 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, _): + 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, _): + 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 + + elif state == "last_played": + sort_func = self.last_played_sort + + self.schema.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, _): + 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 search_mode == False: + self.set_focus(search_entry) + else: + search_entry.set_text("") + + def on_escape_action(self, widget, _): + if self.stack.get_visible_child() == self.overview: + self.on_go_back_action(None, None) + return + elif 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 + open_file = open(os.path.join(os.path.join(os.environ.get("XDG_DATA_HOME"), "games", game_id + ".json")), "r") + data = json.loads(open_file.read()) + open_file.close() + data.pop("removed") + save_games({game_id : data}) + self.update_games({game_id : self.games[game_id]}) + self.toasts[game_id].dismiss() + self.toasts.pop(game_id) diff --git a/subprojects/blueprint-compiler.wrap b/subprojects/blueprint-compiler.wrap new file mode 100644 index 000000000..32237c733 --- /dev/null +++ b/subprojects/blueprint-compiler.wrap @@ -0,0 +1,8 @@ +[wrap-git] +directory = blueprint-compiler +url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +revision = v0.4.0 +depth = 1 + +[provide] +program_names = blueprint-compiler \ No newline at end of file
Game Shelf is a simple game launcher. It has support for importing your games from Steam with organizational features such as hiding and sorting by date added or last played.