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) | |
42 | if profile.exists(): | |
43 | output("Loaded profile '%s'" % profile.name) | |
44 | ||
45 | def output(msg): | |
46 | if not settings.silent(): | |
47 | print msg | |
48 | ||
49 | def error(msg): | |
50 | print >>sys.stderr, msg | |
51 | sys.exit(1) | |
52 | ||
53 | def abs_path(path): | |
54 | """Returns the absolute path""" | |
55 | if not os.path.isabs(path): ret = os.path.join(cwd, path) | |
56 | else: ret = path | |
57 | return os.path.abspath(ret) | |
58 | ||
59 | def manga_dir(path): | |
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"): | |
63 | return True | |
64 | return False | |
65 | ||
66 | def natsorted(strings): | |
67 | """Sorts a list of strings naturally""" | |
68 | # import re | |
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("-")]) | |
71 | ||
72 | class Reader(object): | |
73 | """GUI""" | |
74 | def __init__(self): | |
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 | |
82 | ||
83 | self.set_title() | |
84 | self.fullscreen = False | |
85 | self.cursor = None | |
86 | ||
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) | |
96 | ||
97 | self.combo = gtk.combo_box_new_text() | |
98 | self.combo.set_wrap_width(2) | |
99 | for i in range(50): | |
100 | self.combo.append_text('item - %d' % i) | |
101 | self.combo.set_active(0) | |
102 | ||
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) | |
113 | ||
114 | self.key.show() | |
115 | self.page_num.show() | |
116 | self.sep.show() | |
117 | self.page.show() | |
118 | self.page_bar.show() | |
119 | self.container.show() | |
120 | self.window.show() | |
121 | ||
122 | def build_menu(self): | |
123 | menus = (("_File", ( | |
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), | |
127 | (), | |
128 | ("_Quit", gtk.STOCK_QUIT, lambda widget, data: self.quit()), | |
129 | )), | |
130 | ("_Edit", ( | |
131 | ("_Profile", gtk.STOCK_EDIT, lambda widget, data: None), | |
132 | (), | |
133 | ("_Settings", gtk.STOCK_PREFERENCES, lambda widget, data: None), | |
134 | )), | |
135 | ) | |
136 | menu_bar = gtk.MenuBar() | |
137 | menu_bar.show() | |
138 | for submenu in menus: | |
139 | lbl, items = submenu | |
140 | menu = gtk.Menu() | |
141 | menu.show() | |
142 | mi = gtk.MenuItem(lbl, True) | |
143 | mi.show() | |
144 | mi.set_submenu(menu) | |
145 | menu_bar.add(mi) | |
146 | for item in items: | |
147 | if not item: | |
148 | mi = gtk.SeparatorMenuItem() | |
149 | mi.show() | |
150 | menu.add(mi) | |
151 | else: | |
152 | lbl, icon, func = item | |
153 | img = gtk.Image() | |
154 | img.set_from_stock(icon, gtk.ICON_SIZE_MENU) | |
155 | mi = gtk.ImageMenuItem(lbl, True) | |
156 | mi.show() | |
157 | mi.set_image(img) | |
158 | mi.connect("activate", func, None) | |
159 | menu.add(mi) | |
160 | return menu_bar | |
161 | ||
162 | def start(self): | |
163 | gtk.main() | |
164 | ||
165 | def quit(self): | |
166 | gtk.main_quit() | |
167 | ||
168 | def set_title(self, title = None): | |
169 | self.window.set_title("Automanga" + (" - " + title if title else "")) | |
170 | ||
171 | def set_manga(self, manga): | |
172 | self.manga = manga | |
173 | self.set_title(manga.title()) | |
174 | self.cursor = manga.mark | |
175 | self.update_page() | |
176 | ||
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()) | |
181 | ||
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())) | |
184 | ||
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)) | |
195 | ||
196 | def browse_page(self, step): | |
197 | self.cursor = self.cursor.previous() if step < 0 else self.cursor.next() | |
198 | self.update_page() | |
199 | ||
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) | |
206 | self.update_page() | |
207 | ||
208 | def browse_start(self): | |
209 | self.cursor = self.manga.first_page() | |
210 | self.update_page() | |
211 | ||
212 | def browse_end(self): | |
213 | self.cursor = self.manga.last_page() | |
214 | self.update_page() | |
215 | ||
216 | def toggle_fullscreen(self): | |
217 | self.fullscreen = not self.fullscreen | |
218 | # if self.fullscreen: self.window.fullscreen() | |
219 | # else: self.window.unfullscreen() | |
220 | if self.fullscreen: | |
221 | self.window.set_decorated(False) | |
222 | self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS) | |
223 | self.menu_bar.hide() | |
224 | else: | |
225 | self.window.set_decorated(True) | |
226 | self.window.set_position(gtk.WIN_POS_NONE) | |
227 | self.menu_bar.show() | |
228 | ||
229 | class File(object): | |
230 | """Loads and parses a file and returns a File object from which | |
231 | you can access the parsed sections as attributes""" | |
232 | ||
233 | def __init__(self, path, create = False): ## add autosync - save everytime an attribute is set - does not work well with list attributes | |
234 | self.path = path | |
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): | |
237 | file = open(path) | |
238 | self.parse(file) | |
239 | file.close() | |
240 | elif create: | |
241 | self.save() | |
242 | ||
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) | |
247 | ||
248 | def __getattr__(self, name): | |
249 | try: return object.__getattribute__(self, name) | |
250 | except: return None | |
251 | ||
252 | def __delattr__(self, name): | |
253 | self.attributes.remove(name) | |
254 | object.__delattr__(self, name) | |
255 | ||
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(): | |
265 | line = line.strip() | |
266 | if line.startswith("["): | |
267 | add_attr(type, attr, val) | |
268 | (type, attr), val = line[1:-1].split(":"), [] | |
269 | elif line: | |
270 | val.append(line) | |
271 | add_attr(type, attr, val) | |
272 | ||
273 | def exists(self): | |
274 | return os.path.exists(self.path) | |
275 | ||
276 | def save(self): | |
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): | |
282 | for i in val: | |
283 | file.write(i + "\n") | |
284 | else: | |
285 | file.write(str(val) + "\n") | |
286 | file.write("\n") | |
287 | file.close() | |
288 | ||
289 | class Settings(object): | |
290 | def __init__(self): | |
291 | self.file = File(FILE_SETTINGS, True) | |
292 | if self.file.dirs is None: | |
293 | self.file.dirs = [] | |
294 | ||
295 | def save(self): | |
296 | self.file.save() | |
297 | ||
298 | def add_dir(self, path): | |
299 | if not os.path.exists(path): | |
300 | output("Failed to add directory - '%s' does not exist!" % path) | |
301 | return | |
302 | if path not in self.file.dirs: | |
303 | self.file.dirs.append(path) | |
304 | self.save() | |
305 | output("Added '%s'!" % path) | |
306 | ||
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) | |
310 | return | |
311 | self.file.dirs.remove(dir) | |
312 | self.save() | |
313 | output("Removed '%s'!" % path) | |
314 | ||
315 | def silent(self, state = None): | |
316 | if state is None: return self.file.silent | |
317 | else: self.file.silent = state | |
318 | ||
319 | def last_profile(self, val = None): | |
320 | if val is None: return self.file.last_profile | |
321 | else: self.file.last_profile = val | |
322 | ||
323 | def load_last_profile(self): | |
324 | if not self.file.last_profile: | |
325 | return False | |
326 | load_profile(self.file.last_profile) | |
327 | if not profile.exists(): | |
328 | del self.file.last_profile | |
329 | self.save() | |
330 | return False | |
331 | return True | |
332 | ||
333 | def list_manga(self): | |
334 | ret = [] | |
335 | for dir in self.file.dirs: | |
336 | if os.path.exists(dir): | |
337 | ret += os.listdir(dir) | |
338 | return ret | |
339 | ||
340 | class Profile(object): | |
341 | def __init__(self, name): | |
342 | self.name = name | |
343 | self.file = File(os.path.join(DIR_PROFILES, name)) | |
344 | if not self.file.mangas: | |
345 | self.file.mangas = [] | |
346 | ||
347 | def exists(self): | |
348 | return self.file.exists() | |
349 | ||
350 | def delete(self): | |
351 | os.remove(self.file.path) | |
352 | ||
353 | def rename(self, new_name): | |
354 | if self.exists(): | |
355 | new_path = os.path.join(DIR_PROFILES, new_name) | |
356 | if os.path.exists(new_path): | |
357 | return False | |
358 | os.rename(self.file.path, new_path) | |
359 | self.name = new_name | |
360 | return True | |
361 | ||
362 | def save(self): | |
363 | self.file.save() | |
364 | ||
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) | |
370 | break | |
371 | else: | |
372 | self.file.mangas.append("%s\t%s" % (manga.path, page.name)) | |
373 | self.save() | |
374 | ||
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) | |
382 | ||
383 | class Manga(object): | |
384 | def __init__(self, reader_profile, path, page = None): | |
385 | self.reader = reader_profile | |
386 | self.path = path | |
387 | self.pages = self.load_pages() | |
388 | self.mark = Page(self, page) if page else self.first_page() | |
389 | ||
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 | |
393 | ||
394 | def first_page(self): | |
395 | return Page(self, self.pages[0]) | |
396 | ||
397 | def last_page(self): | |
398 | return Page(self, self.pages[-1]) | |
399 | ||
400 | def num_pages(self): | |
401 | return len(self.pages) | |
402 | ||
403 | def index_of(self, page): | |
404 | return self.pages.index(page.name) | |
405 | ||
406 | def set_mark(self, page): | |
407 | self.mark = page | |
408 | self.reader.save_page(self, page) | |
409 | ||
410 | def previous(self, page, step = 1): | |
411 | return Page(self, self.pages[max(0, self.pages.index(page) - step)]) | |
412 | ||
413 | def next(self, page, step = 1): | |
414 | return Page(self, self.pages[min(len(self.pages) - 1, self.pages.index(page) + step)]) | |
415 | ||
416 | def title(self): | |
417 | return self.path.rsplit("/", 1)[-1] | |
418 | ||
419 | class Page(object): | |
420 | def __init__(self, manga, name): | |
421 | self.manga = manga | |
422 | self.name = name | |
423 | self.path = os.path.join(manga.path, name) | |
424 | ||
425 | def previous(self, step = 1): | |
426 | return self.manga.previous(self.name, step) | |
427 | ||
428 | def next(self, step = 1): | |
429 | return self.manga.next(self.name, step) | |
430 | ||
431 | if __name__ == "__main__": | |
432 | init() | |
433 | ||
434 | if opts.add: | |
435 | path = abs_path(opts.add) | |
436 | settings.add_dir(path) | |
437 | ||
438 | if opts.delete: | |
439 | path = abs_path(opts.delete) | |
440 | settings.delete_dir(path) | |
441 | ||
442 | if opts.silent: | |
443 | settings.silent(True) | |
444 | if opts.verbose: | |
445 | settings.silent(False) | |
446 | ||
447 | if opts.profile: | |
448 | load_profile(opts.profile) | |
449 | if not profile.exists(): | |
450 | profile.save() | |
451 | output("Created profile '%s'" % profile.name) | |
452 | else: | |
453 | if not settings.load_last_profile(): | |
454 | user = home.split("/")[-1] | |
455 | output("No profile exists - creating one for '%s'" % user) | |
456 | load_profile(user) | |
457 | profile.save() | |
458 | ||
459 | reader = Reader() | |
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)) | |
463 | reader.start() | |
464 | ||
465 | profile.save() | |
466 | settings.save() |