Commit | Line | Data |
---|---|---|
861c3221 TW |
1 | #!/usr/bin/python |
2 | #encoding: utf8 | |
3 | # | |
4 | # automanga | |
5 | # by Kaka <kaka@dolda2000.com> | |
6 | ||
7 | import os, sys, optparse | |
8 | from user import home | |
9 | try: | |
10 | import gtk | |
11 | import pygtk; pygtk.require("2.0") | |
12 | except AssertionError, e: | |
13 | error("You need to install the package 'python-gtk2'") | |
14 | ||
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") | |
18 | ||
19 | def init(): | |
20 | global settings, profile, opts, args, cwd | |
21 | ||
22 | if not os.path.exists(DIR_PROFILES): | |
23 | os.makedirs(DIR_PROFILES) | |
24 | settings = Settings() | |
25 | ||
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() | |
35 | ||
36 | cwd = os.getcwd() | |
37 | ||
38 | def load_profile(name): | |
39 | global profile | |
40 | profile = Profile(name) | |
41 | settings.last_profile(profile.name) | |
861c3221 TW |
42 | |
43 | def output(msg): | |
44 | if not settings.silent(): | |
45 | print msg | |
46 | ||
47 | def error(msg): | |
48 | print >>sys.stderr, msg | |
49 | sys.exit(1) | |
50 | ||
51 | def abs_path(path): | |
52 | """Returns the absolute path""" | |
53 | if not os.path.isabs(path): ret = os.path.join(cwd, path) | |
54 | else: ret = path | |
55 | return os.path.abspath(ret) | |
56 | ||
57 | def manga_dir(path): | |
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"): | |
61 | return True | |
62 | return False | |
63 | ||
64 | def natsorted(strings): | |
65 | """Sorts a list of strings naturally""" | |
66 | # import re | |
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("-")]) | |
69 | ||
70 | class Reader(object): | |
71 | """GUI""" | |
72 | def __init__(self): | |
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 | |
80 | ||
81 | self.set_title() | |
82 | self.fullscreen = False | |
83 | self.cursor = None | |
84 | ||
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) | |
94 | ||
95 | self.combo = gtk.combo_box_new_text() | |
96 | self.combo.set_wrap_width(2) | |
97 | for i in range(50): | |
98 | self.combo.append_text('item - %d' % i) | |
99 | self.combo.set_active(0) | |
100 | ||
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) | |
111 | ||
112 | self.key.show() | |
113 | self.page_num.show() | |
114 | self.sep.show() | |
115 | self.page.show() | |
116 | self.page_bar.show() | |
117 | self.container.show() | |
118 | self.window.show() | |
119 | ||
120 | def build_menu(self): | |
121 | menus = (("_File", ( | |
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), | |
125 | (), | |
126 | ("_Quit", gtk.STOCK_QUIT, lambda widget, data: self.quit()), | |
127 | )), | |
128 | ("_Edit", ( | |
129 | ("_Profile", gtk.STOCK_EDIT, lambda widget, data: None), | |
130 | (), | |
131 | ("_Settings", gtk.STOCK_PREFERENCES, lambda widget, data: None), | |
132 | )), | |
133 | ) | |
134 | menu_bar = gtk.MenuBar() | |
135 | menu_bar.show() | |
136 | for submenu in menus: | |
137 | lbl, items = submenu | |
138 | menu = gtk.Menu() | |
139 | menu.show() | |
140 | mi = gtk.MenuItem(lbl, True) | |
141 | mi.show() | |
142 | mi.set_submenu(menu) | |
143 | menu_bar.add(mi) | |
144 | for item in items: | |
145 | if not item: | |
146 | mi = gtk.SeparatorMenuItem() | |
147 | mi.show() | |
148 | menu.add(mi) | |
149 | else: | |
150 | lbl, icon, func = item | |
151 | img = gtk.Image() | |
152 | img.set_from_stock(icon, gtk.ICON_SIZE_MENU) | |
153 | mi = gtk.ImageMenuItem(lbl, True) | |
154 | mi.show() | |
155 | mi.set_image(img) | |
156 | mi.connect("activate", func, None) | |
157 | menu.add(mi) | |
158 | return menu_bar | |
159 | ||
160 | def start(self): | |
161 | gtk.main() | |
162 | ||
163 | def quit(self): | |
164 | gtk.main_quit() | |
165 | ||
166 | def set_title(self, title = None): | |
167 | self.window.set_title("Automanga" + (" - " + title if title else "")) | |
168 | ||
169 | def set_manga(self, manga): | |
170 | self.manga = manga | |
171 | self.set_title(manga.title()) | |
172 | self.cursor = manga.mark | |
173 | self.update_page() | |
174 | ||
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()) | |
179 | ||
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())) | |
182 | ||
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)) | |
193 | ||
194 | def browse_page(self, step): | |
195 | self.cursor = self.cursor.previous() if step < 0 else self.cursor.next() | |
196 | self.update_page() | |
197 | ||
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) | |
204 | self.update_page() | |
205 | ||
206 | def browse_start(self): | |
207 | self.cursor = self.manga.first_page() | |
208 | self.update_page() | |
209 | ||
210 | def browse_end(self): | |
211 | self.cursor = self.manga.last_page() | |
212 | self.update_page() | |
213 | ||
214 | def toggle_fullscreen(self): | |
215 | self.fullscreen = not self.fullscreen | |
216 | # if self.fullscreen: self.window.fullscreen() | |
217 | # else: self.window.unfullscreen() | |
218 | if self.fullscreen: | |
219 | self.window.set_decorated(False) | |
220 | self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS) | |
221 | self.menu_bar.hide() | |
222 | else: | |
223 | self.window.set_decorated(True) | |
224 | self.window.set_position(gtk.WIN_POS_NONE) | |
225 | self.menu_bar.show() | |
226 | ||
227 | class File(object): | |
d9b99a70 TW |
228 | """A class for accessing the parsed content of a file as |
229 | attributes on an object.""" | |
861c3221 TW |
230 | |
231 | def __init__(self, path, create = False): ## add autosync - save everytime an attribute is set - does not work well with list attributes | |
232 | self.path = path | |
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): | |
235 | file = open(path) | |
236 | self.parse(file) | |
237 | file.close() | |
238 | elif create: | |
239 | self.save() | |
240 | ||
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) | |
245 | ||
246 | def __getattr__(self, name): | |
247 | try: return object.__getattribute__(self, name) | |
248 | except: return None | |
249 | ||
250 | def __delattr__(self, name): | |
251 | self.attributes.remove(name) | |
252 | object.__delattr__(self, name) | |
253 | ||
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(): | |
263 | line = line.strip() | |
264 | if line.startswith("["): | |
265 | add_attr(type, attr, val) | |
266 | (type, attr), val = line[1:-1].split(":"), [] | |
267 | elif line: | |
268 | val.append(line) | |
269 | add_attr(type, attr, val) | |
270 | ||
271 | def exists(self): | |
272 | return os.path.exists(self.path) | |
273 | ||
d9b99a70 TW |
274 | def delete(self): |
275 | self.attributes = [] | |
276 | os.remove(self.path) | |
277 | ||
861c3221 | 278 | def save(self): |
d9b99a70 TW |
279 | dir = self.path.rsplit("/", 1)[0] |
280 | if not os.path.exists(dir): | |
281 | os.makedirs(dir) | |
861c3221 TW |
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): | |
287 | for i in val: | |
288 | file.write(i + "\n") | |
289 | else: | |
290 | file.write(str(val) + "\n") | |
291 | file.write("\n") | |
292 | file.close() | |
293 | ||
1773398f TW |
294 | def save_all(self): |
295 | self.save() | |
296 | ||
d9b99a70 TW |
297 | class Directory(object): |
298 | """A class for accessing files, or directories, in a directory as | |
299 | attributes on an object.""" | |
300 | ||
301 | def __init__(self, path): | |
302 | self._path = 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 = {} | |
305 | ||
1773398f TW |
306 | def __iter__(self): |
307 | return self._opened_nodes.itervalues() | |
308 | ||
7939bf81 TW |
309 | def __contains__(self, name): |
310 | return name in self._nodes | |
311 | ||
1773398f TW |
312 | def __getitem__(self, name): |
313 | if name in self._nodes: | |
314 | return getattr(self, name) | |
315 | raise KeyError(name) | |
316 | ||
d9b99a70 TW |
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) | |
324 | ||
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() | |
330 | else: | |
331 | object.__delattr__(self, name) | |
332 | ||
333 | def delete(self): | |
334 | for n in self._nodes: | |
335 | getattr(self, n).delete() | |
336 | self._opened_nodes = {} | |
337 | self._nodes = [] | |
338 | os.rmdir(self.path()) | |
339 | ||
340 | def save(self): | |
1773398f TW |
341 | if not self.exists(): |
342 | os.makedirs(self.path()) | |
343 | ||
344 | def save_all(self): | |
345 | self.save() | |
346 | for name, node in self._opened_nodes.items(): | |
347 | node.save_all() | |
348 | ||
349 | def exists(self): | |
350 | return os.path.exists(self.path()) | |
351 | ||
352 | def move(self, new_path): | |
353 | if not self.exists() or os.path.exists(new_path): | |
354 | return False | |
355 | os.rename(self.path(), new_path) | |
356 | self._path = new_path | |
357 | return True | |
d9b99a70 TW |
358 | |
359 | def path(self, *name): | |
360 | return os.path.join(self._path, *name) | |
361 | ||
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)) | |
1773398f | 366 | return getattr(self, name) |
d9b99a70 TW |
367 | |
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)) | |
1773398f | 372 | return getattr(self, name) |
d9b99a70 | 373 | |
861c3221 TW |
374 | class Settings(object): |
375 | def __init__(self): | |
376 | self.file = File(FILE_SETTINGS, True) | |
377 | if self.file.dirs is None: | |
378 | self.file.dirs = [] | |
379 | ||
380 | def save(self): | |
381 | self.file.save() | |
382 | ||
383 | def add_dir(self, path): | |
384 | if not os.path.exists(path): | |
385 | output("Failed to add directory - '%s' does not exist!" % path) | |
386 | return | |
387 | if path not in self.file.dirs: | |
388 | self.file.dirs.append(path) | |
389 | self.save() | |
390 | output("Added '%s'!" % path) | |
391 | ||
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) | |
395 | return | |
396 | self.file.dirs.remove(dir) | |
397 | self.save() | |
398 | output("Removed '%s'!" % path) | |
399 | ||
400 | def silent(self, state = None): | |
401 | if state is None: return self.file.silent | |
402 | else: self.file.silent = state | |
403 | ||
404 | def last_profile(self, val = None): | |
405 | if val is None: return self.file.last_profile | |
406 | else: self.file.last_profile = val | |
407 | ||
408 | def load_last_profile(self): | |
409 | if not self.file.last_profile: | |
410 | return False | |
411 | load_profile(self.file.last_profile) | |
412 | if not profile.exists(): | |
413 | del self.file.last_profile | |
414 | self.save() | |
415 | return False | |
416 | return True | |
417 | ||
418 | def list_manga(self): | |
419 | ret = [] | |
420 | for dir in self.file.dirs: | |
421 | if os.path.exists(dir): | |
422 | ret += os.listdir(dir) | |
423 | return ret | |
424 | ||
425 | class Profile(object): | |
426 | def __init__(self, name): | |
427 | self.name = name | |
1773398f TW |
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") | |
861c3221 TW |
431 | |
432 | def exists(self): | |
1773398f | 433 | return self.dir.exists() |
861c3221 TW |
434 | |
435 | def delete(self): | |
1773398f | 436 | self.dir.delete() |
861c3221 TW |
437 | |
438 | def rename(self, new_name): | |
1773398f TW |
439 | if not self.dir.move(os.path.join(DIR_PROFILES, new_name)): |
440 | return False | |
861c3221 TW |
441 | self.name = new_name |
442 | return True | |
443 | ||
444 | def save(self): | |
1773398f | 445 | self.dir.save_all() |
861c3221 | 446 | |
1773398f TW |
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()] | |
7939bf81 | 451 | file.page = manga.mark.name |
1773398f | 452 | file.save() |
861c3221 TW |
453 | |
454 | def load_manga(self, path): | |
1773398f TW |
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) | |
861c3221 TW |
459 | return Manga(self, path) |
460 | ||
461 | class Manga(object): | |
462 | def __init__(self, reader_profile, path, page = None): | |
463 | self.reader = reader_profile | |
464 | self.path = path | |
465 | self.pages = self.load_pages() | |
466 | self.mark = Page(self, page) if page else self.first_page() | |
467 | ||
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 | |
471 | ||
472 | def first_page(self): | |
473 | return Page(self, self.pages[0]) | |
474 | ||
475 | def last_page(self): | |
476 | return Page(self, self.pages[-1]) | |
477 | ||
478 | def num_pages(self): | |
479 | return len(self.pages) | |
480 | ||
481 | def index_of(self, page): | |
482 | return self.pages.index(page.name) | |
483 | ||
484 | def set_mark(self, page): | |
485 | self.mark = page | |
1773398f | 486 | self.reader.save_page(self) |
861c3221 TW |
487 | |
488 | def previous(self, page, step = 1): | |
489 | return Page(self, self.pages[max(0, self.pages.index(page) - step)]) | |
490 | ||
491 | def next(self, page, step = 1): | |
492 | return Page(self, self.pages[min(len(self.pages) - 1, self.pages.index(page) + step)]) | |
493 | ||
494 | def title(self): | |
495 | return self.path.rsplit("/", 1)[-1] | |
496 | ||
497 | class Page(object): | |
498 | def __init__(self, manga, name): | |
499 | self.manga = manga | |
500 | self.name = name | |
501 | self.path = os.path.join(manga.path, name) | |
502 | ||
503 | def previous(self, step = 1): | |
504 | return self.manga.previous(self.name, step) | |
505 | ||
506 | def next(self, step = 1): | |
507 | return self.manga.next(self.name, step) | |
508 | ||
509 | if __name__ == "__main__": | |
510 | init() | |
511 | ||
512 | if opts.add: | |
513 | path = abs_path(opts.add) | |
514 | settings.add_dir(path) | |
515 | ||
516 | if opts.delete: | |
517 | path = abs_path(opts.delete) | |
518 | settings.delete_dir(path) | |
519 | ||
520 | if opts.silent: | |
521 | settings.silent(True) | |
522 | if opts.verbose: | |
523 | settings.silent(False) | |
524 | ||
525 | if opts.profile: | |
526 | load_profile(opts.profile) | |
d9b99a70 TW |
527 | if profile.exists(): |
528 | output("Loaded profile '%s'" % profile.name) | |
529 | else: | |
861c3221 TW |
530 | profile.save() |
531 | output("Created profile '%s'" % profile.name) | |
532 | else: | |
d9b99a70 TW |
533 | if settings.load_last_profile(): |
534 | output("No profile specified - loading '%s'" % profile.name) | |
535 | else: | |
861c3221 TW |
536 | user = home.split("/")[-1] |
537 | output("No profile exists - creating one for '%s'" % user) | |
538 | load_profile(user) | |
539 | profile.save() | |
540 | ||
541 | reader = Reader() | |
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)) | |
545 | reader.start() | |
546 | ||
547 | profile.save() | |
548 | settings.save() |