class AddDialog: page = None search_lock = False server = None manga_slug = None manga_data = None manga = None def __init__(self, window): self.window = window self.builder = Gtk.Builder() self.builder.add_from_resource('/info/febvre/Komikku/ui/add_dialog.ui') self.dialog = self.builder.get_object('dialog') self.dialog.get_children()[0].set_border_width(0) # Header bar self.builder.get_object('back_button').connect('clicked', self.on_back_button_clicked) self.custom_title_stack = self.builder.get_object('custom_title_stack') # Make title centered self.builder.get_object('custom_title_servers_page_label').set_margin_end(38) self.overlay = self.builder.get_object('overlay') self.stack = self.builder.get_object('stack') self.activity_indicator = ActivityIndicator() self.overlay.add_overlay(self.activity_indicator) self.overlay.set_overlay_pass_through(self.activity_indicator, True) self.activity_indicator.show_all() # Servers page listbox = self.builder.get_object('servers_page_listbox') listbox.get_style_context().add_class('list-bordered') listbox.connect('row-activated', self.on_server_clicked) servers_settings = Settings.get_default().servers_settings servers_languages = Settings.get_default().servers_languages for server_data in get_servers_list(): if servers_languages and server_data['lang'] not in servers_languages: continue server_settings = servers_settings.get(get_server_main_id_by_id(server_data['id'])) if server_settings is not None and (not server_settings['enabled'] or server_settings['langs'].get(server_data['lang']) is False): continue row = Gtk.ListBoxRow() row.get_style_context().add_class('add-dialog-server-listboxrow') row.server_data = server_data box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) row.add(box) # Server logo pixbuf = Pixbuf.new_from_resource_at_scale(get_server_logo_resource_path_by_id(server_data['id']), 24, 24, True) logo = Gtk.Image() logo.set_from_pixbuf(pixbuf) box.pack_start(logo, False, True, 0) # Server title label = Gtk.Label(xalign=0) label.set_text(server_data['name']) box.pack_start(label, True, True, 0) # Server language label = Gtk.Label() label.set_text(LANGUAGES[server_data['lang']]) label.get_style_context().add_class('add-dialog-server-language-label') box.pack_start(label, False, True, 0) listbox.add(row) listbox.show_all() # Search page self.custom_title_search_page_searchentry = self.builder.get_object('custom_title_search_page_searchentry') self.custom_title_search_page_searchentry.connect('activate', self.search) self.search_page_listbox = self.builder.get_object('search_page_listbox') self.search_page_listbox.get_style_context().add_class('list-bordered') self.search_page_listbox.connect('row-activated', self.on_manga_clicked) # Manga page grid = self.builder.get_object('manga_page_grid') grid.set_margin_top(6) grid.set_margin_end(6) grid.set_margin_bottom(6) grid.set_margin_start(6) self.custom_title_manga_page_label = self.builder.get_object('custom_title_manga_page_label') self.add_button = self.builder.get_object('add_button') self.add_button.connect('clicked', self.on_add_button_clicked) self.read_button = self.builder.get_object('read_button') self.read_button.connect('clicked', self.on_read_button_clicked) self.show_page('servers') def clear_search(self): self.custom_title_search_page_searchentry.set_text('') self.clear_results() def clear_results(self): for child in self.search_page_listbox.get_children(): self.search_page_listbox.remove(child) def hide_notification(self): self.builder.get_object('notification_revealer').set_reveal_child(False) def on_add_button_clicked(self, button): def run(): manga = Manga.new(self.manga_data, self.server) GLib.idle_add(complete, manga) def complete(manga): self.manga = manga self.show_notification(_('{0} manga added').format(self.manga.name)) self.window.library.on_manga_added(self.manga) self.add_button.set_sensitive(True) self.add_button.hide() self.read_button.show() self.activity_indicator.stop() return False self.activity_indicator.start() self.add_button.set_sensitive(False) thread = threading.Thread(target=run) thread.daemon = True thread.start() def on_back_button_clicked(self, button): if self.page == 'servers': self.dialog.close() elif self.page == 'search': self.activity_indicator.stop() self.search_lock = False self.server = None self.show_page('servers') elif self.page == 'manga': self.activity_indicator.stop() self.manga_slug = None self.show_page('search') def on_manga_clicked(self, listbox, row): if row.manga_data is None: return self.show_manga(row.manga_data) def on_read_button_clicked(self, button): self.window.card.init(self.manga, transition=False) self.dialog.close() def on_server_clicked(self, listbox, row): self.server = getattr(row.server_data['module'], row.server_data['class_name'])() self.show_page('search') def open(self, action, param): self.dialog.set_modal(True) self.dialog.set_transient_for(self.window) self.dialog.present() def search(self, entry=None): if self.search_lock: return term = self.custom_title_search_page_searchentry.get_text().strip() # Find manga by Id if term.startswith('id:'): slug = term[3:] if not slug: return self.show_manga(dict(slug=slug)) return if not term and getattr(self.server, 'get_most_populars', None) is None: # An empty term is allowed only if server has 'get_most_populars' method return def run(server): most_populars = not term try: if most_populars: # We offer most popular mangas as starting search results result = server.get_most_populars() else: result = server.search(term) if result: GLib.idle_add(complete, result, server, most_populars) else: GLib.idle_add(error, result, server) except Exception as e: user_error_message = log_error_traceback(e) GLib.idle_add(error, None, server, user_error_message) def complete(result, server, most_populars): if server != self.server: return False self.activity_indicator.stop() if most_populars: row = Gtk.ListBoxRow() row.get_style_context().add_class('add-dialog-search-section-listboxrow') row.manga_data = None box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) row.add(box) label = Gtk.Label(xalign=0, margin=6) label.set_text(_('MOST POPULARS')) box.pack_start(label, True, True, 0) self.search_page_listbox.add(row) for item in result: row = Gtk.ListBoxRow() row.manga_data = item box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) row.add(box) label = Gtk.Label(xalign=0, margin=6) label.set_ellipsize(Pango.EllipsizeMode.END) label.set_text(item['name']) box.pack_start(label, True, True, 0) self.search_page_listbox.add(row) self.search_page_listbox.show_all() self.search_lock = False return False def error(result, server, message=None): if server != self.server: return self.activity_indicator.stop() self.search_lock = False if message: self.show_notification(message) elif result is None: self.show_notification(_('Oops, search failed. Please try again.'), 2) elif len(result) == 0: self.show_notification(_('No results')) self.search_lock = True self.clear_results() self.activity_indicator.start() thread = threading.Thread(target=run, args=(self.server, )) thread.daemon = True thread.start() def show_manga(self, manga_data): def run(server, manga_slug): try: current_manga_data = server.get_manga_data(manga_data) if current_manga_data is not None: GLib.idle_add(complete, current_manga_data, server) else: GLib.idle_add(error, server, manga_slug) except Exception as e: user_error_message = log_error_traceback(e) GLib.idle_add(error, server, manga_slug, user_error_message) def complete(manga_data, server): if server != self.server or manga_data['slug'] != self.manga_slug: return False self.manga_data = manga_data # Populate manga card try: cover_data = self.server.get_manga_cover_image(self.manga_data.get('cover')) except Exception as e: cover_data = None user_error_message = log_error_traceback(e) if user_error_message: self.show_notification(user_error_message) if cover_data is None: pixbuf = Pixbuf.new_from_resource_at_scale('/info/febvre/Komikku/images/missing_file.png', 174, -1, True) else: cover_stream = Gio.MemoryInputStream.new_from_data(cover_data, None) if get_buffer_mime_type(cover_data) != 'image/gif': pixbuf = Pixbuf.new_from_stream_at_scale(cover_stream, 174, -1, True, None) else: pixbuf = scale_pixbuf_animation(PixbufAnimation.new_from_stream(cover_stream), 174, -1, True, True) if isinstance(pixbuf, PixbufAnimation): self.builder.get_object('cover_image').set_from_animation(pixbuf) else: self.builder.get_object('cover_image').set_from_pixbuf(pixbuf) authors = html_escape(', '.join(self.manga_data['authors'])) if self.manga_data['authors'] else '-' self.builder.get_object('authors_value_label').set_markup('<span size="small">{0}</span>'.format(authors)) genres = html_escape(', '.join(self.manga_data['genres'])) if self.manga_data['genres'] else '-' self.builder.get_object('genres_value_label').set_markup('<span size="small">{0}</span>'.format(genres)) status = _(Manga.STATUSES[self.manga_data['status']]) if self.manga_data['status'] else '-' self.builder.get_object('status_value_label').set_markup('<span size="small">{0}</span>'.format(status)) scanlators = html_escape(', '.join(self.manga_data['scanlators'])) if self.manga_data['scanlators'] else '-' self.builder.get_object('scanlators_value_label').set_markup('<span size="small">{0}</span>'.format(scanlators)) self.builder.get_object('server_value_label').set_markup( '<span size="small"><a href="{0}">{1} [{2}]</a>\n{3} chapters</span>'.format( self.server.get_manga_url(self.manga_data['slug'], self.manga_data.get('url')), html_escape(self.server.name), self.server.lang.upper(), len(self.manga_data['chapters']) ) ) self.builder.get_object('synopsis_value_label').set_text(self.manga_data['synopsis'] or '-') self.activity_indicator.stop() self.show_page('manga') return False def error(server, manga_slug, message=None): if server != self.server or manga_slug != self.manga_slug: return False self.activity_indicator.stop() self.show_notification(message or _("Oops, failed to retrieve manga's information."), 2) return False self.manga_slug = manga_data['slug'] self.activity_indicator.start() thread = threading.Thread(target=run, args=(self.server, self.manga_slug, )) thread.daemon = True thread.start() def show_notification(self, message, interval=5): if not message: return self.builder.get_object('notification_label').set_text(message) self.builder.get_object('notification_revealer').set_reveal_child(True) revealer_timer = threading.Timer(interval, GLib.idle_add, args=[self.hide_notification]) revealer_timer.start() def show_page(self, name): if name == 'search': if self.page == 'servers': self.custom_title_search_page_searchentry.set_placeholder_text(_('Search in {0}…').format(self.server.name)) self.clear_search() self.search() else: self.custom_title_search_page_searchentry.grab_focus_without_selecting() elif name == 'manga': self.custom_title_manga_page_label.set_text(self.manga_data['name']) # Check if selected manga is already in library db_conn = create_db_connection() row = db_conn.execute( 'SELECT * FROM mangas WHERE slug = ? AND server_id = ?', (self.manga_data['slug'], self.manga_data['server_id']) ).fetchone() db_conn.close() if row: self.manga = Manga.get(row['id'], self.server) self.read_button.show() self.add_button.hide() else: self.add_button.show() self.read_button.hide() self.custom_title_stack.set_visible_child_name(name) self.stack.set_visible_child_name(name) self.page = name
class MainWindow(Gtk.ApplicationWindow): mobile_width = False page = None is_maximized = False is_fullscreen = False _prev_size = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.application = kwargs['application'] self._night_light_handler_id = 0 self._night_light_proxy = None self.builder = Gtk.Builder() self.builder.add_from_resource( '/info/febvre/Komikku/ui/main_window.ui') self.builder.add_from_resource('/info/febvre/Komikku/ui/menu/main.xml') self.logging_manager = self.application.get_logger() self.downloader = Downloader(self) self.updater = Updater(self, Settings.get_default().update_at_startup) self.overlay = self.builder.get_object('overlay') self.stack = self.builder.get_object('stack') self.title_stack = self.builder.get_object('title_stack') self.left_button_image = self.builder.get_object('left_button_image') self.menu_button = self.builder.get_object('menu_button') self.menu_button_image = self.builder.get_object('menu_button_image') self.activity_indicator = ActivityIndicator() self.overlay.add_overlay(self.activity_indicator) self.overlay.set_overlay_pass_through(self.activity_indicator, True) self.activity_indicator.show_all() self.assemble_window() def add_accelerators(self): self.application.set_accels_for_action('app.settings', ['<Control>p']) self.application.set_accels_for_action('app.add', ['<Control>plus']) self.application.set_accels_for_action('app.fullscreen', ['F11']) def add_actions(self): add_action = Gio.SimpleAction.new('add', None) add_action.connect('activate', self.on_left_button_clicked) settings_action = Gio.SimpleAction.new('settings', None) settings_action.connect('activate', self.on_settings_menu_clicked) about_action = Gio.SimpleAction.new('about', None) about_action.connect('activate', self.on_about_menu_clicked) shortcuts_action = Gio.SimpleAction.new('shortcuts', None) shortcuts_action.connect('activate', self.on_shortcuts_menu_clicked) fullscreen_action = Gio.SimpleAction.new('fullscreen', None) fullscreen_action.connect('activate', self.toggle_fullscreen) self.application.add_action(add_action) self.application.add_action(settings_action) self.application.add_action(about_action) self.application.add_action(shortcuts_action) self.application.add_action(fullscreen_action) self.library.add_actions() self.card.add_actions() self.reader.add_actions() def assemble_window(self): # Default size window_size = Settings.get_default().window_size self.set_default_size(window_size[0], window_size[1]) # Min size geom = Gdk.Geometry() geom.min_width = 360 geom.min_height = 288 self.set_geometry_hints(None, geom, Gdk.WindowHints.MIN_SIZE) # Titlebar self.titlebar = self.builder.get_object('titlebar') self.headerbar = self.builder.get_object('headerbar') self.left_button = self.builder.get_object('left_button') self.left_button.connect('clicked', self.on_left_button_clicked, None) self.builder.get_object('fullscreen_button').connect( 'clicked', self.toggle_fullscreen, None) self.set_titlebar(self.titlebar) # Fisrt start grid self.first_start_grid = self.builder.get_object('first_start_grid') pix = Pixbuf.new_from_resource_at_scale( '/info/febvre/Komikku/images/logo.png', 256, 256, True) self.builder.get_object('app_logo').set_from_pixbuf(pix) # Init pages self.library = Library(self) self.card = Card(self) self.reader = Reader(self) # Window self.connect('check-resize', self.on_resize) self.connect('delete-event', self.on_application_quit) self.connect('key-press-event', self.on_key_press_event) self.connect('window-state-event', self.on_window_state_event) # Custom CSS screen = Gdk.Screen.get_default() css_provider = Gtk.CssProvider() css_provider_resource = Gio.File.new_for_uri( 'resource:///info/febvre/Komikku/css/style.css') css_provider.load_from_file(css_provider_resource) context = Gtk.StyleContext() context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) if Gio.Application.get_default().development_mode is True: self.get_style_context().add_class('devel') # Theme (light or dark) self.init_theme() self.library.show() def change_layout(self): pass def confirm(self, title, message, callback): def on_response(dialog, response_id): if response_id == Gtk.ResponseType.YES: callback() dialog.destroy() dialog = Handy.Dialog.new(self) dialog.get_style_context().add_class('solid-csd') dialog.connect('response', on_response) dialog.set_title(title) dialog.add_buttons('Yes', Gtk.ResponseType.YES, 'Cancel', Gtk.ResponseType.CANCEL) dialog.set_default_response(Gtk.ResponseType.YES) label = Gtk.Label() label.set_text(message) label.set_line_wrap(True) label.set_vexpand(True) label.set_property('margin', 16) label.set_valign(Gtk.Align.CENTER) label.set_halign(Gtk.Align.CENTER) label.set_justify(Gtk.Justification.CENTER) dialog.get_content_area().add(label) dialog.show_all() def hide_notification(self): self.builder.get_object('notification_revealer').set_reveal_child( False) def init_theme(self): if Settings.get_default().night_light and not self._night_light_proxy: # Watch night light changes self._night_light_proxy = Gio.DBusProxy.new_sync( Gio.bus_get_sync(Gio.BusType.SESSION, None), Gio.DBusProxyFlags.NONE, None, 'org.gnome.SettingsDaemon.Color', '/org/gnome/SettingsDaemon/Color', 'org.gnome.SettingsDaemon.Color', None) def property_changed(proxy, changed_properties, invalidated_properties): properties = changed_properties.unpack() if 'NightLightActive' in properties.keys(): Gtk.Settings.get_default().set_property( 'gtk-application-prefer-dark-theme', properties['NightLightActive']) self._night_light_handler_id = self._night_light_proxy.connect( 'g-properties-changed', property_changed) Gtk.Settings.get_default().set_property( 'gtk-application-prefer-dark-theme', self._night_light_proxy.get_cached_property( 'NightLightActive')) else: if self._night_light_proxy and self._night_light_handler_id > 0: self._night_light_proxy.disconnect( self._night_light_handler_id) self._night_light_proxy = None self._night_light_handler_id = 0 Gtk.Settings.get_default().set_property( 'gtk-application-prefer-dark-theme', Settings.get_default().dark_theme) def on_about_menu_clicked(self, action, param): from komikku.application import CREDITS builder = Gtk.Builder() builder.add_from_resource('/info/febvre/Komikku/about_dialog.ui') about_dialog = builder.get_object('about_dialog') about_dialog.set_authors([ *CREDITS['developers'], '', _('Contributors: Code, Patches, Debugging:'), '', *CREDITS['contributors'], '', ]) about_dialog.set_translator_credits('\n'.join(CREDITS['translators'])) about_dialog.set_modal(True) about_dialog.set_transient_for(self) if about_dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT): about_dialog.hide() def on_application_quit(self, window, event): def before_quit(): self.save_window_size() backup_db() if self.downloader.running or self.updater.running: def confirm_callback(): self.downloader.stop() self.updater.stop() while self.downloader.running or self.updater.running: time.sleep(0.1) continue before_quit() self.application.quit() message = [ _('Are you sure you want to quit?'), ] if self.downloader.running: message.append( _('Some chapters are currently being downloaded.')) if self.updater.running: message.append(_('Some mangas are currently being updated.')) self.confirm(_('Quit?'), '\n'.join(message), confirm_callback) return True before_quit() return False def on_key_press_event(self, widget, event): """ Go back navigation with <Escape> key: - Library <- Manga <- Reader - Exit selection mode (Library and Manga chapters) """ if event.keyval != Gdk.KEY_Escape: # Propagate the event further return False if self.page == 'library' and not self.library.selection_mode: return True self.on_left_button_clicked(None, None) return True def on_left_button_clicked(self, action, param): if self.page == 'library': if self.library.selection_mode: self.library.leave_selection_mode() else: AddDialog(self).open(action, param) elif self.page == 'card': if self.card.selection_mode: self.card.leave_selection_mode() else: self.library.show(invalidate_sort=True) elif self.page == 'reader': self.set_unfullscreen() # Refresh to update all previously chapters consulted (last page read may have changed) # and update info like disk usage self.card.refresh(self.reader.chapters_consulted) self.card.show() def on_resize(self, window): size = self.get_size() if self._prev_size and self._prev_size.width == size.width and self._prev_size.height == size.height: return self._prev_size = size self.library.on_resize() if self.page == 'reader': self.reader.on_resize() if size.width < 700: if self.mobile_width is True: return self.mobile_width = True self.change_layout() else: if self.mobile_width is True: self.mobile_width = False self.change_layout() def on_settings_menu_clicked(self, action, param): SettingsDialog(self).open(action, param) def on_shortcuts_menu_clicked(self, action, param): builder = Gtk.Builder() builder.add_from_resource( '/info/febvre/Komikku/ui/shortcuts_overview.ui') shortcuts_overview = builder.get_object('shortcuts_overview') shortcuts_overview.set_modal(True) shortcuts_overview.set_transient_for(self) shortcuts_overview.present() def on_window_state_event(self, widget, event): self.is_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0 self.is_fullscreen = (event.new_window_state & Gdk.WindowState.FULLSCREEN) != 0 def save_window_size(self): if not self.is_maximized and not self.is_fullscreen: size = self.get_size() Settings.get_default().window_size = [size.width, size.height] def set_fullscreen(self): if not self.is_fullscreen: self.reader.controls.on_fullscreen() self.fullscreen() def set_unfullscreen(self): if self.is_fullscreen: self.reader.controls.on_unfullscreen() self.unfullscreen() def show_notification(self, message, interval=5): self.builder.get_object('notification_label').set_text(message) self.builder.get_object('notification_revealer').set_reveal_child(True) revealer_timer = Timer(interval, GLib.idle_add, args=[self.hide_notification]) revealer_timer.start() def show_page(self, name, transition=True): if not transition: # Save defined transition type transition_type = self.stack.get_transition_type() # Set transition type to NONE self.stack.set_transition_type(Gtk.StackTransitionType.NONE) self.title_stack.set_transition_type(Gtk.StackTransitionType.NONE) self.stack.set_visible_child_name(name) self.title_stack.set_visible_child_name(name) if not transition: # Restore transition type self.stack.set_transition_type(transition_type) self.title_stack.set_transition_type(transition_type) self.page = name def toggle_fullscreen(self, *args): if self.is_fullscreen: self.set_unfullscreen() else: self.set_fullscreen()