From 0d03dd915158cdd960cd7d0e1337698254498a0f Mon Sep 17 00:00:00 2001 From: Mubashshir Date: Fri, 21 Apr 2023 15:49:32 +0600 Subject: [PATCH 1/3] engine: Add pagination support skeleton Signed-off-by: Mubashshir --- trackma/data.py | 6 +++--- trackma/engine.py | 4 ++-- trackma/lib/lib.py | 2 +- trackma/lib/libanilist.py | 4 ++-- trackma/lib/libkitsu.py | 4 ++-- trackma/lib/libmal.py | 4 ++-- trackma/lib/libshikimori.py | 6 +++--- trackma/lib/libvndb.py | 6 +++--- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/trackma/data.py b/trackma/data.py index 72045431..18d67b3a 100644 --- a/trackma/data.py +++ b/trackma/data.py @@ -213,12 +213,12 @@ def get(self): """Get list from memory""" return self.showlist - def search(self, criteria, method): + def search(self, criteria, method, page): # Tell API to search - results = self.api.search(criteria, method) + results = self.api.search(criteria, method, page or 1) self.api.logout() if results: - return results + return results if page else results[0] raise utils.DataError('No results.') diff --git a/trackma/engine.py b/trackma/engine.py index e916fcaa..23120ec0 100644 --- a/trackma/engine.py +++ b/trackma/engine.py @@ -481,7 +481,7 @@ def tracker_status(self): return None - def search(self, criteria, method=utils.SearchMethod.KW): + def search(self, criteria, method=utils.SearchMethod.KW, page=None): """ Request a remote list of shows matching the criteria and returns it as a list of show dictionaries. @@ -491,7 +491,7 @@ def search(self, criteria, method=utils.SearchMethod.KW): raise utils.EngineError( 'Search method not supported by API or mediatype.') - return self.data_handler.search(criteria, method) + return self.data_handler.search(criteria, method, page) def add_show(self, show, status=None): """ diff --git a/trackma/lib/lib.py b/trackma/lib/lib.py index af7d76c6..1ed81fb6 100644 --- a/trackma/lib/lib.py +++ b/trackma/lib/lib.py @@ -138,7 +138,7 @@ def delete_show(self, item): """ raise NotImplementedError - def search(self, criteria, method): + def search(self, criteria, method, page): """ Called when the data handler needs a detailed list of shows from the remote server. It should return a list of show dictionaries with the additional 'extra' key (which is a list of tuples) diff --git a/trackma/lib/libanilist.py b/trackma/lib/libanilist.py index ed5bb87f..8331e737 100644 --- a/trackma/lib/libanilist.py +++ b/trackma/lib/libanilist.py @@ -374,7 +374,7 @@ def delete_show(self, item): variables = {'id': item['my_id']} self._request(query, variables) - def search(self, criteria, method): + def search(self, criteria, method, page): self.check_credentials() self.msg.info("Searching for {}...".format(criteria)) @@ -415,7 +415,7 @@ def search(self, criteria, method): infolist.append(self._parse_info(media)) self._emit_signal('show_info_changed', infolist) - return infolist + return (infolist, len(infolist), 1, 1) def request_info(self, itemlist): self.check_credentials() diff --git a/trackma/lib/libkitsu.py b/trackma/lib/libkitsu.py index 430d690d..ad683f8e 100644 --- a/trackma/lib/libkitsu.py +++ b/trackma/lib/libkitsu.py @@ -452,7 +452,7 @@ def delete_show(self, item): except urllib.error.URLError as e: raise utils.APIError('Error deleting: ' + str(e.reason)) - def search(self, query, method): + def search(self, query, method, page): self.msg.info("Searching for %s..." % query) values = { @@ -475,7 +475,7 @@ def search(self, query, method): if not infolist: raise utils.APIError('No results.') - return infolist + return (infolist, len(infolist), 1, 1,) except urllib.error.HTTPError as e: raise utils.APIError('Error searching: ' + str(e.code)) except urllib.error.URLError as e: diff --git a/trackma/lib/libmal.py b/trackma/lib/libmal.py index 887ad002..b048a869 100644 --- a/trackma/lib/libmal.py +++ b/trackma/lib/libmal.py @@ -308,7 +308,7 @@ def delete_show(self, item): self.msg.info("Deleting item %s..." % item['title']) data = self._request('DELETE', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), auth=True) - def search(self, criteria, method): + def search(self, criteria, method, page): self.check_credentials() self.msg.info("Searching for {}...".format(criteria)) @@ -333,7 +333,7 @@ def search(self, criteria, method): results.append(self._parse_info(item['node'])) self._emit_signal('show_info_changed', results) - return results + return (results, len(results), 1, 1) def request_info(self, itemlist): self.check_credentials() diff --git a/trackma/lib/libshikimori.py b/trackma/lib/libshikimori.py index f2a148ee..dc9e9d00 100644 --- a/trackma/lib/libshikimori.py +++ b/trackma/lib/libshikimori.py @@ -282,7 +282,7 @@ def delete_show(self, item): data = self._request( "DELETE", self.api_url + "/user_rates/{}".format(item['my_id']), auth=True) - def search(self, criteria, method): + def search(self, criteria, method, page): self.check_credentials() self.msg.info("Searching for {}...".format(criteria)) @@ -293,7 +293,7 @@ def search(self, criteria, method): except ValueError: # An empty document, without any JSON, is returned # when there are no results. - return [] + return ([], 0, 0, 0) showlist = [] @@ -314,7 +314,7 @@ def search(self, criteria, method): showlist.append(show) - return showlist + return (showlist, len(showlist), 1, 1) def request_info(self, itemlist): self.check_credentials() diff --git a/trackma/lib/libvndb.py b/trackma/lib/libvndb.py index e30bca0e..27cc640a 100644 --- a/trackma/lib/libvndb.py +++ b/trackma/lib/libvndb.py @@ -321,14 +321,14 @@ def delete_show(self, item): if name != 'ok': raise utils.APIError("Invalid response (%s)" % name) - def search(self, criteria, method): + def search(self, criteria, method, page): self.check_credentials() results = list() self.msg.info('Searching for %s...' % criteria) (name, data) = self._sendcmd('get vn basic,details (search ~ "%s")' % criteria, - {'page': 1, + {'page': page, 'results': self.pagesize_details, }) @@ -345,7 +345,7 @@ def search(self, criteria, method): if not results: raise utils.APIError('No results.') - return results + return (results, len(results), 1, 1) def logout(self): self.msg.info('Disconnecting...') From ed57bdb2b0b7bb30e7afc5198d43260a96df601c Mon Sep 17 00:00:00 2001 From: Mubashshir Date: Fri, 21 Apr 2023 16:06:07 +0600 Subject: [PATCH 2/3] kitsu: Implement paginated search Signed-off-by: Mubashshir --- trackma/lib/libkitsu.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/trackma/lib/libkitsu.py b/trackma/lib/libkitsu.py index ad683f8e..ac3e2026 100644 --- a/trackma/lib/libkitsu.py +++ b/trackma/lib/libkitsu.py @@ -454,16 +454,20 @@ def delete_show(self, item): def search(self, query, method, page): self.msg.info("Searching for %s..." % query) + item_per_page = 20 values = { "filter[text]": query, - "page[limit]": 20, + "page[limit]": item_per_page, + "page[offset]": (page - 1) * item_per_page } try: data = self._request('GET', self.prefix + "/" + self.mediatype, get=values) shows = json.loads(data) + pages = int(shows['meta']['count'] / item_per_page) + \ + 1 if shows['meta']['count'] % item_per_page != 0 else 0 infolist = [] for media in shows['data']: @@ -475,7 +479,7 @@ def search(self, query, method, page): if not infolist: raise utils.APIError('No results.') - return (infolist, len(infolist), 1, 1,) + return (infolist, shows['meta']['count'], page, pages,) except urllib.error.HTTPError as e: raise utils.APIError('Error searching: ' + str(e.code)) except urllib.error.URLError as e: From ad9d6c2a81f45965917e54214d9d698cd4a6929b Mon Sep 17 00:00:00 2001 From: Mubashshir Date: Fri, 21 Apr 2023 16:42:06 +0600 Subject: [PATCH 3/3] Gtk: Implement paginated search Signed-off-by: Mubashshir --- trackma/ui/gtk/data/searchwindow.ui | 203 ++++++++++++++++++++++------ trackma/ui/gtk/mainview.py | 1 - trackma/ui/gtk/searchwindow.py | 101 +++++++++++--- 3 files changed, 245 insertions(+), 60 deletions(-) diff --git a/trackma/ui/gtk/data/searchwindow.ui b/trackma/ui/gtk/data/searchwindow.ui index 2231eb40..8e2c5d3a 100644 --- a/trackma/ui/gtk/data/searchwindow.ui +++ b/trackma/ui/gtk/data/searchwindow.ui @@ -3,56 +3,32 @@ + + False + + + True + False + 8 + 8 + 8 + 8 + 6 + + + True + True + + + + False + True + 0 + + + + + True + True + True + + + + True + False + object-select-symbolic + + + + + + False + True + 1 + + + + + diff --git a/trackma/ui/gtk/mainview.py b/trackma/ui/gtk/mainview.py index 81441b0a..3e3e6a8d 100644 --- a/trackma/ui/gtk/mainview.py +++ b/trackma/ui/gtk/mainview.py @@ -534,7 +534,6 @@ def _on_show_action(self, page, event_type, data): self.emit('show-action', event_type, data) def get_current_status(self): - print(self._engine.mediainfo['statuses']) return self._current_page.status if self._current_page.status is not None else self._engine.mediainfo['statuses'][-1] def get_selected_show(self): diff --git a/trackma/ui/gtk/searchwindow.py b/trackma/ui/gtk/searchwindow.py index a7c81e98..d6aea4d1 100644 --- a/trackma/ui/gtk/searchwindow.py +++ b/trackma/ui/gtk/searchwindow.py @@ -25,18 +25,21 @@ class SearchThread(threading.Thread): - def __init__(self, engine, search_text, callback): + def __init__(self, engine, query, callback): threading.Thread.__init__(self) self._entries = [] self._error = None self._engine = engine - self._search_text = search_text + if isinstance(query, (tuple,)): + self._search_text, self._page = query + else: + self._search_text, self._page = (query, 1) self._callback = callback self._stop_request = threading.Event() def run(self): try: - self._entries = self._engine.search(self._search_text) + self._entries = self._engine.search(self._search_text, page=self._page) except utils.TrackmaError as e: self._entries = [] self._error = e @@ -63,11 +66,23 @@ class SearchWindow(Gtk.Window): show_info_container = Gtk.Template.Child() progress_spinner = Gtk.Template.Child() headerbar = Gtk.Template.Child() + search_entry = Gtk.Template.Child() + + # pagination + paginator = Gtk.Template.Child() + next_page = Gtk.Template.Child() + prev_page = Gtk.Template.Child() + select_page = Gtk.Template.Child() + page_popover = Gtk.Template.Child() + page_entry = Gtk.Template.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._pages = 1 + self._page = 1 + self._results = 0 self._selected_show = None self._showdict = None @@ -91,46 +106,58 @@ def __init__(self, engine, colors, current_status, transient_for=None): @Gtk.Template.Callback() def _on_search_entry_search_changed(self, search_entry): - search_text = search_entry.get_text().strip() + self._search(1) + + def _search(self, page): + text = self.search_entry.get_text().strip() self.progress_spinner.start() - if search_text == "": + + if text == "": if self._search_thread: self._search_thread.stop() self._search_finish() - else: - self._search(search_text) - self.progress_spinner.start() + return - def _search(self, text): if self._search_thread: self._search_thread.stop() self.headerbar.set_subtitle("Searching: \"%s\"" % text) self._search_thread = SearchThread(self._engine, - text, + (text, page), self._search_finish_idle) self._search_thread.start() def _search_finish(self): self.headerbar.set_subtitle( - "%s result%s." % ((len(self._entries), 's') - if len(self._entries) > 0 - else ('No', '') - ) + "%s result%s." % ((self._results, 's') if self._results > 0 + else ('No', '')) ) self.progress_spinner.stop() def _search_finish_idle(self, entries, error): - self._entries = entries + if isinstance(entries, (tuple,)): + self._entries, self._results, self._page, self._pages = entries + else: + self._entries, self._results, self._page, self._pages = ( + entries, len(entries), 1, 1 + ) + self._showdict = dict() self._search_finish() self.showlist.append_start() - for show in entries: + for show in self._entries: self._showdict[show['id']] = show self.showlist.append(show) self.showlist.append_finish() self.btn_add_show.set_sensitive(False) + self.paginator.props.visible = self._pages > 1 + self.select_page.props.sensitive = self._pages > 1 + + if self.paginator.props.visible: + self.prev_page.props.sensitive = self._page > 1 + self.next_page.props.sensitive = self._page < self._pages + self.select_page.props.label = '{} / {}'.format(self._page, self._pages) if error: self.emit('search-error', error) @@ -142,6 +169,48 @@ def _on_btn_add_show_clicked(self, btn): if show is not None: self._add_show(show) + @Gtk.Template.Callback() + def _on_prev_page_clicked(self, btn): + self._search(self._page - 1) + + @Gtk.Template.Callback() + def _on_next_page_clicked(self, btn): + self._search(self._page + 1) + + @Gtk.Template.Callback() + def _on_select_page_clicked(self, btn): + popover = self.page_popover.props + entry = self.page_entry.props + popover.relative_to = btn + popover.position = Gtk.PositionType.BOTTOM + + entry.text = str(self._page) + entry.placeholder_text = "1 <= page <= {}".format(self._pages) + (entry.secondary_icon_tooltip_text, + entry.secondary_icon_name) = ('', '') + + self.page_popover.show() + self.page_entry.grab_focus() + + @Gtk.Template.Callback() + def _on_page_change(self, *args): + props = self.page_entry.props + + if not props.text.isdigit(): + props.secondary_icon_name = 'dialog-error' + props.secondary_icon_tooltip_text = 'Not a number.' + return False + elif not 1 <= int(self.page_entry.props.text) <= self._pages: + props.secondary_icon_name = 'dialog-error' + props.secondary_icon_tooltip_text = 'Not in range 1 <= page <= {}'.format(self._pages) + return False + elif props.secondary_icon_name: + props.secondary_icon_name = '' + props.secondary_icon_tooltip_text = '' + + self.page_popover.hide() + self._search(int(props.text)) + def _get_full_selected_show(self): for item in self._entries: if item['id'] == self._selected_show: