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)
43 output("Loaded profile '%s'" % profile.name)
46 if not settings.silent():
50 print >>sys.stderr, msg
54 """Returns the absolute path"""
55 if not os.path.isabs(path): ret = os.path.join(cwd, path)
57 return os.path.abspath(ret)
60 """Checks if path is a manga directory"""
61 for node in os.listdir(path):
62 if node.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp"):
66 def natsorted(strings):
67 """Sorts a list of strings naturally"""
69 # return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', s)])
70 return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in s.rsplit(".")[0].split("-")])
75 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
76 self.window.connect("delete_event", lambda widget, event, data = None: False)
77 self.window.connect("destroy", lambda widget, data = None: self.quit())
78 self.window.connect("key_press_event", self.keypress)
79 # self.window.set_border_width(10)
80 self.window.set_position(gtk.WIN_POS_CENTER) # save position in settings?
81 # self.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#222")) # seems to do nothing
84 self.fullscreen = False
87 self.container = gtk.VBox()
88 self.menu_bar = self.build_menu()
89 self.page_bar = gtk.HBox()
90 self.key = gtk.Label()
91 # self.key.set_padding(10, 10)
92 self.page_num = gtk.Label()
93 self.page = gtk.Image()
94 self.sep = gtk.HSeparator()
95 self.sbar = gtk.Statusbar(); self.sbar.show(); self.sbar.set_has_resize_grip(False)
97 self.combo = gtk.combo_box_new_text()
98 self.combo.set_wrap_width(2)
100 self.combo.append_text('item - %d' % i)
101 self.combo.set_active(0)
103 self.page_bar.pack_start(self.key, expand=False)
104 vsep = gtk.VSeparator(); vsep.show(); self.page_bar.pack_start(vsep, expand=False)
105 # self.page_bar.pack_start(self.combo, expand=False); self.combo.show()
106 self.page_bar.pack_start(self.page_num, expand=False)
107 # self.container.pack_start(self.menu_bar, expand=False)
108 self.container.pack_start(self.page_bar, expand=False)
109 self.container.pack_start(self.sep, expand=False)
110 self.container.pack_start(self.page)
111 self.container.pack_start(self.sbar, expand=False)
112 self.window.add(self.container)
119 self.container.show()
122 def build_menu(self):
124 ("_New profile", gtk.STOCK_NEW, lambda widget, data: None),
125 ("_Load profile", gtk.STOCK_OPEN, lambda widget, data: None),
126 ("_Delete profile", gtk.STOCK_DELETE, lambda widget, data: None),
128 ("_Quit", gtk.STOCK_QUIT, lambda widget, data: self.quit()),
131 ("_Profile", gtk.STOCK_EDIT, lambda widget, data: None),
133 ("_Settings", gtk.STOCK_PREFERENCES, lambda widget, data: None),
136 menu_bar = gtk.MenuBar()
138 for submenu in menus:
142 mi = gtk.MenuItem(lbl, True)
148 mi = gtk.SeparatorMenuItem()
152 lbl, icon, func = item
154 img.set_from_stock(icon, gtk.ICON_SIZE_MENU)
155 mi = gtk.ImageMenuItem(lbl, True)
158 mi.connect("activate", func, None)
168 def set_title(self, title = None):
169 self.window.set_title("Automanga" + (" - " + title if title else ""))
171 def set_manga(self, manga):
173 self.set_title(manga.title())
174 self.cursor = manga.mark
177 def update_page(self):
178 self.page.set_from_file(self.cursor.path)
179 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()))
180 self.window.resize(*self.container.size_request())
182 self.sbar.pop(self.sbar.get_context_id("stat"))
183 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()))
185 def keypress(self, widget, event, data = None):
186 if event.keyval in [32]: self.read_page(1) # space
187 elif event.keyval in [65288]: self.read_page(-1) # backspace
188 elif event.keyval in [65362, 65363]: self.browse_page(1) # up, right
189 elif event.keyval in [65361, 65364]: self.browse_page(-1) # left, down
190 elif event.keyval in [70, 102]: self.toggle_fullscreen() # f, F
191 elif event.keyval in [81, 113, 65307]: self.quit() # q, Q, esc
192 elif event.keyval in [65360]: self.browse_start() # home
193 elif event.keyval in [65367]: self.browse_end() # end
194 else: self.key.set_text(str(event.keyval))
196 def browse_page(self, step):
197 self.cursor = self.cursor.previous() if step < 0 else self.cursor.next()
200 def read_page(self, step):
201 if self.cursor == self.manga.mark:
202 if step < 0: self.manga.set_mark(self.cursor.previous())
203 else: self.cursor = self.cursor.next()
204 if step < 0: self.cursor = self.manga.mark
205 else: self.manga.set_mark(self.cursor)
208 def browse_start(self):
209 self.cursor = self.manga.first_page()
212 def browse_end(self):
213 self.cursor = self.manga.last_page()
216 def toggle_fullscreen(self):
217 self.fullscreen = not self.fullscreen
218 # if self.fullscreen: self.window.fullscreen()
219 # else: self.window.unfullscreen()
221 self.window.set_decorated(False)
222 self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS)
225 self.window.set_decorated(True)
226 self.window.set_position(gtk.WIN_POS_NONE)
230 """Loads and parses a file and returns a File object from which
231 you can access the parsed sections as attributes"""
233 def __init__(self, path, create = False): ## add autosync - save everytime an attribute is set - does not work well with list attributes
235 self.attributes = [] ## make this a dict which stores the attributes, instead of adding them as attributes to the File object
236 if os.path.exists(path):
243 def __setattr__(self, name, val):
244 if name not in ("path", "attributes") and name not in self.attributes:
245 self.attributes.append(name)
246 object.__setattr__(self, name, val)
248 def __getattr__(self, name):
249 try: return object.__getattribute__(self, name)
252 def __delattr__(self, name):
253 self.attributes.remove(name)
254 object.__delattr__(self, name)
256 def parse(self, file):
257 def add_attr(type, name, val):
258 if type and name and val:
259 if type == "str": val = val[0]
260 elif type == "int": val = int(val[0])
261 elif type == "bool": val = (val[0].lower() == "true")
262 setattr(self, name, val)
263 type, attr, val = None, "", []
264 for line in file.xreadlines():
266 if line.startswith("["):
267 add_attr(type, attr, val)
268 (type, attr), val = line[1:-1].split(":"), []
271 add_attr(type, attr, val)
274 return os.path.exists(self.path)
277 file = open(self.path, "w")
278 for attr in self.attributes:
279 val = getattr(self, attr)
280 file.write("[%s:%s]\n" % (str(type(val))[7:-2], attr))
281 if type(val) in (list, tuple):
285 file.write(str(val) + "\n")
289 class Settings(object):
291 self.file = File(FILE_SETTINGS, True)
292 if self.file.dirs is None:
298 def add_dir(self, path):
299 if not os.path.exists(path):
300 output("Failed to add directory - '%s' does not exist!" % path)
302 if path not in self.file.dirs:
303 self.file.dirs.append(path)
305 output("Added '%s'!" % path)
307 def delete_dir(self, dir):
308 if path not in self.file.dirs:
309 output("Failed to remove directory - '%s' not in list!" % path)
311 self.file.dirs.remove(dir)
313 output("Removed '%s'!" % path)
315 def silent(self, state = None):
316 if state is None: return self.file.silent
317 else: self.file.silent = state
319 def last_profile(self, val = None):
320 if val is None: return self.file.last_profile
321 else: self.file.last_profile = val
323 def load_last_profile(self):
324 if not self.file.last_profile:
326 load_profile(self.file.last_profile)
327 if not profile.exists():
328 del self.file.last_profile
333 def list_manga(self):
335 for dir in self.file.dirs:
336 if os.path.exists(dir):
337 ret += os.listdir(dir)
340 class Profile(object):
341 def __init__(self, name):
343 self.file = File(os.path.join(DIR_PROFILES, name))
344 if not self.file.mangas:
345 self.file.mangas = []
348 return self.file.exists()
351 os.remove(self.file.path)
353 def rename(self, new_name):
355 new_path = os.path.join(DIR_PROFILES, new_name)
356 if os.path.exists(new_path):
358 os.rename(self.file.path, new_path)
365 def save_page(self, manga, page):
366 for n, m in enumerate(self.file.mangas):
367 manga_path = m.split("\t")[0]
368 if manga_path == manga.path:
369 self.file.mangas[n] = "%s\t%s" % (manga_path, page.name)
372 self.file.mangas.append("%s\t%s" % (manga.path, page.name))
375 def load_manga(self, path):
376 self.file.last_read = path
377 for manga in self.file.mangas:
378 manga_path, page_name = manga.split("\t")
379 if manga_path == path:
380 return Manga(self, path, page_name)
381 return Manga(self, path)
384 def __init__(self, reader_profile, path, page = None):
385 self.reader = reader_profile
387 self.pages = self.load_pages()
388 self.mark = Page(self, page) if page else self.first_page()
390 def load_pages(self):
391 files = [f for f in os.listdir(self.path) if f.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp")]
392 return natsorted(files) # check if there's an order file / only load image files
394 def first_page(self):
395 return Page(self, self.pages[0])
398 return Page(self, self.pages[-1])
401 return len(self.pages)
403 def index_of(self, page):
404 return self.pages.index(page.name)
406 def set_mark(self, page):
408 self.reader.save_page(self, page)
410 def previous(self, page, step = 1):
411 return Page(self, self.pages[max(0, self.pages.index(page) - step)])
413 def next(self, page, step = 1):
414 return Page(self, self.pages[min(len(self.pages) - 1, self.pages.index(page) + step)])
417 return self.path.rsplit("/", 1)[-1]
420 def __init__(self, manga, name):
423 self.path = os.path.join(manga.path, name)
425 def previous(self, step = 1):
426 return self.manga.previous(self.name, step)
428 def next(self, step = 1):
429 return self.manga.next(self.name, step)
431 if __name__ == "__main__":
435 path = abs_path(opts.add)
436 settings.add_dir(path)
439 path = abs_path(opts.delete)
440 settings.delete_dir(path)
443 settings.silent(True)
445 settings.silent(False)
448 load_profile(opts.profile)
449 if not profile.exists():
451 output("Created profile '%s'" % profile.name)
453 if not settings.load_last_profile():
454 user = home.split("/")[-1]
455 output("No profile exists - creating one for '%s'" % user)
460 manga_path = abs_path(args[0]) if args else cwd
461 if manga_dir(manga_path):
462 reader.set_manga(profile.load_manga(manga_path))