예제 #1
0
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)
예제 #2
0
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']
예제 #3
0
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)
예제 #4
0
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)
예제 #5
0
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)
예제 #6
0
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)
예제 #7
0
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()
예제 #8
0
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)