├── .gitignore ├── org.perezdecastro.Alexandria.gresources.xml ├── org.perezdecastro.Alexandria.desktop ├── gtk └── menus.ui ├── Makefile ├── README.md └── alexandria /.gitignore: -------------------------------------------------------------------------------- 1 | org.perezdecastro.Alexandria.gresource 2 | -------------------------------------------------------------------------------- /org.perezdecastro.Alexandria.gresources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gtk/menus.ui 5 | 6 | 7 | -------------------------------------------------------------------------------- /org.perezdecastro.Alexandria.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Hidden=false 3 | Name=Alexandria 4 | Exec=alexandria 5 | TryExec=alexandria 6 | Icon=accesories-dictionary 7 | Comment=Read book in ePub format 8 | Type=Application 9 | NoDisplay=false 10 | StartupNotify=true 11 | Categories=GNOME;GTK;Office 12 | Terminal=false 13 | X-GNOME-UsesNotifications=false 14 | -------------------------------------------------------------------------------- /gtk/menus.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | _Quit 7 | app.quit 8 | q]]> 9 | 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(strip $(APP_ID)),) 2 | APP_ID := org.perezdecastro.Alexandria 3 | endif 4 | 5 | all: $(APP_ID).gresource 6 | 7 | RESOURCE_FILES := $(wildcard gtk/*.ui) 8 | 9 | $(APP_ID).gresource: $(APP_ID).gresources.xml $(RESOURCE_FILES) 10 | glib-compile-resources --target=$@ $< 11 | 12 | run: $(APP_ID).gresource 13 | __ALEXANDRIA_DEVELOPMENT=1 ./alexandria $(FILE) 14 | 15 | .PHONY: run 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Alexandria 2 | ========== 3 | 4 | A dumb EPub book viewer. 5 | 6 | Needs [libgepub](https://github.com/danigm/libgepub) and 7 | [PyGI](https://wiki.gnome.org/Projects/PyGObject). 8 | 9 | 10 | Motivation 11 | ---------- 12 | 13 | I wanted a simple reader which would not ask me to install ~20 dependencies 14 | (like GNOME Documents), and that would not use an obscenely outdated WebKitGTK+ 15 | release (like PPub). I use this to preview books in my computer, but in the end 16 | I do my reading using an e-reader, and that's why Alexandria is quite dumb: it 17 | has only the minimum needed for what I do and not look out of place in GNOME. 18 | That's about it. 19 | -------------------------------------------------------------------------------- /alexandria: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Copyright © 2017 Adrian Perez 6 | # 7 | # Distributed under terms of the GPLv3 license. 8 | 9 | import gi 10 | gi.require_versions(dict(WebKit2='4.0', Gepub='0.4', Gtk='3.0', GLib='2.0')) 11 | from gi.repository import Gepub, Gtk, GObject, Gio 12 | 13 | def _find_resources_path(program_path): 14 | from os import path as P, environ 15 | devel = environ.get("__ALEXANDRIA_DEVELOPMENT") 16 | if devel and devel.strip(): 17 | # Use the directory where the executable is located, most likely 18 | # a checkout of the Git repository. 19 | path = P.dirname(program_path) 20 | else: 21 | # Use an installed location: binary is in /bin/alexandria, 22 | # and resources in /share/alexandria/* 23 | path = P.join(P.dirname(P.dirname(program_path)), "share", "alexandria") 24 | return P.abspath(P.join(path, "org.perezdecastro.Alexandria.gresource")) 25 | 26 | 27 | class memoized_function(object): 28 | __slots__ = ("value", "get_value") 29 | INVALID = object() 30 | 31 | def __init__(self, f): 32 | self.value = self.INVALID 33 | self.get_value = f 34 | 35 | def __call__(self, *arg, **kw): 36 | if self.value is self.INVALID: 37 | self.value = self.get_value(*arg, **kw) 38 | return self.value 39 | 40 | 41 | def memoized_property(fget, doc=None): 42 | return property(memoized_function(fget), doc=doc) 43 | 44 | 45 | class AppWindow(Gtk.ApplicationWindow): 46 | def __init__(self, application, document): 47 | self._document = document 48 | Gtk.ApplicationWindow.__init__(self, 49 | application=application, 50 | icon_name="accessories-dictionary", 51 | role="main-window") 52 | self.set_titlebar(self._headerbar) 53 | self._headerbar.set_subtitle(document.get_metadata("title")) 54 | view = Gepub.Widget() 55 | view.set_doc(document) 56 | self.add(view) 57 | 58 | @memoized_property 59 | def _headerbar(self): 60 | header = Gtk.HeaderBar() 61 | header.set_title("Alexandria") 62 | prev_button = Gtk.Button.new_from_icon_name("go-previous-symbolic", 63 | Gtk.IconSize.BUTTON) 64 | prev_button.connect("clicked", self.__on_prev) 65 | next_button = Gtk.Button.new_from_icon_name("go-next-symbolic", 66 | Gtk.IconSize.BUTTON) 67 | next_button.connect("clicked", self.__on_next) 68 | header.pack_start(prev_button) 69 | header.pack_start(next_button) 70 | header.set_show_close_button(True) 71 | header.show_all() 72 | return header 73 | 74 | def __on_prev(self, button): 75 | self._document.go_prev() 76 | 77 | def __on_next(self, button): 78 | self._document.go_next() 79 | 80 | 81 | class App(Gtk.Application): 82 | def __init__(self): 83 | Gtk.Application.__init__(self, 84 | application_id="org.perezdecastro.Alexandria", 85 | flags=Gio.ApplicationFlags.HANDLES_OPEN) 86 | self.connect("startup", self.__on_startup) 87 | self.connect("open", self.__on_open) 88 | 89 | def __action(self, name, callback): 90 | action = Gio.SimpleAction.new(name) 91 | action.connect("activate", callback) 92 | self.add_action(action) 93 | 94 | def __on_startup(self, app): 95 | gtk_settings = Gtk.Settings.get_default() 96 | gtk_settings.set_property("gtk-dialogs-use-header", True) 97 | self.__action("quit", lambda *arg: self.quit()) 98 | 99 | def __on_open(self, app, files, n_files, hint): 100 | for f in files: 101 | if f.query_exists(None): 102 | window = AppWindow(self, Gepub.Doc.new(f.get_path())) 103 | self.add_window(window) 104 | window.show_all() 105 | window.present() 106 | 107 | 108 | if __name__ == "__main__": 109 | import signal, sys 110 | signal.signal(signal.SIGINT, signal.SIG_DFL) 111 | Gio.Resource.load(_find_resources_path(__file__))._register() 112 | sys.exit(App().run(sys.argv)) 113 | --------------------------------------------------------------------------------