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")
294 class Directory(object):
295 """A class for accessing files, or directories, in a directory as
296 attributes on an object."""
298 def __init__(self, path):
300 self._nodes = [f for f in os.listdir(path) if "." not in f] if os.path.exists(path) else []
301 self._opened_nodes = {}
303 def __getattr__(self, name):
304 if name in self._nodes:
305 if name not in self._opened_nodes:
306 if os.path.isdir(self.path(name)): self._opened_nodes[name] = Directory(self.path(name))
307 else: self._opened_nodes[name] = File(self.path(name))
308 return self._opened_nodes[name]
309 return object.__getattribute__(self, name)
311 def __delattr__(self, name):
312 if name in self._nodes:
313 self._nodes.remove(name)
314 if name in self._opened_nodes:
315 self._opened_nodes.pop(name).delete()
317 object.__delattr__(self, name)
320 for n in self._nodes:
321 getattr(self, n).delete()
322 self._opened_nodes = {}
324 os.rmdir(self.path())
327 os.makedirs(self.path())
329 def path(self, *name):
330 return os.path.join(self._path, *name)
332 def add_dir(self, name):
333 if not os.path.exists(self.path(name)):
334 self._nodes.append(name)
335 self._opened_nodes[name] = Directory(self.path(name))
336 return self._opened_nodes[name]
338 def add_file(self, name):
339 if not os.path.exists(self.path(name)):
340 self._nodes.append(name)
341 self._opened_nodes[name] = File(self.path(name))
342 return self._opened_nodes[name]
344 class Settings(object):
346 self.file = File(FILE_SETTINGS, True)
347 if self.file.dirs is None:
353 def add_dir(self, path):
354 if not os.path.exists(path):
355 output("Failed to add directory - '%s' does not exist!" % path)
357 if path not in self.file.dirs:
358 self.file.dirs.append(path)
360 output("Added '%s'!" % path)
362 def delete_dir(self, dir):
363 if path not in self.file.dirs:
364 output("Failed to remove directory - '%s' not in list!" % path)
366 self.file.dirs.remove(dir)
368 output("Removed '%s'!" % path)
370 def silent(self, state = None):
371 if state is None: return self.file.silent
372 else: self.file.silent = state
374 def last_profile(self, val = None):
375 if val is None: return self.file.last_profile
376 else: self.file.last_profile = val
378 def load_last_profile(self):
379 if not self.file.last_profile:
381 load_profile(self.file.last_profile)
382 if not profile.exists():
383 del self.file.last_profile
388 def list_manga(self):
390 for dir in self.file.dirs:
391 if os.path.exists(dir):
392 ret += os.listdir(dir)
395 class Profile(object):
396 def __init__(self, name):
398 self.file = File(os.path.join(DIR_PROFILES, name))
399 if not self.file.mangas:
400 self.file.mangas = []
403 return self.file.exists()
406 os.remove(self.file.path)
408 def rename(self, new_name):
410 new_path = os.path.join(DIR_PROFILES, new_name)
411 if os.path.exists(new_path):
413 os.rename(self.file.path, new_path)
420 def save_page(self, manga, page):
421 for n, m in enumerate(self.file.mangas):
422 manga_path = m.split("\t")[0]
423 if manga_path == manga.path:
424 self.file.mangas[n] = "%s\t%s" % (manga_path, page.name)
427 self.file.mangas.append("%s\t%s" % (manga.path, page.name))
430 def load_manga(self, path):
431 self.file.last_read = path
432 for manga in self.file.mangas:
433 manga_path, page_name = manga.split("\t")
434 if manga_path == path:
435 return Manga(self, path, page_name)
436 return Manga(self, path)
439 def __init__(self, reader_profile, path, page = None):
440 self.reader = reader_profile
442 self.pages = self.load_pages()
443 self.mark = Page(self, page) if page else self.first_page()
445 def load_pages(self):
446 files = [f for f in os.listdir(self.path) if f.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp")]
447 return natsorted(files) # check if there's an order file / only load image files
449 def first_page(self):
450 return Page(self, self.pages[0])
453 return Page(self, self.pages[-1])
456 return len(self.pages)
458 def index_of(self, page):
459 return self.pages.index(page.name)
461 def set_mark(self, page):
463 self.reader.save_page(self, page)
465 def previous(self, page, step = 1):
466 return Page(self, self.pages[max(0, self.pages.index(page) - step)])
468 def next(self, page, step = 1):
469 return Page(self, self.pages[min(len(self.pages) - 1, self.pages.index(page) + step)])
472 return self.path.rsplit("/", 1)[-1]
475 def __init__(self, manga, name):
478 self.path = os.path.join(manga.path, name)
480 def previous(self, step = 1):
481 return self.manga.previous(self.name, step)
483 def next(self, step = 1):
484 return self.manga.next(self.name, step)
486 if __name__ == "__main__":
490 path = abs_path(opts.add)
491 settings.add_dir(path)
494 path = abs_path(opts.delete)
495 settings.delete_dir(path)
498 settings.silent(True)
500 settings.silent(False)
503 load_profile(opts.profile)
505 output("Loaded profile '%s'" % profile.name)
508 output("Created profile '%s'" % profile.name)
510 if settings.load_last_profile():
511 output("No profile specified - loading '%s'" % profile.name)
513 user = home.split("/")[-1]
514 output("No profile exists - creating one for '%s'" % user)
519 manga_path = abs_path(args[0]) if args else cwd
520 if manga_dir(manga_path):
521 reader.set_manga(profile.load_manga(manga_path))