5 # by Kaka <kaka@dolda2000.com>
7 import os, sys, optparse
11 import pygtk; pygtk.require("2.0")
12 except AssertionError, e:
13 error("You need to install the package 'python-gtk2'")
15 DIR_BASE = os.path.join(home, ".automanga")
16 DIR_PROFILES = os.path.join(DIR_BASE, "profiles")
17 FILE_SETTINGS = os.path.join(DIR_BASE, "settings")
20 global settings, profile, opts, args, cwd
22 if not os.path.exists(DIR_PROFILES):
23 os.makedirs(DIR_PROFILES)
26 usage = "Usage: %prog [options]"
27 parser = optparse.OptionParser(usage) # one delete option to use in combination with profile and directory?
28 parser.add_option("-p", "--profile", help="load or create a profile", metavar="profile")
29 parser.add_option("-r", "--remove-profile", help="remove profile", metavar="profile")
30 parser.add_option("-a", "--add", help="add a directory", metavar="dir")
31 parser.add_option("-d", "--delete", help="delete a directory", metavar="dir")
32 parser.add_option("-s", "--silent", help="no output", default=False, action="store_true")
33 parser.add_option("-v", "--verbose", help="show output", default=False, action="store_true")
34 opts, args = parser.parse_args()
38 def load_profile(name):
40 profile = Profile(name)
41 settings.last_profile(profile.name)
44 if not settings.silent():
48 print >>sys.stderr, msg
52 """Returns the absolute path"""
53 if not os.path.isabs(path): ret = os.path.join(cwd, path)
55 return os.path.abspath(ret)
58 """Checks if path is a manga directory"""
59 for node in os.listdir(path):
60 if node.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp"):
64 def natsorted(strings):
65 """Sorts a list of strings naturally"""
67 # return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', s)])
68 return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in s.rsplit(".")[0].split("-")])
73 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
74 self.window.connect("delete_event", lambda widget, event, data = None: False)
75 self.window.connect("destroy", lambda widget, data = None: self.quit())
76 self.window.connect("key_press_event", self.keypress)
77 # self.window.set_border_width(10)
78 self.window.set_position(gtk.WIN_POS_CENTER) # save position in settings?
79 # self.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#222")) # seems to do nothing
82 self.fullscreen = False
85 self.container = gtk.VBox()
86 self.menu_bar = self.build_menu()
87 self.page_bar = gtk.HBox()
88 self.key = gtk.Label()
89 # self.key.set_padding(10, 10)
90 self.page_num = gtk.Label()
91 self.page = gtk.Image()
92 self.sep = gtk.HSeparator()
93 self.sbar = gtk.Statusbar(); self.sbar.show(); self.sbar.set_has_resize_grip(False)
95 self.combo = gtk.combo_box_new_text()
96 self.combo.set_wrap_width(2)
98 self.combo.append_text('item - %d' % i)
99 self.combo.set_active(0)
101 self.page_bar.pack_start(self.key, expand=False)
102 vsep = gtk.VSeparator(); vsep.show(); self.page_bar.pack_start(vsep, expand=False)
103 # self.page_bar.pack_start(self.combo, expand=False); self.combo.show()
104 self.page_bar.pack_start(self.page_num, expand=False)
105 # self.container.pack_start(self.menu_bar, expand=False)
106 self.container.pack_start(self.page_bar, expand=False)
107 self.container.pack_start(self.sep, expand=False)
108 self.container.pack_start(self.page)
109 self.container.pack_start(self.sbar, expand=False)
110 self.window.add(self.container)
117 self.container.show()
120 def build_menu(self):
122 ("_New profile", gtk.STOCK_NEW, lambda widget, data: None),
123 ("_Load profile", gtk.STOCK_OPEN, lambda widget, data: None),
124 ("_Delete profile", gtk.STOCK_DELETE, lambda widget, data: None),
126 ("_Quit", gtk.STOCK_QUIT, lambda widget, data: self.quit()),
129 ("_Profile", gtk.STOCK_EDIT, lambda widget, data: None),
131 ("_Settings", gtk.STOCK_PREFERENCES, lambda widget, data: None),
134 menu_bar = gtk.MenuBar()
136 for submenu in menus:
140 mi = gtk.MenuItem(lbl, True)
146 mi = gtk.SeparatorMenuItem()
150 lbl, icon, func = item
152 img.set_from_stock(icon, gtk.ICON_SIZE_MENU)
153 mi = gtk.ImageMenuItem(lbl, True)
156 mi.connect("activate", func, None)
166 def set_title(self, title = None):
167 self.window.set_title("Automanga" + (" - " + title if title else ""))
169 def set_manga(self, manga):
171 self.set_title(manga.title())
172 self.cursor = manga.mark
175 def update_page(self):
176 self.page.set_from_file(self.cursor.path)
177 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()))
178 self.window.resize(*self.container.size_request())
180 self.sbar.pop(self.sbar.get_context_id("stat"))
181 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()))
183 def keypress(self, widget, event, data = None):
184 if event.keyval in [32]: self.read_page(1) # space
185 elif event.keyval in [65288]: self.read_page(-1) # backspace
186 elif event.keyval in [65362, 65363]: self.browse_page(1) # up, right
187 elif event.keyval in [65361, 65364]: self.browse_page(-1) # left, down
188 elif event.keyval in [70, 102]: self.toggle_fullscreen() # f, F
189 elif event.keyval in [81, 113, 65307]: self.quit() # q, Q, esc
190 elif event.keyval in [65360]: self.browse_start() # home
191 elif event.keyval in [65367]: self.browse_end() # end
192 else: self.key.set_text(str(event.keyval))
194 def browse_page(self, step):
195 self.cursor = self.cursor.previous() if step < 0 else self.cursor.next()
198 def read_page(self, step):
199 if self.cursor == self.manga.mark:
200 if step < 0: self.manga.set_mark(self.cursor.previous())
201 else: self.cursor = self.cursor.next()
202 if step < 0: self.cursor = self.manga.mark
203 else: self.manga.set_mark(self.cursor)
206 def browse_start(self):
207 self.cursor = self.manga.first_page()
210 def browse_end(self):
211 self.cursor = self.manga.last_page()
214 def toggle_fullscreen(self):
215 self.fullscreen = not self.fullscreen
216 # if self.fullscreen: self.window.fullscreen()
217 # else: self.window.unfullscreen()
219 self.window.set_decorated(False)
220 self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS)
223 self.window.set_decorated(True)
224 self.window.set_position(gtk.WIN_POS_NONE)
228 """A class for accessing the parsed content of a file as
229 attributes on an object."""
231 def __init__(self, path, create = False): ## add autosync - save everytime an attribute is set - does not work well with list attributes
233 self.attributes = [] ## make this a dict which stores the attributes, instead of adding them as attributes to the File object
234 if os.path.exists(path):
241 def __setattr__(self, name, val):
242 if name not in ("path", "attributes") and name not in self.attributes:
243 self.attributes.append(name)
244 object.__setattr__(self, name, val)
246 def __getattr__(self, name):
247 try: return object.__getattribute__(self, name)
250 def __delattr__(self, name):
251 self.attributes.remove(name)
252 object.__delattr__(self, name)
254 def parse(self, file):
255 def add_attr(type, name, val):
256 if type and name and val:
257 if type == "str": val = val[0]
258 elif type == "int": val = int(val[0])
259 elif type == "bool": val = (val[0].lower() == "true")
260 setattr(self, name, val)
261 type, attr, val = None, "", []
262 for line in file.xreadlines():
264 if line.startswith("["):
265 add_attr(type, attr, val)
266 (type, attr), val = line[1:-1].split(":"), []
269 add_attr(type, attr, val)
272 return os.path.exists(self.path)
279 dir = self.path.rsplit("/", 1)[0]
280 if not os.path.exists(dir):
282 file = open(self.path, "w")
283 for attr in self.attributes:
284 val = getattr(self, attr)
285 file.write("[%s:%s]\n" % (str(type(val))[7:-2], attr))
286 if type(val) in (list, tuple):
290 file.write(str(val) + "\n")
297 class Directory(object):
298 """A class for accessing files, or directories, in a directory as
299 attributes on an object."""
301 def __init__(self, path):
303 self._nodes = [f for f in os.listdir(path) if "." not in f] if os.path.exists(path) else []
304 self._opened_nodes = {}
307 return self._opened_nodes.itervalues()
309 def __contains__(self, name):
310 return name in self._nodes
312 def __getitem__(self, name):
313 if name in self._nodes:
314 return getattr(self, name)
317 def __getattr__(self, name):
318 if name in self._nodes:
319 if name not in self._opened_nodes:
320 if os.path.isdir(self.path(name)): self._opened_nodes[name] = Directory(self.path(name))
321 else: self._opened_nodes[name] = File(self.path(name))
322 return self._opened_nodes[name]
323 return object.__getattribute__(self, name)
325 def __delattr__(self, name):
326 if name in self._nodes:
327 self._nodes.remove(name)
328 if name in self._opened_nodes:
329 self._opened_nodes.pop(name).delete()
331 object.__delattr__(self, name)
334 for n in self._nodes:
335 getattr(self, n).delete()
336 self._opened_nodes = {}
338 os.rmdir(self.path())
341 if not self.exists():
342 os.makedirs(self.path())
346 for name, node in self._opened_nodes.items():
350 return os.path.exists(self.path())
352 def move(self, new_path):
353 if not self.exists() or os.path.exists(new_path):
355 os.rename(self.path(), new_path)
356 self._path = new_path
359 def path(self, *name):
360 return os.path.join(self._path, *name)
362 def add_dir(self, name):
363 if not os.path.exists(self.path(name)):
364 self._nodes.append(name)
365 self._opened_nodes[name] = Directory(self.path(name))
366 return getattr(self, name)
368 def add_file(self, name):
369 if not os.path.exists(self.path(name)):
370 self._nodes.append(name)
371 self._opened_nodes[name] = File(self.path(name))
372 return getattr(self, name)
374 class Settings(object):
376 self.file = File(FILE_SETTINGS, True)
377 if self.file.dirs is None:
383 def add_dir(self, path):
384 if not os.path.exists(path):
385 output("Failed to add directory - '%s' does not exist!" % path)
387 if path not in self.file.dirs:
388 self.file.dirs.append(path)
390 output("Added '%s'!" % path)
392 def delete_dir(self, dir):
393 if path not in self.file.dirs:
394 output("Failed to remove directory - '%s' not in list!" % path)
396 self.file.dirs.remove(dir)
398 output("Removed '%s'!" % path)
400 def silent(self, state = None):
401 if state is None: return self.file.silent
402 else: self.file.silent = state
404 def last_profile(self, val = None):
405 if val is None: return self.file.last_profile
406 else: self.file.last_profile = val
408 def load_last_profile(self):
409 if not self.file.last_profile:
411 load_profile(self.file.last_profile)
412 if not profile.exists():
413 del self.file.last_profile
418 def list_manga(self):
420 for dir in self.file.dirs:
421 if os.path.exists(dir):
422 ret += os.listdir(dir)
425 class Profile(object):
426 def __init__(self, name):
428 self.dir = Directory(os.path.join(DIR_PROFILES, name))
429 self.mangas = self.dir.add_dir("manga")
430 self.settings = self.dir.add_file("settings")
433 return self.dir.exists()
438 def rename(self, new_name):
439 if not self.dir.move(os.path.join(DIR_PROFILES, new_name)):
447 def save_page(self, manga):
448 if manga.title() not in self.mangas:
449 self.mangas.add_file(manga.title())
450 file = self.mangas[manga.title()]
451 file.page = manga.mark.name
454 def load_manga(self, path):
455 self.settings.last_read = path
456 manga = path.rsplit("/", 1)[-1]
457 if manga in self.mangas:
458 return Manga(self, path, self.mangas[manga].page)
459 return Manga(self, path)
462 def __init__(self, reader_profile, path, page = None):
463 self.reader = reader_profile
465 self.pages = self.load_pages()
466 self.mark = Page(self, page) if page else self.first_page()
468 def load_pages(self):
469 files = [f for f in os.listdir(self.path) if f.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp")]
470 return natsorted(files) # check if there's an order file / only load image files
472 def first_page(self):
473 return Page(self, self.pages[0])
476 return Page(self, self.pages[-1])
479 return len(self.pages)
481 def index_of(self, page):
482 return self.pages.index(page.name)
484 def set_mark(self, page):
486 self.reader.save_page(self)
488 def previous(self, page, step = 1):
489 return Page(self, self.pages[max(0, self.pages.index(page) - step)])
491 def next(self, page, step = 1):
492 return Page(self, self.pages[min(len(self.pages) - 1, self.pages.index(page) + step)])
495 return self.path.rsplit("/", 1)[-1]
498 def __init__(self, manga, name):
501 self.path = os.path.join(manga.path, name)
503 def previous(self, step = 1):
504 return self.manga.previous(self.name, step)
506 def next(self, step = 1):
507 return self.manga.next(self.name, step)
509 if __name__ == "__main__":
513 path = abs_path(opts.add)
514 settings.add_dir(path)
517 path = abs_path(opts.delete)
518 settings.delete_dir(path)
521 settings.silent(True)
523 settings.silent(False)
526 load_profile(opts.profile)
528 output("Loaded profile '%s'" % profile.name)
531 output("Created profile '%s'" % profile.name)
533 if settings.load_last_profile():
534 output("No profile specified - loading '%s'" % profile.name)
536 user = home.split("/")[-1]
537 output("No profile exists - creating one for '%s'" % user)
542 manga_path = abs_path(args[0]) if args else cwd
543 if manga_dir(manga_path):
544 reader.set_manga(profile.load_manga(manga_path))