diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py index 3a674ea568..73503fe407 100644 --- a/src/calibre/srv/code.py +++ b/src/calibre/srv/code.py @@ -16,6 +16,7 @@ from calibre.srv.errors import HTTPNotFound, HTTPBadRequest from calibre.srv.metadata import book_as_json from calibre.srv.routes import endpoint, json from calibre.utils.icu import sort_key +from calibre.utils.search_query_parser import ParseException html_cache = {} cache_lock = Lock() @@ -67,7 +68,9 @@ def get_basic_query_data(ctx, query): sorts, orders = [], [] for x in query.get('sort', '').split(','): if x: - s, o = x.partition('.')[::2] + s, o = x.rpartition('.')[::2] + if o and not s: + s, o = o, '' if o not in ('asc', 'desc'): o = 'asc' if s.startswith('_'): @@ -170,3 +173,29 @@ def set_session_data(ctx, rd): ud = ctx.user_manager.get_session_data(rd.username) ud.update(new_data) ctx.user_manager.set_session_data(rd.username, ud) + +@endpoint('/interface-data/get-books', postprocess=json) +def get_books(ctx, rd): + ''' + Get books for the specified query + + Optional: ?library_id=&num=50&sort=timestamp.desc&search='' + ''' + library_id, db, sorts, orders = get_basic_query_data(ctx, rd.query) + try: + num = int(rd.query.get('num', DEFAULT_NUMBER_OF_BOOKS)) + except Exception: + raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) + searchq = rd.query.get('search', '') + db = get_library_data(ctx, rd.query)[0] + ans = {} + mdata = ans['metadata'] = {} + with db.safe_read_lock: + try: + ans['search_result'] = search_result(ctx, rd, db, searchq, num, 0, ','.join(sorts), ','.join(orders)) + except ParseException as err: + raise HTTPBadRequest('Invalid search expression: %s' % as_unicode(err)) + for book_id in ans['search_result']['book_ids']: + data = book_as_json(db, book_id) + mdata[book_id] = data + return ans diff --git a/src/pyj/book_list/boss.pyj b/src/pyj/book_list/boss.pyj index c798f326cd..bb844b52a2 100644 --- a/src/pyj/book_list/boss.pyj +++ b/src/pyj/book_list/boss.pyj @@ -4,6 +4,7 @@ from book_list.ui import UI from modals import error_dialog from gettext import gettext as _ +from book_list.globals import get_session_data class Boss: @@ -29,3 +30,14 @@ class Boss: return True except: console.error('There was an error in the unhandled exception handler') + + def change_books(self, data): + data.search_result.sort = str.split(data.search_result.sort, ',')[:2].join(',') + data.search_result.sort_order = str.split(data.search_result.sort_order, ',')[:2].join(',') + sval = '' + for field, order in zip(str.split(data.search_result.sort, ','), str.split(data.search_result.sort_order, ',')): + sval += field + '.' + order + ',' + get_session_data().set('sort', str.rstrip(sval, ',')) + self.interface_data.metadata = data.metadata + self.interface_data.search_result = data.search_result + self.ui.books_view.refresh() diff --git a/src/pyj/book_list/ui.pyj b/src/pyj/book_list/ui.pyj index d265459a1c..85b949372e 100644 --- a/src/pyj/book_list/ui.pyj +++ b/src/pyj/book_list/ui.pyj @@ -30,10 +30,11 @@ class ClosePanelBar(BarState): class UIState: - def __init__(self, top_bar_state=None, main_panel=None, panel_data=None): + def __init__(self, top_bar_state=None, main_panel=None, panel_data=None, is_cacheable=True): self.top_bar_state = top_bar_state self.main_panel = main_panel or get_boss().ui.items_view self.panel_data = panel_data + self.is_cacheable = is_cacheable def add_button(self, **kw): self.top_bar_state.add_button(**kw) @@ -43,7 +44,9 @@ panels = {} def panel(key): ans = panels[key] if not ans: - ans = panels[key] = create_panel[key]() + ans = create_panel[key]() + if ans.is_cacheable: + panels[key] = ans return ans def close_panel(): @@ -65,8 +68,12 @@ create_panel = { , 'booklist-mode-menu': def booklist_mode_menu(): - return UIState(ClosePanelBar(_('Book List Mode')), panel_data=[ - ]) + return UIState(ClosePanelBar(_('Book List Mode')), panel_data=[]) + , + + 'booklist-sort-menu': def change_booklist_sort(): + data = get_boss().ui.books_view.sort_panel_data(create_item) + return UIState(ClosePanelBar(_('Sort books')), panel_data=data, is_cacheable=False) , } @@ -82,7 +89,7 @@ class UI: self.books_view = BooksView(interface_data) self.items_view = ItemsView(interface_data) ibs = BarState(run_animation=True) - ibs.add_button(icon_name='sort-alpha-asc', tooltip=_('Sort books')) + ibs.add_button(icon_name='sort-amount-desc', tooltip=_('Sort books'), action=show_panel_action('booklist-sort-menu')) ibs.add_button(icon_name='search', tooltip=_('Search for books')) ibs.add_button(icon_name='ellipsis-v', tooltip=_('More actions'), action=show_panel_action('more-actions-menu')) self.states.append(UIState(ibs, self.books_view)) diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj index b44a4f5677..f96af2a57c 100644 --- a/src/pyj/book_list/views.pyj +++ b/src/pyj/book_list/views.pyj @@ -5,9 +5,9 @@ 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 modals import error_dialog, ajax_progress_dialog -from book_list.globals import get_session_data +from book_list.globals import get_session_data, get_boss from widgets import create_button, create_spinner THUMBNAIL_MAX_WIDTH = 300 @@ -148,7 +148,7 @@ class BooksView: 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) + error_dialog(_('Could not get more books'), xhr.error_html) # Cover grid {{{ @@ -186,3 +186,51 @@ class BooksView: ) # }}} + + def sort_panel_data(self, create_item): + current_sorted_field = str.partition(self.interface_data.search_result.sort, ',')[0] + current_sorted_field_order = str.partition(self.interface_data.search_result.sort_order, ',')[0] + new_sort_order = 'desc' if current_sorted_field_order == 'asc' else 'asc' + if current_sorted_field == 'date': + current_sorted_field = 'timestamp' + ans = [] + ans.subtitle = _('Change how the list of books is sorted') + for field, name in self.interface_data.sortable_fields: + subtitle = icon_name = None + if field == current_sorted_field: + subtitle = _('Reverse current sort order') + icon_name = 'sort-amount-asc' if current_sorted_field_order == 'asc' else 'sort-amount-desc' + action = self.change_sort.bind(self, field, new_sort_order) + else: + action = self.change_sort.bind(self, field, None) + ans.push(create_item(name, subtitle=subtitle, icon_name=icon_name, action=action)) + return ans + + def change_sort(self, field, order): + key = 'sort-order-for-' + field + sd = get_session_data() + order = order or sd.get(key, 'asc') + order = 'asc' if order == 'asc' else 'desc' + sd.set(key, order) + sr = self.interface_data.search_result + sort = field + '.' + order + ',' + sr.sort + '.' + sr.order + data = {'search':sr.query or '', 'sort':sort, 'num':self.shown_book_ids.length, 'library_id':self.interface_data.library_id} + ajax_progress_dialog('interface-data/get-books', self.sort_change_completed.bind(self), _( + 'Fetching data from server, please wait') + '…', query=data) + + def sort_change_completed(self, end_type, xhr, ev): + if end_type == 'load': + boss = get_boss() + try: + data = JSON.parse(xhr.responseText) + boss.change_books(data) + except Exception as err: + return error_dialog(_('Could not change sort order'), err + '', details=err.stack) + boss.ui.close_panel() + window.scrollTo(0, 0) + elif end_type != 'abort': + error_dialog(_('Could not change sort order'), xhr.error_html) + + def refresh(self): + self.clear() + self.render_ids()