class ShowInfoWindow(Gtk.Dialog): __gtype_name__ = 'ShowInfoWindow' info_container = GtkTemplate.Child() def __init__(self, engine, show_data, transient_for=None): Gtk.Dialog.__init__(self, transient_for=transient_for) self.init_template() self._engine = engine self._show = show_data info_box = ShowInfoBox(engine) info_box.load(show_data) info_box.show() self.info_container.pack_start(info_box, True, True, 0) @GtkTemplate.Callback def _on_dialog_close(self, widget): self.destroy() @GtkTemplate.Callback def _on_btn_website_clicked(self, btn): if self._show['url']: Gtk.show_uri(None, self._show['url'], Gdk.CURRENT_TIME)
class AccountRow(Gtk.ListBoxRow): __gtype_name__ = 'AccountRow' account_logo = GtkTemplate.Child() account_username = GtkTemplate.Child() account_api = GtkTemplate.Child() def __init__(self, account): Gtk.ListBoxRow.__init__(self) self.init_template() self.account = account if account['active']: self.account_username.set_text(account['username']) self.account_api.set_text(account['libname_desc']) self.account_logo.set_from_pixbuf(account['logo']) def get_account_id(self): return self.account['number'] def get_libname(self): return self.account['libname']
class MainView(Gtk.Box): __gtype_name__ = 'MainView' __gsignals__ = { 'error': (GObject.SignalFlags.RUN_FIRST, None, (str, )), 'error-fatal': (GObject.SignalFlags.RUN_FIRST, None, (str, )), 'show-action': (GObject.SignalFlags.RUN_FIRST, None, (int, object)), } image_container_box = GtkTemplate.Child() top_box = GtkTemplate.Child() show_title = GtkTemplate.Child() api_icon = GtkTemplate.Child() api_user = GtkTemplate.Child() btn_episode_remove = GtkTemplate.Child() btn_episode_show_entry = GtkTemplate.Child() entry_episode = GtkTemplate.Child() btn_episode_add = GtkTemplate.Child() btn_play_next = GtkTemplate.Child() spinbtn_score = GtkTemplate.Child() btn_score_set = GtkTemplate.Child() statusbox = GtkTemplate.Child() statusmodel = GtkTemplate.Child() notebook = GtkTemplate.Child() def __init__(self, config, debug=False): Gtk.Box.__init__(self) self.init_template() self._configfile = utils.to_config_path('ui-Gtk.json') self._config = config self._engine = None self._account = None self._debug = debug self._image_thread = None self._current_page = None self.statusbox_handler = None self.notebook_switch_handler = None self._pages = {} self._page_handler_ids = {} self._init_widgets() self._init_signals() def load_engine_account(self, engine, account): self._engine = engine self._account = account self._engine_start() self._init_signals_engine() def load_account_mediatype(self, account, mediatype): if account: self._account = account self._engine_reload(account, mediatype) def _init_widgets(self): self.image_box = ImageBox(100, 150) self.image_box.show() self.image_container_box.pack_start(self.image_box, False, False, 0) self.statusbar = Gtk.Statusbar() self.statusbar.push(0, 'Trackma-gtk ' + utils.VERSION) self.statusbar.show() self.pack_start(self.statusbar, False, False, 0) def _init_signals(self): self.btn_episode_remove.connect("clicked", self._on_btn_episode_remove_clicked) self.btn_episode_show_entry.connect("clicked", self._show_episode_entry) self.entry_episode.connect("activate", self._on_entry_episode_activate) self.entry_episode.connect("focus-out-event", self._hide_episode_entry) self.btn_episode_add.connect("clicked", self._on_btn_episode_add_clicked) self.btn_play_next.connect("clicked", self._on_btn_play_next_clicked, True) self.spinbtn_score.connect("activate", self._on_spinbtn_score_activate) self.btn_score_set.connect("clicked", self._on_spinbtn_score_activate) self.statusbox_handler = self.statusbox.connect( "changed", self._on_statusbox_changed) self.notebook_switch_handler = self.notebook.connect( "switch-page", self._on_switch_notebook_page) def _init_signals_engine(self): self._engine.connect_signal('episode_changed', self._on_changed_show_idle) self._engine.connect_signal('score_changed', self._on_changed_show_idle) self._engine.connect_signal('status_changed', self._on_changed_show_status_idle) self._engine.connect_signal('playing', self._on_playing_show_idle) self._engine.connect_signal('show_added', self._on_changed_show_status_idle) self._engine.connect_signal('show_deleted', self._on_changed_show_status_idle) self._engine.connect_signal('prompt_for_update', self._on_prompt_update_next_idle) def _engine_start(self): threading.Thread(target=self._engine_start_task).start() def _engine_start_task(self): if self._engine.loaded: return try: self._engine.start() except utils.TrackmaFatal as e: self.emit('error-fatal', e) return GLib.idle_add(self._update_widgets) def _engine_reload(self, account, mediatype): self.set_buttons_sensitive(False) threading.Thread(target=self._engine_reload_task, args=[account, mediatype]).start() def _engine_reload_task(self, account, mediatype): try: self._engine.reload(account, mediatype) except utils.TrackmaError as e: self.emit('error', e) except utils.TrackmaFatal as e: self.emit('error-fatal', e) return GLib.idle_add(self._update_widgets) def _update_widgets(self): self.statusbox.handler_block(self.statusbox_handler) self._reset_widgets() self._create_notebook_pages() self._set_score_ranges() self.populate_all_pages() self._populate_statusbox() self.statusbox.handler_unblock(self.statusbox_handler) self.set_status_idle("Ready.") self.set_buttons_sensitive_idle(True) def _reset_widgets(self): self.show_title.set_text('<span size="14000"><b>Trackma</b></span>') self.show_title.set_use_markup(True) self.image_box.reset() current_api = utils.available_libs[self._account['api']] api_iconfile = current_api[1] self.api_icon.set_from_file(api_iconfile) self.api_user.set_text("%s (%s)" % (self._engine.get_userconfig('username'), self._engine.api_info['mediatype'])) can_play = self._engine.mediainfo['can_play'] can_update = self._engine.mediainfo['can_update'] self.btn_play_next.set_sensitive(can_play) self.btn_episode_show_entry.set_sensitive(can_update) self.entry_episode.set_sensitive(can_update) self.btn_episode_add.set_sensitive(can_update) def _create_notebook_pages(self): statuses_nums = self._engine.mediainfo['statuses'] statuses_names = self._engine.mediainfo['statuses_dict'] self.notebook.handler_block(self.notebook_switch_handler) # Clear notebook for i in range(self.notebook.get_n_pages()): self.notebook.remove_page(-1) self._pages = {} self._page_handler_ids = {} # Insert pages for status in statuses_nums: self._pages[status] = NotebookPage(self._engine, self.notebook.get_n_pages(), status, self._config) self._page_handler_ids[status] = [] self._page_handler_ids[status].append(self._pages[status].connect( 'show-selected', self._on_show_selected)) self._page_handler_ids[status].append(self._pages[status].connect( 'show-action', self._on_show_action)) self._page_handler_ids[status].append(self._pages[status].connect( 'column-toggled', self._on_column_toggled)) self.notebook.append_page(self._pages[status], Gtk.Label(statuses_names[status])) self.notebook.handler_unblock(self.notebook_switch_handler) self.notebook.show_all() def populate_all_pages(self): for status in self._pages: self.populate_page(status) def populate_page(self, status): self._block_handlers_for_status(status) tree_view = self._pages[status].show_tree_view tree_view.append_start() library = self._engine.library() for show in self._engine.filter_list(tree_view.status_filter): tree_view.append(show, self._engine.altname(show['id']), library.get(show['id'])) tree_view.append_finish() self._unblock_handlers_for_status(status) def _block_handlers_for_status(self, status): for handler_id in self._page_handler_ids[status]: self._pages[status].handler_block(handler_id) def _unblock_handlers_for_status(self, status): for handler_id in self._page_handler_ids[status]: self._pages[status].handler_unblock(handler_id) def _populate_statusbox(self): statuses_nums = self._engine.mediainfo['statuses'] statuses_names = self._engine.mediainfo['statuses_dict'] self.statusmodel.clear() for status in statuses_nums: self.statusmodel.append([str(status), statuses_names[status]]) self.statusbox.set_model(self.statusmodel) self.statusbox.show_all() def _set_score_ranges(self): score_decimal_places = 0 if isinstance(self._engine.mediainfo['score_step'], float): score_decimal_places = len( str(self._engine.mediainfo['score_step']).split('.')[1]) self.spinbtn_score.set_value(0) self.spinbtn_score.set_digits(score_decimal_places) self.spinbtn_score.set_range(0, self._engine.mediainfo['score_max']) self.spinbtn_score.get_adjustment().set_step_increment( self._engine.mediainfo['score_step']) for view in self._pages.values(): view.decimals = score_decimal_places def set_status_idle(self, msg): # Thread safe GLib.idle_add(self._set_status, msg) def _set_status(self, msg): print(msg) self.statusbar.push(0, msg) def set_buttons_sensitive_idle(self, boolean): # Thread safe GLib.idle_add(self.set_buttons_sensitive, boolean) def set_buttons_sensitive(self, boolean, lists_too=True): if lists_too: self.notebook.set_sensitive(boolean) if self._current_page.selected_show or not boolean: if self._engine.mediainfo['can_play']: self.btn_play_next.set_sensitive(boolean) if self._engine.mediainfo['can_update']: self.btn_episode_show_entry.set_sensitive(boolean) self.entry_episode.set_sensitive(boolean) self.btn_episode_add.set_sensitive(boolean) self.btn_episode_remove.set_sensitive(boolean) self.btn_score_set.set_sensitive(boolean) self.spinbtn_score.set_sensitive(boolean) self.statusbox.set_sensitive(boolean) def _on_btn_episode_remove_clicked(self, widget): self.emit('show-action', ShowEventType.EPISODE_REMOVE, (self._current_page.selected_show, )) def _show_episode_entry(self, *args): self.btn_episode_show_entry.hide() self.entry_episode.set_text(self.btn_episode_show_entry.get_label()) self.entry_episode.show() self.entry_episode.grab_focus() def _on_entry_episode_activate(self, widget): try: episode = int(self.entry_episode.get_text()) self.emit('show-action', ShowEventType.EPISODE_SET, (self._current_page.selected_show, episode)) except ValueError: pass def _hide_episode_entry(self, *args): self.entry_episode.hide() self.btn_episode_show_entry.show() def _on_btn_episode_add_clicked(self, widget): self.emit('show-action', ShowEventType.EPISODE_ADD, (self._current_page.selected_show, )) def _on_btn_play_next_clicked(self, widget, playnext, ep=None): self.emit('show-action', ShowEventType.PLAY_NEXT, (self._current_page.selected_show, )) def _on_spinbtn_score_activate(self, widget): score = self.spinbtn_score.get_value() self.emit('show-action', ShowEventType.SET_SCORE, (self._current_page.selected_show, score)) def _on_statusbox_changed(self, widget): statusiter = self.statusbox.get_active_iter() status = self.statusmodel.get(statusiter, 0)[0] self.emit('show-action', ShowEventType.SET_STATUS, (self._current_page.selected_show, status)) def message_handler(self, classname, msgtype, msg): # Thread safe if msgtype == messenger.TYPE_WARN: self.set_status_idle("%s warning: %s" % (classname, msg)) elif msgtype != messenger.TYPE_DEBUG: self.set_status_idle("%s: %s" % (classname, msg)) elif self._debug: print('[D] {}: {}'.format(classname, msg)) def _on_changed_show_idle(self, show): GLib.idle_add(self._update_show, show) def _update_show(self, show): status = show['my_status'] self._pages[status].show_tree_view.update(show) if show['id'] == self._current_page.selected_show: self.btn_episode_show_entry.set_label(str(show['my_progress'])) self.spinbtn_score.set_value(show['my_score']) def change_show_title_idle(self, show, altname): GLib.idle_add(self._update_show_title, show, altname) def _update_show_title(self, show, altname): status = show['my_status'] self._pages[status].show_tree_view.update_title(show, altname) def _on_changed_show_status_idle(self, show, old_status=None): GLib.idle_add(self._update_show_status, show, old_status) def _update_show_status(self, show, old_status): # Rebuild lists status = show['my_status'] self.populate_page(status) if old_status: self.populate_page(old_status) pagenumber = self._pages[status].pagenumber self.notebook.set_current_page(pagenumber) self._pages[status].show_tree_view.select(show) def _on_playing_show_idle(self, show, is_playing, episode): GLib.idle_add(self._set_show_playing, show, is_playing, episode) def _set_show_playing(self, show, is_playing, episode): status = show['my_status'] self._pages[status].show_tree_view.playing(show, is_playing) def _on_prompt_update_next_idle(self, show, played_ep): GLib.idle_add(self._prompt_update_next, show, played_ep) def _prompt_update_next(self, show, played_ep): dialog = Gtk.MessageDialog( self.get_toplevel(), Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, "Update %s to episode %d?" % (show['title'], played_ep)) dialog.show_all() dialog.connect("response", self._on_response_update_next, show, played_ep) def _on_response_update_next(self, widget, response, show, played_ep): widget.destroy() if response == Gtk.ResponseType.YES: self.emit('show-action', ShowEventType.EPISODE_SET, (show['id'], played_ep)) def _on_switch_notebook_page(self, notebook, page, page_num): self._current_page = page self._update_widgets_for_selected_show() def _on_show_selected(self, page, selected_show): self._update_widgets_for_selected_show() def _update_widgets_for_selected_show(self): if not self._current_page.selected_show: self.set_buttons_sensitive(False, lists_too=False) return self.set_buttons_sensitive(True, lists_too=False) show = self._engine.get_show_info(self._current_page.selected_show) # Block handlers self.statusbox.handler_block(self.statusbox_handler) if self._image_thread is not None: self._image_thread.cancel() self.show_title.set_text('<span size="14000"><b>{0}</b></span>'.format( html.escape(show['title']))) self.show_title.set_use_markup(True) # Episode selector self.btn_episode_show_entry.set_label(str(show['my_progress'])) self._hide_episode_entry() # Status selector for i in self.statusmodel: if str(i[0]) == str(show['my_status']): self.statusbox.set_active_iter(i.iter) break # Score selector self.spinbtn_score.set_value(show['my_score']) # Image if show.get('image_thumb') or show.get('image'): utils.make_dir(utils.to_cache_path()) filename = utils.to_cache_path( "%s_%s_%s.jpg" % (self._engine.api_info['shortname'], self._engine.api_info['mediatype'], show['id'])) if os.path.isfile(filename): self.image_box.set_image(filename) else: self.image_box.set_image_remote( show.get('image_thumb') or show['image'], filename) else: self.image_box.set_text('No Image') # Unblock handlers self.statusbox.handler_unblock(self.statusbox_handler) def _on_show_action(self, page, event_type, data): self.emit('show-action', event_type, data) def get_current_status(self): return self._current_page.status def get_selected_show(self): if not self._current_page: return None return self._current_page.selected_show def _on_column_toggled(self, page, column_name, visible): if visible: # Make column visible self._config['visible_columns'].append(column_name) else: # Make column invisible if len(self._config['visible_columns']) <= 1: return # There should be at least 1 column visible self._config['visible_columns'].remove(column_name) for page in self._pages.values(): page.set_column_visible(column_name, visible) utils.save_config(self._config, self._configfile)
class TrackmaWindow(Gtk.ApplicationWindow): __gtype_name__ = 'TrackmaWindow' btn_appmenu = GtkTemplate.Child() btn_mediatype = GtkTemplate.Child() header_bar = GtkTemplate.Child() def __init__(self, app, debug=False): Gtk.ApplicationWindow.__init__(self, application=app) self.init_template() self._debug = debug self._configfile = utils.to_config_path('ui-Gtk.json') self._config = utils.parse_config(self._configfile, utils.gtk_defaults) self.statusicon = None self._main_view = None self._modals = [] self._account = None self._engine = None self.close_thread = None self.hidden = False self._init_widgets() def init_account_selection(self): manager = AccountManager() # Use the remembered account if there's one if manager.get_default(): self._create_engine(manager.get_default()) else: self._show_accounts(switch=False) def _init_widgets(self): Gtk.Window.set_default_icon_from_file(utils.DATADIR + '/icon.png') self.set_position(Gtk.WindowPosition.CENTER) self.set_title('Trackma') if self._config['remember_geometry']: self.resize(self._config['last_width'], self._config['last_height']) if not self._main_view: self._main_view = MainView(self._config) self._main_view.connect('error', self._on_main_view_error) self._main_view.connect('error-fatal', self._on_main_view_error_fatal) self._main_view.connect('show-action', self._on_show_action) self.add(self._main_view) self.connect('delete_event', self._on_delete_event) builder = Gtk.Builder.new_from_file( os.path.join(gtk_dir, 'data/shortcuts.ui')) help_overlay = builder.get_object('shortcuts-window') self.set_help_overlay(help_overlay) # Status icon if TrackmaStatusIcon.is_tray_available(): self.statusicon = TrackmaStatusIcon() self.statusicon.connect('hide-clicked', self._on_tray_hide_clicked) self.statusicon.connect('about-clicked', self._on_tray_about_clicked) self.statusicon.connect('quit-clicked', self._on_tray_quit_clicked) if self._config['show_tray']: self.statusicon.set_visible(True) else: self.statusicon.set_visible(False) # Don't show the main window if start in tray option is set if self.statusicon and self._config['show_tray'] and self._config[ 'start_in_tray']: self.hidden = True else: self.present() def _on_tray_hide_clicked(self, status_icon): self._destroy_modals() if self.hidden: self.present() if not self._engine: self._show_accounts(switch=False) else: self.hide() self.hidden = not self.hidden def _destroy_modals(self): self.get_help_overlay().hide() for modal_window in self._modals: modal_window.destroy() self._modals = [] def _on_tray_about_clicked(self, status_icon): self._on_about(None, None) def _on_tray_quit_clicked(self, status_icon): self._quit() def _on_delete_event(self, widget, event, data=None): if self.statusicon and self.statusicon.get_visible( ) and self._config['close_to_tray']: self.hidden = True self.hide() else: self._quit() return True def _create_engine(self, account): self._engine = Engine(account, self._message_handler) self._main_view.load_engine_account(self._engine, account) self._set_actions() self._set_mediatypes_menu() self._update_widgets(account) def _set_actions(self): builder = Gtk.Builder.new_from_file( os.path.join(gtk_dir, 'data/app-menu.ui')) settings = Gtk.Settings.get_default() if not settings.get_property("gtk-shell-shows-menubar"): self.btn_appmenu.set_menu_model(builder.get_object('app-menu')) else: self.get_application().set_menubar(builder.get_object('menu-bar')) self.btn_appmenu.set_property('visible', False) def add_action(name, callback): action = Gio.SimpleAction.new(name, None) action.connect('activate', callback) self.add_action(action) add_action('search', self._on_search) add_action('syncronize', self._on_synchronize) add_action('upload', self._on_upload) add_action('download', self._on_download) add_action('scanfiles', self._on_scanfiles) add_action('accounts', self._on_accounts) add_action('preferences', self._on_preferences) add_action('about', self._on_about) add_action('play_next', self._on_action_play_next) add_action('play_random', self._on_action_play_random) add_action('episode_add', self._on_action_episode_add) add_action('episode_remove', self._on_action_episode_remove) add_action('delete', self._on_action_delete) add_action('copy', self._on_action_copy) def _set_mediatypes_action(self): action_name = 'change-mediatype' if self.has_action(action_name): self.remove_action(action_name) state = GLib.Variant.new_string(self._engine.api_info['mediatype']) action = Gio.SimpleAction.new_stateful(action_name, state.get_type(), state) action.connect('change-state', self._on_change_mediatype) self.add_action(action) def _set_mediatypes_menu(self): self._set_mediatypes_action() menu = Gio.Menu() for mediatype in self._engine.api_info['supported_mediatypes']: variant = GLib.Variant.new_string(mediatype) menu_item = Gio.MenuItem() menu_item.set_label(mediatype) menu_item.set_action_and_target_value('win.change-mediatype', variant) menu.append_item(menu_item) self.btn_mediatype.set_menu_model(menu) if len(self._engine.api_info['supported_mediatypes']) <= 1: self.btn_mediatype.hide() def _update_widgets(self, account): current_api = utils.available_libs[account['api']] api_iconpath = 1 api_iconfile = current_api[api_iconpath] self.header_bar.set_subtitle(self._engine.api_info['name'] + " (" + self._engine.api_info['mediatype'] + ")") if self.statusicon and self._config['tray_api_icon']: self.statusicon.set_from_file(api_iconfile) def _on_change_mediatype(self, action, value): action.set_state(value) mediatype = value.get_string() self._main_view.load_account_mediatype(None, mediatype, self.header_bar) def _on_search(self, action, param): current_status = self._main_view.get_current_status() win = SearchWindow(self._engine, self._config['colors'], current_status, transient_for=self) win.connect('search-error', self._on_search_error) win.connect('destroy', self._on_modal_destroy) win.present() self._modals.append(win) def _on_search_error(self, search_window, error_msg): print(error_msg) def _on_synchronize(self, action, param): threading.Thread(target=self._synchronization_task, args=(True, True)).start() def _on_upload(self, action, param): threading.Thread(target=self._synchronization_task, args=(True, False)).start() def _on_download(self, action, param): def _download_lists(): threading.Thread(target=self._synchronization_task, args=(False, True)).start() def _on_download_response(_dialog, response): _dialog.destroy() if response == Gtk.ResponseType.YES: _download_lists() queue = self._engine.get_queue() if queue: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, "There are %d queued changes in your list. If you retrieve the remote list now you will lose your queued changes. Are you sure you want to continue?" % len(queue)) dialog.show_all() dialog.connect("response", _on_download_response) else: # If the user doesn't have any queued changes # just go ahead _download_lists() def _synchronization_task(self, send, retrieve): self._set_buttons_sensitive_idle(False) try: if send: self._engine.list_upload() if retrieve: self._engine.list_download() # GLib.idle_add(self._set_score_ranges) GLib.idle_add(self._main_view.populate_all_pages) except utils.TrackmaError as e: self._error_dialog_idle(e) except utils.TrackmaFatal as e: self._show_accounts_idle(switch=False, forget=True) self._error_dialog_idle("Fatal engine error: %s" % e) return self._main_view.set_status_idle("Ready.") self._set_buttons_sensitive_idle(True) def _on_scanfiles(self, action, param): threading.Thread(target=self._scanfiles_task).start() def _scanfiles_task(self): self._set_buttons_sensitive_idle(False) try: self._engine.scan_library(rescan=True) except utils.TrackmaError as e: self._error_dialog_idle(e) GLib.idle_add(self._main_view.populate_all_pages) self._main_view.set_status_idle("Ready.") self._set_buttons_sensitive_idle(True) def _on_accounts(self, action, param): self._show_accounts() def _show_accounts_idle(self, switch=True, forget=False): GLib.idle_add(self._show_accounts, switch, forget) def _show_accounts(self, switch=True, forget=False): manager = AccountManager() if forget: manager.set_default(None) accountsel = AccountsWindow(manager, transient_for=self) accountsel.connect('account-open', self._on_account_open) accountsel.connect('account-cancel', self._on_account_cancel, switch) accountsel.connect('destroy', self._on_modal_destroy) accountsel.present() self._modals.append(accountsel) def _on_account_open(self, accounts_window, account_num, remember): manager = AccountManager() account = manager.get_account(account_num) if remember: manager.set_default(account_num) else: manager.set_default(None) # Reload the engine if already started, # start it otherwise if self._engine and self._engine.loaded: self._main_view.load_account_mediatype(account, None, None) else: self._create_engine(account) def _on_account_cancel(self, _accounts_window, switch): manager = AccountManager() if not switch or not manager.get_accounts(): self._quit() def _on_preferences(self, _action, _param): win = SettingsWindow(self._engine, self._config, self._configfile, transient_for=self) win.connect('destroy', self._on_modal_destroy) win.present() self._modals.append(win) def _on_about(self, _action, _param): about = Gtk.AboutDialog(parent=self) about.set_modal(True) about.set_transient_for(self) about.set_program_name("Trackma GTK") about.set_version(utils.VERSION) about.set_license_type(Gtk.License.GPL_3_0_ONLY) about.set_comments( "Trackma is an open source client for media tracking websites.\nThanks to all contributors." ) about.set_website("http://github.com/z411/trackma") about.set_copyright("© z411, et al.") about.set_authors(["See AUTHORS file"]) about.set_artists(["shuuichi"]) about.connect('destroy', self._on_modal_destroy) about.connect('response', lambda dialog, response: dialog.destroy()) about.present() self._modals.append(about) def _on_modal_destroy(self, modal_window): self._modals.remove(modal_window) def _quit(self): if self._config['remember_geometry']: self._store_geometry() if not self._engine: self.get_application().quit() return if self.close_thread is None: self._set_buttons_sensitive_idle(False) self.close_thread = threading.Thread(target=self._unload_task) self.close_thread.start() def _unload_task(self): self._engine.unload() GLib.idle_add(self.get_application().quit) def _store_geometry(self): (width, height) = self.get_size() self._config['last_width'] = width self._config['last_height'] = height utils.save_config(self._config, self._configfile) def _message_handler(self, classname, msgtype, msg): # Thread safe # print("%s: %s" % (classname, msg)) if msgtype == messenger.TYPE_WARN: self._main_view.set_status_idle("%s warning: %s" % (classname, msg)) elif msgtype != messenger.TYPE_DEBUG: self._main_view.set_status_idle("%s: %s" % (classname, msg)) elif self._debug: print('[D] {}: {}'.format(classname, msg)) def _on_main_view_error(self, main_view, error_msg): self._error_dialog_idle(error_msg) def _on_main_view_error_fatal(self, main_view, error_msg): self._show_accounts_idle(switch=False, forget=True) self._error_dialog_idle(error_msg) def _error_dialog_idle(self, msg, icon=Gtk.MessageType.ERROR): # Thread safe GLib.idle_add(self._error_dialog, msg, icon) def _error_dialog(self, msg, icon=Gtk.MessageType.ERROR): def error_dialog_response(widget, response_id): widget.destroy() dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL, icon, Gtk.ButtonsType.OK, str(msg)) dialog.show_all() dialog.connect("response", error_dialog_response) print('Error: {}'.format(msg)) def _on_action_play_next(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._play_next(selected_show) def _on_action_play_random(self, action, param): self._play_random() def _on_action_episode_add(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._episode_add(selected_show) def _on_action_episode_remove(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._episode_remove(selected_show) def _on_action_delete(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._remove_show(selected_show) def _on_action_copy(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._copy_title(selected_show) def _on_show_action(self, main_view, event_type, data): if event_type == ShowEventType.PLAY_NEXT: self._play_next(*data) elif event_type == ShowEventType.PLAY_EPISODE: self._play_episode(*data) elif event_type == ShowEventType.EPISODE_REMOVE: self._episode_remove(*data) elif event_type == ShowEventType.EPISODE_SET: self._episode_set(*data) elif event_type == ShowEventType.EPISODE_ADD: self._episode_add(*data) elif event_type == ShowEventType.SET_SCORE: self._set_score(*data) elif event_type == ShowEventType.SET_STATUS: self._set_status(*data) elif event_type == ShowEventType.DETAILS: self._open_details(*data) elif event_type == ShowEventType.OPEN_WEBSITE: self._open_website(*data) elif event_type == ShowEventType.OPEN_FOLDER: self._open_folder(*data) elif event_type == ShowEventType.COPY_TITLE: self._copy_title(*data) elif event_type == ShowEventType.CHANGE_ALTERNATIVE_TITLE: self._change_alternative_title(*data) elif event_type == ShowEventType.REMOVE: self._remove_show(*data) def _play_next(self, show_id): threading.Thread(target=self._play_task, args=[show_id, True, None]).start() def _play_episode(self, show_id, episode): threading.Thread(target=self._play_task, args=[show_id, False, episode]).start() def _play_task(self, show_id, playnext, episode): self._set_buttons_sensitive_idle(False) show = self._engine.get_show_info(show_id) try: if playnext: self._engine.play_episode(show) else: if not episode: episode = self.show_ep_num.get_value_as_int() self._engine.play_episode(show, episode) except utils.TrackmaError as e: self._error_dialog_idle(e) self._main_view.set_status_idle("Ready.") self._set_buttons_sensitive_idle(True) def _play_random(self): threading.Thread(target=self._play_random_task).start() def _play_random_task(self): self._set_buttons_sensitive_idle(False) try: self._engine.play_random() except utils.TrackmaError as e: self._error_dialog_idle(e) self._main_view.set_status_idle("Ready.") self._set_buttons_sensitive_idle(True) def _episode_add(self, show_id): show = self._engine.get_show_info(show_id) self._episode_set(show_id, show['my_progress'] + 1) def _episode_remove(self, show_id): show = self._engine.get_show_info(show_id) self._episode_set(show_id, show['my_progress'] - 1) def _episode_set(self, show_id, episode): try: self._engine.set_episode(show_id, episode) except utils.TrackmaError as e: self._error_dialog(e) def _set_score(self, show_id, score): try: self._engine.set_score(show_id, score) except utils.TrackmaError as e: self._error_dialog(e) def _set_status(self, show_id, status): try: self._engine.set_status(show_id, status) except utils.TrackmaError as e: self._error_dialog(e) def _open_details(self, show_id): show = self._engine.get_show_info(show_id) win = ShowInfoWindow(self._engine, show, transient_for=self) win.connect('destroy', self._on_modal_destroy) win.present() self._modals.append(win) def _open_website(self, show_id): show = self._engine.get_show_info(show_id) if show['url']: Gtk.show_uri(None, show['url'], Gdk.CURRENT_TIME) def _open_folder(self, show_id): show = self._engine.get_show_info(show_id) try: filename = self._engine.get_episode_path(show, 1) with open(os.devnull, 'wb') as DEVNULL: if sys.platform == 'darwin': subprocess.Popen( ["open", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) elif sys.platform == 'win32': subprocess.Popen( ["explorer", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) else: subprocess.Popen( ["/usr/bin/xdg-open", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) except OSError: # xdg-open failed. raise utils.EngineError("Could not open folder.") except utils.EngineError: # Show not in library. self._error_dialog_idle("No folder found.") def _copy_title(self, show_id): show = self._engine.get_show_info(show_id) clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(show['title'], -1) self._main_view.set_status_idle('Title copied to clipboard.') def _change_alternative_title(self, show_id): show = self._engine.get_show_info(show_id) current_altname = self._engine.altname(show_id) def altname_response(entry, dialog, response): dialog.response(response) dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, None) dialog.set_markup('Set the <b>alternate title</b> for the show.') entry = Gtk.Entry() entry.set_text(current_altname) entry.connect("activate", altname_response, dialog, Gtk.ResponseType.OK) hbox = Gtk.HBox() hbox.pack_start(Gtk.Label("Alternate Title:"), False, 5, 5) hbox.pack_end(entry, True, True, 0) dialog.format_secondary_markup( "Use this if the tracker is unable to find this show. Leave blank to disable." ) dialog.vbox.pack_end(hbox, True, True, 0) dialog.show_all() retval = dialog.run() if retval == Gtk.ResponseType.OK: text = entry.get_text() self._engine.altname(show_id, text) self._main_view.change_show_title_idle(show, text) dialog.destroy() def _remove_show(self, show_id): try: show = self._engine.get_show_info(show_id) self._engine.delete_show(show) except utils.TrackmaError as e: self._error_dialog_idle(e) def _set_buttons_sensitive_idle(self, sensitive): GLib.idle_add(self._set_buttons_sensitive, sensitive) self._main_view.set_buttons_sensitive_idle(sensitive) def _set_buttons_sensitive(self, sensitive): actions_names = [ 'search', 'syncronize', 'upload', 'download', 'scanfiles', 'accounts', 'play_next', 'play_random', 'episode_add', 'episode_remove', 'delete', 'copy' ] for action_name in actions_names: action = self.lookup_action(action_name) if action is not None: action.set_enabled(sensitive)
class SearchWindow(Gtk.Window): __gtype_name__ = 'SearchWindow' __gsignals__ = {'search-error': (GObject.SIGNAL_RUN_FIRST, None, (str, ))} btn_add_show = GtkTemplate.Child() search_paned = GtkTemplate.Child() shows_viewport = GtkTemplate.Child() show_info_container = GtkTemplate.Child() def __init__(self, engine, colors, current_status, transient_for=None): Gtk.Window.__init__(self, transient_for=transient_for) self.init_template() self._entries = [] self._selected_show = None self._showdict = None self._engine = engine self._current_status = current_status self._search_thread = None self.showlist = SearchTreeView(colors) self.showlist.get_selection().connect("changed", self._on_selection_changed) self.info = ShowInfoBox(engine) self.info.set_size_request(200, 350) self.shows_viewport.add(self.showlist) self.show_info_container.pack_start(self.info, True, True, 0) self.search_paned.set_position(400) @GtkTemplate.Callback def _on_search_entry_search_changed(self, search_entry): self._search(search_entry.get_text()) def _search(self, text): if self._search_thread: self._search_thread.stop() self._search_thread = SearchThread(self._engine, text, self._search_finish_idle) self._search_thread.start() def _search_finish_idle(self, entries, error): if error: self.emit('search-error', error) return self._entries = entries self._showdict = dict() self.showlist.append_start() for show in entries: self._showdict[show['id']] = show self.showlist.append(show) self.showlist.append_finish() self.btn_add_show.set_sensitive(False) @GtkTemplate.Callback def _on_btn_add_show_clicked(self, btn): show = self._get_full_selected_show() if show is not None: self._add_show(show) def _get_full_selected_show(self): for item in self._entries: if item['id'] == self._selected_show: return item return None def _add_show(self, show): try: self._engine.add_show(show, self._current_status) except utils.TrackmaError as e: self.emit('search-error', e) def _on_selection_changed(self, selection): # Get selected show ID (tree_model, tree_iter) = selection.get_selected() if not tree_iter: return self._selected_show = int(tree_model.get(tree_iter, 0)[0]) if self._selected_show in self._showdict: self.info.load(self._showdict[self._selected_show]) self.btn_add_show.set_sensitive(True)
class SettingsWindow(Gtk.Window): __gtype_name__ = 'SettingsWindow' btn_save = GtkTemplate.Child() switch_tracker = GtkTemplate.Child() radio_tracker_local = GtkTemplate.Child() entry_player_process = GtkTemplate.Child() btn_file_chooser_executable = GtkTemplate.Child() listbox_directories = GtkTemplate.Child() btn_add_directory = GtkTemplate.Child() checkbox_library_startup = GtkTemplate.Child() checkbox_library_entire_list = GtkTemplate.Child() checkbox_library_full_path = GtkTemplate.Child() radio_tracker_plex = GtkTemplate.Child() entry_plex_host = GtkTemplate.Child() spin_plex_port = GtkTemplate.Child() entry_plex_username = GtkTemplate.Child() entry_plex_password = GtkTemplate.Child() checkbox_plex_obey_wait = GtkTemplate.Child() spin_tracker_update_wait = GtkTemplate.Child() checkbox_tracker_update_close = GtkTemplate.Child() checkbox_tracker_update_prompt = GtkTemplate.Child() checkbox_tracker_not_found_prompt = GtkTemplate.Child() radiobutton_download_days = GtkTemplate.Child() radiobutton_download_always = GtkTemplate.Child() radiobutton_download_off = GtkTemplate.Child() radiobutton_upload_minutes = GtkTemplate.Child() radiobutton_upload_size = GtkTemplate.Child() radiobutton_upload_always = GtkTemplate.Child() radiobutton_upload_off = GtkTemplate.Child() checkbox_upload_exit = GtkTemplate.Child() spinbutton_download_days = GtkTemplate.Child() spinbutton_upload_minutes = GtkTemplate.Child() spinbutton_upload_size = GtkTemplate.Child() checkbox_auto_status_change = GtkTemplate.Child() checkbox_auto_status_change_if_scored = GtkTemplate.Child() checkbox_auto_date_change = GtkTemplate.Child() checkbox_show_tray = GtkTemplate.Child() checkbox_close_to_tray = GtkTemplate.Child() checkbox_start_in_tray = GtkTemplate.Child() checkbox_tray_api_icon = GtkTemplate.Child() checkbox_remember_geometry = GtkTemplate.Child() checkbox_classic_progress = GtkTemplate.Child() colorbutton_rows_playing = GtkTemplate.Child() colorbutton_rows_queued = GtkTemplate.Child() colorbutton_rows_new_episode = GtkTemplate.Child() colorbutton_rows_is_airing = GtkTemplate.Child() colorbutton_rows_not_aired = GtkTemplate.Child() colorbutton_progress_bg = GtkTemplate.Child() colorbutton_progress_fg = GtkTemplate.Child() colorbutton_progress_sub_bg = GtkTemplate.Child() colorbutton_progress_sub_fg = GtkTemplate.Child() colorbutton_progress_complete = GtkTemplate.Child() def __init__(self, engine, config, configfile, transient_for=None): Gtk.Window.__init__(self, transient_for=transient_for) self.init_template() self.engine = engine self.config = config self.configfile = configfile self._color_buttons = { 'is_playing': self.colorbutton_rows_playing, 'is_queued': self.colorbutton_rows_queued, 'new_episode': self.colorbutton_rows_new_episode, 'is_airing': self.colorbutton_rows_is_airing, 'not_aired': self.colorbutton_rows_not_aired, 'progress_bg': self.colorbutton_progress_bg, 'progress_fg': self.colorbutton_progress_fg, 'progress_sub_bg': self.colorbutton_progress_sub_bg, 'progress_sub_fg': self.colorbutton_progress_sub_fg, 'progress_complete': self.colorbutton_progress_complete } self.radiobutton_download_days.connect("toggled", self._button_toggled, self.spinbutton_download_days) self.radiobutton_upload_minutes.connect("toggled", self._button_toggled, self.spinbutton_upload_minutes) self.radiobutton_upload_size.connect("toggled", self._button_toggled, self.spinbutton_upload_size) self.checkbox_auto_status_change.connect( "toggled", self._button_toggled, self.checkbox_auto_status_change_if_scored) self.checkbox_show_tray.connect("toggled", self._button_toggled, self.checkbox_close_to_tray) self.checkbox_show_tray.connect("toggled", self._button_toggled, self.checkbox_start_in_tray) self.checkbox_show_tray.connect("toggled", self._button_toggled, self.checkbox_tray_api_icon) self._load_config() def _load_config(self): """Engine Configuration""" self.switch_tracker.set_active( self.engine.get_config('tracker_enabled')) if self.engine.get_config('tracker_type') == 'local': self.radio_tracker_local.set_active(True) elif self.engine.get_config('tracker_type') == 'plex': self.radio_tracker_plex.set_active(True) self.entry_player_process.set_text( self.engine.get_config('tracker_process')) self.btn_file_chooser_executable.set_filename( self.engine.get_config('player')) self.checkbox_library_startup.set_active( self.engine.get_config('library_autoscan')) self.checkbox_library_entire_list.set_active( self.engine.get_config('scan_whole_list')) self.checkbox_library_full_path.set_active( self.engine.get_config('library_full_path')) self._load_directories(self.engine.get_config('searchdir')) self.entry_plex_host.set_text(self.engine.get_config('plex_host')) self.spin_plex_port.set_value(int(self.engine.get_config('plex_port'))) self.entry_plex_username.set_text(self.engine.get_config('plex_user')) self.entry_plex_password.set_text( self.engine.get_config('plex_passwd')) self.checkbox_plex_obey_wait.set_active( self.engine.get_config('plex_obey_update_wait_s')) self.spin_tracker_update_wait.set_value( self.engine.get_config('tracker_update_wait_s')) self.checkbox_tracker_update_close.set_active( self.engine.get_config('tracker_update_close')) self.checkbox_tracker_update_prompt.set_active( self.engine.get_config('tracker_update_prompt')) self.checkbox_tracker_not_found_prompt.set_active( self.engine.get_config('tracker_not_found_prompt')) if self.engine.get_config('autoretrieve') == 'always': self.radiobutton_download_always.set_active(True) elif self.engine.get_config('autoretrieve') == 'days': self.radiobutton_download_days.set_active(True) else: self.radiobutton_download_off.set_active(True) if self.engine.get_config('autosend') == 'always': self.radiobutton_upload_always.set_active(True) elif self.engine.get_config('autosend') in ('minutes', 'hours'): self.radiobutton_upload_minutes.set_active(True) elif self.engine.get_config('autosend') == 'size': self.radiobutton_upload_size.set_active(True) else: self.radiobutton_upload_off.set_active(True) self.checkbox_upload_exit.set_active( self.engine.get_config('autosend_at_exit')) self.spinbutton_download_days.set_value( self.engine.get_config('autoretrieve_days')) self.spinbutton_upload_minutes.set_value( self.engine.get_config('autosend_minutes')) self.spinbutton_upload_size.set_value( self.engine.get_config('autosend_size')) self.checkbox_auto_status_change.set_active( self.engine.get_config('auto_status_change')) self.checkbox_auto_status_change_if_scored.set_active( self.engine.get_config('auto_status_change_if_scored')) self.checkbox_auto_date_change.set_active( self.engine.get_config('auto_date_change')) self.checkbox_show_tray.set_active(self.config['show_tray']) self.checkbox_close_to_tray.set_active(self.config['close_to_tray']) self.checkbox_start_in_tray.set_active(self.config['start_in_tray']) self.checkbox_tray_api_icon.set_active(self.config['tray_api_icon']) """GTK Interface configuration""" self.checkbox_remember_geometry.set_active( self.config['remember_geometry']) self.checkbox_classic_progress.set_active( not self.config['episodebar_style']) for color_key, color_button in self._color_buttons.items(): color = getColor(self.config['colors'][color_key]) color_button.set_color(color) self._set_tracker_radio_buttons() self._button_toggled(self.radiobutton_download_days, self.spinbutton_download_days) self._button_toggled(self.radiobutton_upload_minutes, self.spinbutton_upload_minutes) self._button_toggled(self.radiobutton_upload_size, self.spinbutton_upload_size) self._button_toggled(self.checkbox_auto_status_change, self.checkbox_auto_status_change_if_scored) self._button_toggled(self.checkbox_show_tray, self.checkbox_close_to_tray) self._button_toggled(self.checkbox_show_tray, self.checkbox_start_in_tray) self._button_toggled(self.checkbox_show_tray, self.checkbox_tray_api_icon) if not TrackmaStatusIcon.is_tray_available(): self.checkbox_show_tray.set_label( 'Show tray icon (Not supported in this environment)') self.checkbox_show_tray.set_sensitive(False) self.checkbox_close_to_tray.set_sensitive(False) self.checkbox_start_in_tray.set_sensitive(False) self.checkbox_tray_api_icon.set_sensitive(False) def _button_toggled(self, widget, spin): spin.set_sensitive(widget.get_active()) @GtkTemplate.Callback def _on_btn_save_clicked(self, btn): self.save_config() self.destroy() @GtkTemplate.Callback def _on_switch_tracker_state_set(self, switch, state): self.radio_tracker_local.set_sensitive(state) self.radio_tracker_plex.set_sensitive(state) if state: self._set_tracker_radio_buttons() else: self._enable_local(state) self._enable_plex(state) self.checkbox_tracker_update_close.set_sensitive(state) self.checkbox_tracker_update_prompt.set_sensitive(state) self.checkbox_tracker_not_found_prompt.set_sensitive(state) @GtkTemplate.Callback def _on_radio_tracker_local_toggled(self, radio_button): self._set_tracker_radio_buttons() @GtkTemplate.Callback def _on_radio_tracker_plex_toggled(self, radio_button): self._set_tracker_radio_buttons() def _set_tracker_radio_buttons(self): if self.radio_tracker_local.get_active(): self._enable_local(True) self._enable_plex(False) else: self._enable_local(False) self._enable_plex(True) def _enable_local(self, enable): self.entry_player_process.set_sensitive(enable) self.btn_file_chooser_executable.set_sensitive(enable) self.checkbox_library_startup.set_sensitive(enable) self.checkbox_library_entire_list.set_sensitive(enable) self.checkbox_library_full_path.set_sensitive(enable) def _enable_plex(self, enable): self.entry_plex_host.set_sensitive(enable) self.spin_plex_port.set_sensitive(enable) self.entry_plex_username.set_sensitive(enable) self.entry_plex_password.set_sensitive(enable) self.checkbox_plex_obey_wait.set_sensitive(enable) def _load_directories(self, paths): if isinstance(paths, str): paths = [paths] for path in paths: self._add_row_listbox_directory(path) def _add_row_listbox_directory(self, path): row = DirectoryRow(path) self.listbox_directories.add(row) @GtkTemplate.Callback def _on_btn_add_directory_clicked(self, btn): chooser_dialog = Gtk.FileChooserDialog( 'Select a directory', self.get_parent_window(), Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) chooser_dialog.set_default_response(Gtk.ResponseType.OK) chooser_dialog.set_action(Gtk.FileChooserAction.SELECT_FOLDER) response = chooser_dialog.run() if response == Gtk.ResponseType.OK: self._add_row_listbox_directory(chooser_dialog.get_filename()) chooser_dialog.destroy() def save_config(self): """Engine Configuration""" self.engine.set_config( 'player', self.btn_file_chooser_executable.get_filename() or '') self.engine.set_config('tracker_process', self.entry_player_process.get_text()) self.engine.set_config('library_autoscan', self.checkbox_library_startup.get_active()) self.engine.set_config('scan_whole_list', self.checkbox_library_entire_list.get_active()) self.engine.set_config('library_full_path', self.checkbox_library_full_path.get_active()) self.engine.set_config('plex_host', self.entry_plex_host.get_text()) self.engine.set_config('plex_port', str(int(self.spin_plex_port.get_value()))) self.engine.set_config('plex_obey_update_wait_s', self.checkbox_plex_obey_wait.get_active()) self.engine.set_config('plex_user', self.entry_plex_username.get_text()) self.engine.set_config('plex_passwd', self.entry_plex_password.get_text()) self.engine.set_config('tracker_enabled', self.switch_tracker.get_active()) self.engine.set_config('autosend_at_exit', self.checkbox_upload_exit.get_active()) self.engine.set_config('tracker_update_wait_s', self.spin_tracker_update_wait.get_value()) self.engine.set_config('tracker_update_close', self.checkbox_tracker_update_close.get_active()) self.engine.set_config( 'tracker_update_prompt', self.checkbox_tracker_update_prompt.get_active()) self.engine.set_config( 'tracker_not_found_prompt', self.checkbox_tracker_not_found_prompt.get_active()) self.engine.set_config( 'searchdir', [row.directory for row in self.listbox_directories.get_children()]) # Tracker type if self.radio_tracker_local.get_active(): self.engine.set_config('tracker_type', 'local') elif self.radio_tracker_plex.get_active(): self.engine.set_config('tracker_type', 'plex') # Auto-retrieve if self.radiobutton_download_always.get_active(): self.engine.set_config('autoretrieve', 'always') elif self.radiobutton_download_days.get_active(): self.engine.set_config('autoretrieve', 'days') else: self.engine.set_config('autoretrieve', 'off') # Auto-send if self.radiobutton_upload_always.get_active(): self.engine.set_config('autosend', 'always') elif self.radiobutton_upload_minutes.get_active(): self.engine.set_config('autosend', 'minutes') elif self.radiobutton_upload_size.get_active(): self.engine.set_config('autosend', 'size') else: self.engine.set_config('autosend', 'off') self.engine.set_config( 'autoretrieve_days', self.spinbutton_download_days.get_value_as_int()) self.engine.set_config( 'autosend_minutes', self.spinbutton_upload_minutes.get_value_as_int()) self.engine.set_config('autosend_size', self.spinbutton_upload_size.get_value_as_int()) self.engine.set_config('auto_status_change', self.checkbox_auto_status_change.get_active()) self.engine.set_config( 'auto_status_change_if_scored', self.checkbox_auto_status_change_if_scored.get_active()) self.engine.set_config('auto_date_change', self.checkbox_auto_date_change.get_active()) self.engine.save_config() """GTK Interface configuration""" self.config['show_tray'] = self.checkbox_show_tray.get_active() if self.checkbox_show_tray.get_active(): self.config[ 'close_to_tray'] = self.checkbox_close_to_tray.get_active() self.config[ 'start_in_tray'] = self.checkbox_start_in_tray.get_active() self.config[ 'tray_api_icon'] = self.checkbox_tray_api_icon.get_active() else: self.config['close_to_tray'] = False self.config['start_in_tray'] = False self.config['tray_api_icon'] = False self.config[ 'remember_geometry'] = self.checkbox_remember_geometry.get_active( ) self.config['episodebar_style'] = int( not self.checkbox_classic_progress.get_active()) """Update Colors""" self.config['colors'] = { key: reprColor(col.get_color()) for key, col in self._color_buttons.items() } utils.save_config(self.config, self.configfile)
class ShowInfoBox(Gtk.Box): __gtype_name__ = 'ShowInfoBox' label_title = GtkTemplate.Child() data_container = GtkTemplate.Child() image_container = GtkTemplate.Child() def __init__(self, engine, orientation=Gtk.Orientation.HORIZONTAL): Gtk.Box.__init__(self) self.init_template() self._engine = engine self._show = None self.image_thread = None self.details = None self.details_e = None self.image_box = ImageBox(225, 300) self.image_box.show() self.image_container.pack_start(self.image_box, False, False, 0) self.data_label = Gtk.Label('') self.data_label.set_line_wrap(True) self.data_label.set_property('selectable', True) if isinstance(orientation, Gtk.Orientation): self.data_container.set_orientation(orientation) self.data_container.pack_start(self.data_label, True, True, 0) def set_size(self, w, h): self.scrolled_sidebox.set_size_request(w, h) def load(self, show): self._show = show # Load image if show.get('image'): imagefile = utils.to_cache_path( "%s_%s_f_%s.jpg" % (self._engine.api_info['shortname'], self._engine.api_info['mediatype'], show['id'])) if os.path.isfile(imagefile): self.image_box.set_image(imagefile) else: self.image_box.set_image_remote(show['image'], imagefile) else: self.image_box.set_text('No Image') # Start info loading thread threading.Thread(target=self._show_load_start_task).start() def _show_load_start_task(self): # Thread to ask the engine for show details try: self.details = self._engine.get_show_details(self._show) except utils.TrackmaError as e: self.details = None self.details_e = e GObject.idle_add(self._show_load_finish_idle) def _show_load_finish_idle(self): if self.details: # Put the returned details into the lines VBox self.label_title.set_text(html.escape(self.details['title'])) detail = list() for line in self.details['extra']: if line[0] and line[1]: title, content, *_ = line if isinstance(content, list): content = ", ".join(filter(None, content)) detail.append( "<b>%s</b>\n%s" % (html.escape(str(title)), html.escape(str(content)))) self.data_label.set_text("\n\n".join(detail)) self.data_label.set_use_markup(True) else: self.label_title.set_text('Error while getting details.') if self.details_e: self.data_label.set_text(str(self.details_e)) self.label_title.show() self.data_label.show()
class AccountsWindow(Gtk.Dialog): __gtype_name__ = 'AccountsWindow' __gsignals__ = { 'account-cancel': (GObject.SignalFlags.RUN_FIRST, None, ()), 'account-open': (GObject.SignalFlags.RUN_FIRST, None, (int, bool)) } header_bar = GtkTemplate.Child() internal_box = GtkTemplate.Child() accounts_frame = GtkTemplate.Child() accounts_listbox = GtkTemplate.Child() revealer_action_bar = GtkTemplate.Child() btn_cancel = GtkTemplate.Child() btn_add = GtkTemplate.Child() btn_new_confirm = GtkTemplate.Child() btn_new_cancel = GtkTemplate.Child() btn_edit_confirm = GtkTemplate.Child() remember_switch = GtkTemplate.Child() accounts_stack = GtkTemplate.Child() accounts_combo = GtkTemplate.Child() password_label = GtkTemplate.Child() password_entry = GtkTemplate.Child() username_label = GtkTemplate.Child() username_entry = GtkTemplate.Child() btn_pin_request = GtkTemplate.Child() def __init__(self, manager, **kwargs): super().__init__(self, **kwargs) self.init_template() self.accounts = [] self.pixbufs = {} self.treeiters = {} self.current = AccountsView.LIST self.manager = manager self.account_edit = None self.adding_allow = False self.adding_extra = {} self._remove_border() self._add_separators() self._refresh_remember() self._refresh_pixbufs() self._refresh_list() self._populate_combobox() def _remove_border(self): self.internal_box.set_border_width(0) def _add_separators(self): self.accounts_listbox.set_header_func( self._accounts_listbox_header_func, None) @staticmethod def _accounts_listbox_header_func(row, before, _user_data): if before is None: row.set_header(None) return current = row.get_header() if current is None: current = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) current.show() row.set_header(current) def _refresh_remember(self): remember = self.manager.get_default() is not None self.remember_switch.set_active(remember) def _refresh_pixbufs(self): self.pixbufs = {} for (libname, lib) in utils.available_libs.items(): self.pixbufs[libname] = GdkPixbuf.Pixbuf.new_from_file(lib[1]) def _refresh_list(self): for account in self.accounts: account.destroy() self.accounts = [] for k, account in self.manager.get_accounts(): libname = account['api'] try: api = utils.available_libs[libname] account = AccountRow({ 'number': k, 'username': account['username'], 'libname': libname, 'libname_desc': api[0], 'logo': self.pixbufs[libname], 'active': True }) except KeyError: # Invalid API account = AccountRow({ 'number': k, 'username': account['username'], 'libname': None, 'libname_desc': 'N/A', 'logo': None, 'active': False }) self.accounts_listbox.add(account) self.accounts.append(account) if not self.accounts: self.accounts_frame.hide() else: self.accounts_frame.show() @GtkTemplate.Callback def _on_dialog_close(self, dialog): self.emit('account-cancel') self.destroy() @GtkTemplate.Callback def _on_btn_cancel_clicked(self, btn): self.emit('account-cancel') self.destroy() @GtkTemplate.Callback def _on_row_selected(self, list_box, row): reveal = row is not None self.revealer_action_bar.set_reveal_child(reveal) @GtkTemplate.Callback def _on_row_activated(self, list_box, row): acc_num = row.get_account_id() remember = self.remember_switch.get_active() self.emit('account-open', acc_num, remember) self.destroy() @GtkTemplate.Callback def _on_btn_edit_clicked(self, btn): self._show_edit() @GtkTemplate.Callback def _on_btn_open_clicked(self, btn): acc_num = self.accounts_listbox.get_selected_row().get_account_id() remember = self.remember_switch.get_active() self.emit('account-open', acc_num, remember) self.destroy() @GtkTemplate.Callback def _on_btn_delete_clicked(self, btn): row = self.accounts_listbox.get_selected_row() self.manager.delete_account(row.get_account_id()) row.destroy() @GtkTemplate.Callback def _on_btn_add_clicked(self, btn): self._show_add_new() @GtkTemplate.Callback def _on_btn_new_cancel_clicked(self, btn): self._show_accounts_list() @GtkTemplate.Callback def _on_btn_new_confirm_clicked(self, btn): self._add_account() self._refresh_list() self._show_accounts_list() @GtkTemplate.Callback def _on_btn_edit_confirm_clicked(self, btn): self._edit_account() self._refresh_list() self._show_accounts_list() def _show_edit(self): row = self.accounts_listbox.get_selected_row() self.account_edit = self.manager.get_account(row.get_account_id()) self.account_edit['account_id'] = row.get_account_id() self.header_bar.set_title("Edit account") self._clear_new_account() if utils.available_libs[ self.account_edit['api']][2] == utils.LOGIN_OAUTH: self._show_oauth_account() else: self._show_password_account() self.accounts_combo.set_active_iter(self.treeiters[row.get_libname()]) self.username_entry.set_text(self.account_edit['username']) self.password_entry.set_text(self.account_edit['password']) self.accounts_combo.set_sensitive(False) self.username_entry.set_sensitive(False) self.btn_new_confirm.hide() self.btn_new_cancel.show() self.btn_edit_confirm.show() self.btn_add.hide() self.btn_cancel.hide() self.current = AccountsView.EDIT self.accounts_stack.set_visible_child_full( 'new_account', Gtk.StackTransitionType.SLIDE_LEFT) def _show_add_new(self): self.header_bar.set_title("Add account") self._clear_new_account() self.btn_new_confirm.show() self.btn_new_cancel.show() self.btn_add.hide() self.btn_cancel.hide() self.accounts_combo.set_sensitive(True) self.username_entry.set_sensitive(True) self.current = AccountsView.NEW self.accounts_stack.set_visible_child_full( 'new_account', Gtk.StackTransitionType.SLIDE_LEFT) def _show_accounts_list(self): self.header_bar.set_title("Accounts") self.btn_new_confirm.hide() self.btn_new_cancel.hide() self.btn_edit_confirm.hide() self.btn_add.show() self.btn_cancel.show() self.current = AccountsView.LIST self.accounts_stack.set_visible_child_full( 'accounts', Gtk.StackTransitionType.SLIDE_RIGHT) def _populate_combobox(self): model_api = Gtk.ListStore(str, str, GdkPixbuf.Pixbuf) for (libname, lib) in sorted(utils.available_libs.items()): self.treeiters[libname] = model_api.append( [libname, lib[0], self.pixbufs[libname]]) self.accounts_combo.set_model(model_api) @GtkTemplate.Callback def _on_accounts_combo_changed(self, combo): self.username_entry.set_text("") self.password_entry.set_text("") api = self._get_combo_active_api_name() if not api or utils.available_libs[api][2] in [ utils.LOGIN_OAUTH, utils.LOGIN_OAUTH_PKCE ]: # We'll only allow adding the account if the user requests the PIN self.adding_allow = False self._show_oauth_account() else: self.adding_allow = True self._show_password_account() @GtkTemplate.Callback def _on_btn_pin_request_clicked(self, btn): api = self._get_combo_active_api_name() auth_url = utils.available_libs[api][3] if utils.available_libs[api][2] == utils.LOGIN_OAUTH_PKCE: self.adding_extra = {'code_verifier': utils.oauth_generate_pkce()} auth_url = auth_url % self.adding_extra['code_verifier'] self.adding_allow = True webbrowser.open(auth_url, 2, True) def _clear_new_account(self): self.accounts_combo.set_active_id(None) self.username_entry.set_text("") self.password_entry.set_text("") def _show_oauth_account(self): self.username_label.set_text("Name") self.password_label.set_text("PIN") self.password_entry.set_visibility(True) self.btn_pin_request.show() def _show_password_account(self): self.username_label.set_text("Username") self.password_label.set_text("Password") self.password_entry.set_visibility(False) self.btn_pin_request.hide() def _get_combo_active_api_name(self): apiiter = self.accounts_combo.get_active_iter() if not apiiter: return None return self.accounts_combo.get_model().get(apiiter, 0)[0] @GtkTemplate.Callback def _on_username_entry_changed(self, entry): if self.current == AccountsView.NEW: self._refresh_btn_new_confirm() elif self.current == AccountsView.EDIT: self._refresh_btn_edit_confirm() @GtkTemplate.Callback def _on_password_entry_changed(self, entry): if self.current == AccountsView.NEW: self._refresh_btn_new_confirm() elif self.current == AccountsView.EDIT: self._refresh_btn_edit_confirm() def _refresh_btn_new_confirm(self): sensitive = (self.adding_allow and self._get_combo_active_api_name() and self.username_entry.get_text().strip() and self.password_entry.get_text()) self.btn_new_confirm.set_sensitive(sensitive) def _refresh_btn_edit_confirm(self): sensitive = (self.password_entry.get_text() and not self.account_edit['password'] == self.password_entry.get_text()) self.btn_edit_confirm.set_sensitive(sensitive) def _add_account(self): username = self.username_entry.get_text().strip() password = self.password_entry.get_text() api = self._get_combo_active_api_name() self.manager.add_account(username, password, api, self.adding_extra) def _edit_account(self): num = self.account_edit['account_id'] username = self.account_edit['username'] password = self.password_entry.get_text() api = self.account_edit['api'] self.manager.edit_account(num, username, password, api)