diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py index e420e2a010..a1cbf5a6a3 100644 --- a/src/calibre/srv/code.py +++ b/src/calibre/srv/code.py @@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) -import re +import re, httplib from functools import partial from threading import Lock from json import load as load_json_file @@ -116,7 +116,7 @@ def interface_data(ctx, rd): return ans -@endpoint('/interface-data/more-books', postprocess=json, methods={'GET', 'HEAD', 'POST'}) +@endpoint('/interface-data/more-books', postprocess=json, methods={'GET', 'POST', 'HEAD'}, ok_code=httplib.OK) def more_books(ctx, rd): ''' Get more results from the specified search-query, which must @@ -132,14 +132,14 @@ def more_books(ctx, rd): raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) try: search_query = load_json_file(rd.request_body_file) - query, sorts, orders = search_query['query'], search_query['sort'], search_query['sort_order'] + query, offset, sorts, orders = search_query['query'], search_query['offset'], search_query['sort'], search_query['sort_order'] except KeyError as err: raise HTTPBadRequest('Search query missing key: %s' % as_unicode(err)) except Exception as err: raise HTTPBadRequest('Invalid query: %s' % as_unicode(err)) ans = {} with db.safe_read_lock: - ans['search_result'] = search_result(ctx, rd, db, query, num, 0, sorts, orders) + ans['search_result'] = search_result(ctx, rd, db, query, num, offset, sorts, orders) mdata = ans['metadata'] = {} for book_id in ans['search_result']['book_ids']: data = book_as_json(db, book_id) diff --git a/src/pyj/ajax.pyj b/src/pyj/ajax.pyj index cdf857f215..63c9a3f673 100644 --- a/src/pyj/ajax.pyj +++ b/src/pyj/ajax.pyj @@ -1,7 +1,9 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2015, Kovid Goyal -def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None): +from gettext import gettext as _ + +def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None, timeout=30*1000, ok_code=200): query = query or {} xhr = XMLHttpRequest() keys = Object.keys(query) @@ -11,12 +13,22 @@ def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', q val = query[k] if val is undefined or val is None: continue - path += ('&' if has_query else '?') + window.encodeURIComponent(k) + '=' + window.encodeURIComponent(val.toString()) + path += ('&' if has_query else '?') + encodeURIComponent(k) + '=' + encodeURIComponent(val.toString()) has_query = True if bypass_cache: path += ('&' if has_query else '?') + Date().getTime() xhr.request_path = path + xhr.error_string = '' + + def set_error(event): + if event == 'timeout': + xhr.error_string = str.format(_('Failed to download data from "{}", timed out after: {} seconds'), xhr.request_path, timeout/1000) + elif event == 'abort': + xhr.error_string = str.format(_('Failed to download data from "{}", aborted'), xhr.request_path) + else: + rtext = xhr.responseText or '' + xhr.error_string = str.format(_('Failed to download data from "{}", with status: [{}] {}
{}'), xhr.request_path, xhr.status, xhr.statusText, rtext[:200]) def progress_callback(ev): if ev.lengthComputable: @@ -31,19 +43,28 @@ def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', q on_progress(ev.loaded, ul) def complete_callback(end_type, ev): - if end_type != 'load': - on_complete(end_type, xhr, ev) - return - if xhr.status != 200: + if xhr.status != ok_code and end_type == 'load': end_type = 'error' + if end_type != 'load': + set_error(end_type) on_complete(end_type, xhr, ev) if on_progress: xhr.addEventListener('progress', progress_callback) - xhr.addEventListener('abort', def(ev): complete_callback('abort', xhr, ev);) - xhr.addEventListener('error', def(ev): complete_callback('error', xhr, ev);) - xhr.addEventListener('load', def(ev): complete_callback('load', xhr, ev);) + xhr.addEventListener('abort', def(ev): complete_callback('abort', ev);) + xhr.addEventListener('error', def(ev): complete_callback('error', ev);) + xhr.addEventListener('load', def(ev): complete_callback('load', ev);) + xhr.addEventListener('timeout', def(ev): complete_callback('timeout', ev);) xhr.open(method, path) + xhr.timeout = timeout # IE requires timeout to be set after open + return xhr + +def ajax_send(path, data, on_complete, on_progress=None, query=None, timeout=30*1000, ok_code=200): + # Unfortunately, browsers do not allow sending of data with HTTP GET, except + # as query parameters, so we have to use POST + xhr = ajax(path, on_complete, on_progress, False, 'POST', query, timeout, ok_code) + xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') + xhr.send(JSON.stringify(data)) return xhr # TODO: Implement AJAX based switch user by: diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj index 30c0075c81..e1357c3f41 100644 --- a/src/pyj/book_list/views.pyj +++ b/src/pyj/book_list/views.pyj @@ -1,9 +1,11 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2015, Kovid Goyal +from ajax import ajax_send from dom import set_css, build_rule, clear from elementmaker import E from gettext import gettext as _ +from modals import error_dialog from book_list.globals import get_session_data from widgets import create_button @@ -19,8 +21,8 @@ class BooksView: nonlocal bv_counter bv_counter += 1 self.interface_data = interface_data - self.is_fetching = False - self.shown_book_ids = {} + self.is_fetching = None + self.shown_book_ids = set() self.container_id = 'books-view-' + bv_counter # We have to apply the transform on the containing div not the img because of a bug in WebKit # that causes img aspect ratios to be messed up on window resize if the transform is specified @@ -59,7 +61,7 @@ class BooksView: if mode == 'cover_grid': self.render_book = self.cover_grid_item.bind(self) self.init_grid = self.init_cover_grid.bind(self) - self.init_grid() + self.clear() self.render_ids() @property @@ -84,23 +86,30 @@ class BooksView: c = self.container c.removeChild(self.grid) c.insertBefore(E.div(), c.lastChild) + self.shown_book_ids.clear() self.init_grid() - def render_ids(self): - book_ids = self.interface_data['search_result']['book_ids'] - self.shown_book_ids = {} + def render_id(self, book_id): + l = self.shown_book_ids.length + self.shown_book_ids.add(book_id) + if l < self.shown_book_ids.length: + return self.render_book(book_id) + + def render_ids(self, book_ids): + book_ids = book_ids or self.interface_data['search_result']['book_ids'] div = self.grid for book_id in book_ids: - div.appendChild(self.render_book(book_id)) - self.shown_book_ids[book_id] = True + child = self.render_id(book_id) + if child is not None: + div.appendChild(self.render_book(book_id)) def update_fetching_status(self): c = self.container more = c.lastChild - if self.is_fetching: + if self.is_fetching is not None: more.firstChild.style.display = 'none' more.lastChild.style.display = 'block' - elif self.interface_data['search_result']['total_num'] > self.interface_data['search_result']['book_ids'].length: + elif self.interface_data['search_result']['total_num'] > self.shown_book_ids.length: more.firstChild.style.display = 'block' more.lastChild.style.display = 'none' else: @@ -108,8 +117,38 @@ class BooksView: more.lastChild.style.display = 'none' def get_more_books(self): + data = {'offset':self.shown_book_ids.length} + for key in 'query', 'sort', 'sort_order': + data[key] = self.interface_data['search_result'][key] + self.is_fetching = ajax_send('interface-data/more-books', data, self.got_more_books.bind(self), + query={'library_id':self.interface_data.library_id}) self.update_fetching_status() + def abort_get_more_books(self): + if self.is_fetching: + a, self.is_fetching = self.is_fetching, None + a.abort() + self.update_fetching_status() + + def got_more_books(self, end_type, xhr, event): + if self.is_fetching is None or self.is_fetching is not xhr: + return # Fetching was aborted + self.is_fetching = None + self.update_fetching_status() + if end_type == 'load': + try: + data = JSON.parse(xhr.responseText) + for key in data.metadata: + self.interface_data.metadata[key] = data.metadata[key] + if not data.search_result.book_ids: + raise Exception('No books ids object in search result from server') + self.render_ids(data.search_result.book_ids) + self.interface_data.search_result = data.search_result + except Exception as err: + error_dialog(_('Could not get more books'), _('Server returned an invalid response'), err.stack or err.toString()) + elif end_type != 'abort': + error_dialog(_('Could not get more books'), xhr.error_string) + # Cover grid {{{ def init_cover_grid(self): diff --git a/src/pyj/srv.pyj b/src/pyj/srv.pyj index b1645702f4..16f05cc956 100644 --- a/src/pyj/srv.pyj +++ b/src/pyj/srv.pyj @@ -3,7 +3,6 @@ from ajax import ajax from elementmaker import E -from gettext import gettext as _ from session import UserSessionData, SessionData from book_list.boss import Boss from book_list.globals import set_boss, set_session_data @@ -19,9 +18,7 @@ def on_library_loaded(end_type, xhr, ev): boss = Boss(interface_data) set_boss(boss) else: - document.body.appendChild(E.p(style='color:red', str.format(_( - 'Failed to download library data from "{}", with status: [{}] {}'), - xhr.request_path, xhr.status, xhr.statusText))) + document.body.appendChild(E.p(style='color:red', xhr.error_string)) def on_library_load_progress(loaded, total): p = document.querySelector('#page_load_progress > progress')