--- /dev/null
+#!/usr/bin/python
+#encoding: utf8
+#
+# automanga
+# by Kaka <kaka@dolda2000.com>
+
+import os, sys, optparse
+from user import home
+try:
+ import gtk
+ import pygtk; pygtk.require("2.0")
+except AssertionError, e:
+ error("You need to install the package 'python-gtk2'")
+
+DIR_BASE = os.path.join(home, ".automanga")
+DIR_PROFILES = os.path.join(DIR_BASE, "profiles")
+FILE_SETTINGS = os.path.join(DIR_BASE, "settings")
+
+def init():
+ global settings, profile, opts, args, cwd
+
+ if not os.path.exists(DIR_PROFILES):
+ os.makedirs(DIR_PROFILES)
+ settings = Settings()
+
+ usage = "Usage: %prog [options]"
+ parser = optparse.OptionParser(usage) # one delete option to use in combination with profile and directory?
+ parser.add_option("-p", "--profile", help="load or create a profile", metavar="profile")
+ parser.add_option("-r", "--remove-profile", help="remove profile", metavar="profile")
+ parser.add_option("-a", "--add", help="add a directory", metavar="dir")
+ parser.add_option("-d", "--delete", help="delete a directory", metavar="dir")
+ parser.add_option("-s", "--silent", help="no output", default=False, action="store_true")
+ parser.add_option("-v", "--verbose", help="show output", default=False, action="store_true")
+ opts, args = parser.parse_args()
+
+ cwd = os.getcwd()
+
+def load_profile(name):
+ global profile
+ profile = Profile(name)
+ settings.last_profile(profile.name)
+ if profile.exists():
+ output("Loaded profile '%s'" % profile.name)
+
+def output(msg):
+ if not settings.silent():
+ print msg
+
+def error(msg):
+ print >>sys.stderr, msg
+ sys.exit(1)
+
+def abs_path(path):
+ """Returns the absolute path"""
+ if not os.path.isabs(path): ret = os.path.join(cwd, path)
+ else: ret = path
+ return os.path.abspath(ret)
+
+def manga_dir(path):
+ """Checks if path is a manga directory"""
+ for node in os.listdir(path):
+ if node.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp"):
+ return True
+ return False
+
+def natsorted(strings):
+ """Sorts a list of strings naturally"""
+# import re
+# return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', s)])
+ return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in s.rsplit(".")[0].split("-")])
+
+class Reader(object):
+ """GUI"""
+ def __init__(self):
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.window.connect("delete_event", lambda widget, event, data = None: False)
+ self.window.connect("destroy", lambda widget, data = None: self.quit())
+ self.window.connect("key_press_event", self.keypress)
+# self.window.set_border_width(10)
+ self.window.set_position(gtk.WIN_POS_CENTER) # save position in settings?
+# self.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#222")) # seems to do nothing
+
+ self.set_title()
+ self.fullscreen = False
+ self.cursor = None
+
+ self.container = gtk.VBox()
+ self.menu_bar = self.build_menu()
+ self.page_bar = gtk.HBox()
+ self.key = gtk.Label()
+# self.key.set_padding(10, 10)
+ self.page_num = gtk.Label()
+ self.page = gtk.Image()
+ self.sep = gtk.HSeparator()
+ self.sbar = gtk.Statusbar(); self.sbar.show(); self.sbar.set_has_resize_grip(False)
+
+ self.combo = gtk.combo_box_new_text()
+ self.combo.set_wrap_width(2)
+ for i in range(50):
+ self.combo.append_text('item - %d' % i)
+ self.combo.set_active(0)
+
+ self.page_bar.pack_start(self.key, expand=False)
+ vsep = gtk.VSeparator(); vsep.show(); self.page_bar.pack_start(vsep, expand=False)
+# self.page_bar.pack_start(self.combo, expand=False); self.combo.show()
+ self.page_bar.pack_start(self.page_num, expand=False)
+# self.container.pack_start(self.menu_bar, expand=False)
+ self.container.pack_start(self.page_bar, expand=False)
+ self.container.pack_start(self.sep, expand=False)
+ self.container.pack_start(self.page)
+ self.container.pack_start(self.sbar, expand=False)
+ self.window.add(self.container)
+
+ self.key.show()
+ self.page_num.show()
+ self.sep.show()
+ self.page.show()
+ self.page_bar.show()
+ self.container.show()
+ self.window.show()
+
+ def build_menu(self):
+ menus = (("_File", (
+ ("_New profile", gtk.STOCK_NEW, lambda widget, data: None),
+ ("_Load profile", gtk.STOCK_OPEN, lambda widget, data: None),
+ ("_Delete profile", gtk.STOCK_DELETE, lambda widget, data: None),
+ (),
+ ("_Quit", gtk.STOCK_QUIT, lambda widget, data: self.quit()),
+ )),
+ ("_Edit", (
+ ("_Profile", gtk.STOCK_EDIT, lambda widget, data: None),
+ (),
+ ("_Settings", gtk.STOCK_PREFERENCES, lambda widget, data: None),
+ )),
+ )
+ menu_bar = gtk.MenuBar()
+ menu_bar.show()
+ for submenu in menus:
+ lbl, items = submenu
+ menu = gtk.Menu()
+ menu.show()
+ mi = gtk.MenuItem(lbl, True)
+ mi.show()
+ mi.set_submenu(menu)
+ menu_bar.add(mi)
+ for item in items:
+ if not item:
+ mi = gtk.SeparatorMenuItem()
+ mi.show()
+ menu.add(mi)
+ else:
+ lbl, icon, func = item
+ img = gtk.Image()
+ img.set_from_stock(icon, gtk.ICON_SIZE_MENU)
+ mi = gtk.ImageMenuItem(lbl, True)
+ mi.show()
+ mi.set_image(img)
+ mi.connect("activate", func, None)
+ menu.add(mi)
+ return menu_bar
+
+ def start(self):
+ gtk.main()
+
+ def quit(self):
+ gtk.main_quit()
+
+ def set_title(self, title = None):
+ self.window.set_title("Automanga" + (" - " + title if title else ""))
+
+ def set_manga(self, manga):
+ self.manga = manga
+ self.set_title(manga.title())
+ self.cursor = manga.mark
+ self.update_page()
+
+ def update_page(self):
+ self.page.set_from_file(self.cursor.path)
+ self.page_num.set_label("Mark's at %s (%s/%s)\t\tYou're at %s (%s/%s)" % (self.manga.mark.name, self.manga.index_of(self.manga.mark) + 1, self.manga.num_pages(), self.cursor.name, self.manga.index_of(self.cursor) + 1, self.manga.num_pages()))
+ self.window.resize(*self.container.size_request())
+
+ self.sbar.pop(self.sbar.get_context_id("stat"))
+ self.sbar.push(self.sbar.get_context_id("stat"), "Mark's at %s (%s/%s)\t\tYou're at %s (%s/%s)" % (self.manga.mark.name, self.manga.index_of(self.manga.mark) + 1, self.manga.num_pages(), self.cursor.name, self.manga.index_of(self.cursor) + 1, self.manga.num_pages()))
+
+ def keypress(self, widget, event, data = None):
+ if event.keyval in [32]: self.read_page(1) # space
+ elif event.keyval in [65288]: self.read_page(-1) # backspace
+ elif event.keyval in [65362, 65363]: self.browse_page(1) # up, right
+ elif event.keyval in [65361, 65364]: self.browse_page(-1) # left, down
+ elif event.keyval in [70, 102]: self.toggle_fullscreen() # f, F
+ elif event.keyval in [81, 113, 65307]: self.quit() # q, Q, esc
+ elif event.keyval in [65360]: self.browse_start() # home
+ elif event.keyval in [65367]: self.browse_end() # end
+ else: self.key.set_text(str(event.keyval))
+
+ def browse_page(self, step):
+ self.cursor = self.cursor.previous() if step < 0 else self.cursor.next()
+ self.update_page()
+
+ def read_page(self, step):
+ if self.cursor == self.manga.mark:
+ if step < 0: self.manga.set_mark(self.cursor.previous())
+ else: self.cursor = self.cursor.next()
+ if step < 0: self.cursor = self.manga.mark
+ else: self.manga.set_mark(self.cursor)
+ self.update_page()
+
+ def browse_start(self):
+ self.cursor = self.manga.first_page()
+ self.update_page()
+
+ def browse_end(self):
+ self.cursor = self.manga.last_page()
+ self.update_page()
+
+ def toggle_fullscreen(self):
+ self.fullscreen = not self.fullscreen
+# if self.fullscreen: self.window.fullscreen()
+# else: self.window.unfullscreen()
+ if self.fullscreen:
+ self.window.set_decorated(False)
+ self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.menu_bar.hide()
+ else:
+ self.window.set_decorated(True)
+ self.window.set_position(gtk.WIN_POS_NONE)
+ self.menu_bar.show()
+
+class File(object):
+ """Loads and parses a file and returns a File object from which
+ you can access the parsed sections as attributes"""
+
+ def __init__(self, path, create = False): ## add autosync - save everytime an attribute is set - does not work well with list attributes
+ self.path = path
+ self.attributes = [] ## make this a dict which stores the attributes, instead of adding them as attributes to the File object
+ if os.path.exists(path):
+ file = open(path)
+ self.parse(file)
+ file.close()
+ elif create:
+ self.save()
+
+ def __setattr__(self, name, val):
+ if name not in ("path", "attributes") and name not in self.attributes:
+ self.attributes.append(name)
+ object.__setattr__(self, name, val)
+
+ def __getattr__(self, name):
+ try: return object.__getattribute__(self, name)
+ except: return None
+
+ def __delattr__(self, name):
+ self.attributes.remove(name)
+ object.__delattr__(self, name)
+
+ def parse(self, file):
+ def add_attr(type, name, val):
+ if type and name and val:
+ if type == "str": val = val[0]
+ elif type == "int": val = int(val[0])
+ elif type == "bool": val = (val[0].lower() == "true")
+ setattr(self, name, val)
+ type, attr, val = None, "", []
+ for line in file.xreadlines():
+ line = line.strip()
+ if line.startswith("["):
+ add_attr(type, attr, val)
+ (type, attr), val = line[1:-1].split(":"), []
+ elif line:
+ val.append(line)
+ add_attr(type, attr, val)
+
+ def exists(self):
+ return os.path.exists(self.path)
+
+ def save(self):
+ file = open(self.path, "w")
+ for attr in self.attributes:
+ val = getattr(self, attr)
+ file.write("[%s:%s]\n" % (str(type(val))[7:-2], attr))
+ if type(val) in (list, tuple):
+ for i in val:
+ file.write(i + "\n")
+ else:
+ file.write(str(val) + "\n")
+ file.write("\n")
+ file.close()
+
+class Settings(object):
+ def __init__(self):
+ self.file = File(FILE_SETTINGS, True)
+ if self.file.dirs is None:
+ self.file.dirs = []
+
+ def save(self):
+ self.file.save()
+
+ def add_dir(self, path):
+ if not os.path.exists(path):
+ output("Failed to add directory - '%s' does not exist!" % path)
+ return
+ if path not in self.file.dirs:
+ self.file.dirs.append(path)
+ self.save()
+ output("Added '%s'!" % path)
+
+ def delete_dir(self, dir):
+ if path not in self.file.dirs:
+ output("Failed to remove directory - '%s' not in list!" % path)
+ return
+ self.file.dirs.remove(dir)
+ self.save()
+ output("Removed '%s'!" % path)
+
+ def silent(self, state = None):
+ if state is None: return self.file.silent
+ else: self.file.silent = state
+
+ def last_profile(self, val = None):
+ if val is None: return self.file.last_profile
+ else: self.file.last_profile = val
+
+ def load_last_profile(self):
+ if not self.file.last_profile:
+ return False
+ load_profile(self.file.last_profile)
+ if not profile.exists():
+ del self.file.last_profile
+ self.save()
+ return False
+ return True
+
+ def list_manga(self):
+ ret = []
+ for dir in self.file.dirs:
+ if os.path.exists(dir):
+ ret += os.listdir(dir)
+ return ret
+
+class Profile(object):
+ def __init__(self, name):
+ self.name = name
+ self.file = File(os.path.join(DIR_PROFILES, name))
+ if not self.file.mangas:
+ self.file.mangas = []
+
+ def exists(self):
+ return self.file.exists()
+
+ def delete(self):
+ os.remove(self.file.path)
+
+ def rename(self, new_name):
+ if self.exists():
+ new_path = os.path.join(DIR_PROFILES, new_name)
+ if os.path.exists(new_path):
+ return False
+ os.rename(self.file.path, new_path)
+ self.name = new_name
+ return True
+
+ def save(self):
+ self.file.save()
+
+ def save_page(self, manga, page):
+ for n, m in enumerate(self.file.mangas):
+ manga_path = m.split("\t")[0]
+ if manga_path == manga.path:
+ self.file.mangas[n] = "%s\t%s" % (manga_path, page.name)
+ break
+ else:
+ self.file.mangas.append("%s\t%s" % (manga.path, page.name))
+ self.save()
+
+ def load_manga(self, path):
+ self.file.last_read = path
+ for manga in self.file.mangas:
+ manga_path, page_name = manga.split("\t")
+ if manga_path == path:
+ return Manga(self, path, page_name)
+ return Manga(self, path)
+
+class Manga(object):
+ def __init__(self, reader_profile, path, page = None):
+ self.reader = reader_profile
+ self.path = path
+ self.pages = self.load_pages()
+ self.mark = Page(self, page) if page else self.first_page()
+
+ def load_pages(self):
+ files = [f for f in os.listdir(self.path) if f.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp")]
+ return natsorted(files) # check if there's an order file / only load image files
+
+ def first_page(self):
+ return Page(self, self.pages[0])
+
+ def last_page(self):
+ return Page(self, self.pages[-1])
+
+ def num_pages(self):
+ return len(self.pages)
+
+ def index_of(self, page):
+ return self.pages.index(page.name)
+
+ def set_mark(self, page):
+ self.mark = page
+ self.reader.save_page(self, page)
+
+ def previous(self, page, step = 1):
+ return Page(self, self.pages[max(0, self.pages.index(page) - step)])
+
+ def next(self, page, step = 1):
+ return Page(self, self.pages[min(len(self.pages) - 1, self.pages.index(page) + step)])
+
+ def title(self):
+ return self.path.rsplit("/", 1)[-1]
+
+class Page(object):
+ def __init__(self, manga, name):
+ self.manga = manga
+ self.name = name
+ self.path = os.path.join(manga.path, name)
+
+ def previous(self, step = 1):
+ return self.manga.previous(self.name, step)
+
+ def next(self, step = 1):
+ return self.manga.next(self.name, step)
+
+if __name__ == "__main__":
+ init()
+
+ if opts.add:
+ path = abs_path(opts.add)
+ settings.add_dir(path)
+
+ if opts.delete:
+ path = abs_path(opts.delete)
+ settings.delete_dir(path)
+
+ if opts.silent:
+ settings.silent(True)
+ if opts.verbose:
+ settings.silent(False)
+
+ if opts.profile:
+ load_profile(opts.profile)
+ if not profile.exists():
+ profile.save()
+ output("Created profile '%s'" % profile.name)
+ else:
+ if not settings.load_last_profile():
+ user = home.split("/")[-1]
+ output("No profile exists - creating one for '%s'" % user)
+ load_profile(user)
+ profile.save()
+
+ reader = Reader()
+ manga_path = abs_path(args[0]) if args else cwd
+ if manga_dir(manga_path):
+ reader.set_manga(profile.load_manga(manga_path))
+ reader.start()
+
+ profile.save()
+ settings.save()