From ab332d0b998f191f65c3c67dc822d11d5c6cd3aa Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 Jan 2017 12:00:43 +0530 Subject: [PATCH] Work on refactoring the books view --- src/calibre/srv/code.py | 70 ++++++++++++++++++++---------- src/pyj/book_list/cover_grid.pyj | 3 +- src/pyj/book_list/home.pyj | 22 +++++++++- src/pyj/book_list/library_data.pyj | 46 ++++++++++++++++++++ src/pyj/book_list/main.pyj | 16 +++++-- src/pyj/book_list/router.pyj | 39 ++++++++++++++--- src/pyj/book_list/ui.pyj | 46 ++++++++++++++++++-- src/pyj/book_list/views.pyj | 20 ++++++--- src/pyj/utils.pyj | 7 +++ 9 files changed, 224 insertions(+), 45 deletions(-) create mode 100644 src/pyj/book_list/library_data.pyj diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py index 89b72d49df..0701d26b4f 100644 --- a/src/calibre/srv/code.py +++ b/src/calibre/srv/code.py @@ -113,6 +113,52 @@ def update_interface_data(ctx, rd): return basic_interface_data(ctx, rd) +def get_library_init_data(ctx, rd, db, num, sorts, orders): + ans = {} + with db.safe_read_lock: + try: + ans['search_result'] = search_result(ctx, rd, db, rd.query.get('search', ''), num, 0, ','.join(sorts), ','.join(orders)) + except ParseException: + ans['search_result'] = search_result(ctx, rd, db, '', num, 0, ','.join(sorts), ','.join(orders)) + sf = db.field_metadata.ui_sortable_field_keys() + sf.pop('ondevice', None) + ans['sortable_fields'] = sorted((( + sanitize_sort_field_name(db.field_metadata, k), v) for k, v in sf.iteritems()), + key=lambda (field, name):sort_key(name)) + ans['field_metadata'] = db.field_metadata.all_metadata() + mdata = ans['metadata'] = {} + try: + extra_books = set(int(x) for x in rd.query.get('extra_books', '').split(',')) + except Exception: + extra_books = () + for coll in (ans['search_result']['book_ids'], extra_books): + for book_id in coll: + if book_id not in mdata: + data = book_as_json(db, book_id) + if data is not None: + mdata[book_id] = data + return ans + + +@endpoint('/interface-data/books-init', postprocess=json) +def books(ctx, rd): + ''' + Get data to create list of books + + Optional: ?num=50&sort=timestamp.desc&library_id= + &search=''&extra_books='' + ''' + ans = {} + try: + num = int(rd.query.get('num', DEFAULT_NUMBER_OF_BOOKS)) + except Exception: + raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) + library_id, db, sorts, orders = get_basic_query_data(ctx, rd) + ans = get_library_init_data(ctx, rd, db, num, sorts, orders) + ans['library_id'] = library_id + return ans + + @endpoint('/interface-data/init', postprocess=json) def interface_data(ctx, rd): ''' @@ -139,29 +185,7 @@ def interface_data(ctx, rd): num = int(rd.query.get('num', DEFAULT_NUMBER_OF_BOOKS)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) - with db.safe_read_lock: - try: - ans['search_result'] = search_result(ctx, rd, db, rd.query.get('search', ''), num, 0, ','.join(sorts), ','.join(orders)) - except ParseException: - ans['search_result'] = search_result(ctx, rd, db, '', num, 0, ','.join(sorts), ','.join(orders)) - sf = db.field_metadata.ui_sortable_field_keys() - sf.pop('ondevice', None) - ans['sortable_fields'] = sorted((( - sanitize_sort_field_name(db.field_metadata, k), v) for k, v in sf.iteritems()), - key=lambda (field, name):sort_key(name)) - ans['field_metadata'] = db.field_metadata.all_metadata() - mdata = ans['metadata'] = {} - try: - extra_books = set(int(x) for x in rd.query.get('extra_books', '').split(',')) - except Exception: - extra_books = () - for coll in (ans['search_result']['book_ids'], extra_books): - for book_id in coll: - if book_id not in mdata: - data = book_as_json(db, book_id) - if data is not None: - mdata[book_id] = data - + ans.update(get_library_init_data(ctx, rd, db, num, sorts, orders)) return ans diff --git a/src/pyj/book_list/cover_grid.pyj b/src/pyj/book_list/cover_grid.pyj index bce1c19d61..f975ada636 100644 --- a/src/pyj/book_list/cover_grid.pyj +++ b/src/pyj/book_list/cover_grid.pyj @@ -11,7 +11,7 @@ THUMBNAIL_MAX_HEIGHT = 400 BORDER_RADIUS = 10 def cover_grid_css(): - sel = '#' + this + sel = '.' + this ans = build_rule(sel, display='flex', flex_wrap='wrap', justify_content='space-around', align_items='flex-end', align_content='flex-start', user_select='none', overflow='hidden') # Container for an individual cover @@ -65,4 +65,3 @@ def create_item(book_id, interface_data, onclick): def append_item(container, item): first_filler = container.lastChild.querySelector('.cover-grid-filler') container.lastChild.insertBefore(item, first_filler) - diff --git a/src/pyj/book_list/home.pyj b/src/pyj/book_list/home.pyj index 429cf67ed9..6417a357db 100644 --- a/src/pyj/book_list/home.pyj +++ b/src/pyj/book_list/home.pyj @@ -4,10 +4,12 @@ from __python__ import hash_literals, bound_methods from dom import ensure_id from elementmaker import E +from session import get_interface_data +from gettext import gettext as _ from book_list.globals import get_db from book_list.top_bar import create_top_bar -from book_list.ui import set_default_panel_handler +from book_list.ui import set_default_panel_handler, show_panel def show_recent(): @@ -23,9 +25,27 @@ def show_recent(): def init(container_id): create_top_bar(container_id, run_animation=True) container = document.getElementById(container_id) + interface_data = get_interface_data() + + # Recent books recent = E.div(style='display:none') recent_container_id = ensure_id(recent) container.appendChild(recent) window.setTimeout(show_recent.bind(recent_container_id), 5) + # Choose library + cl = E.div( + E.h2(_('Choose the calibre library to browse...')) + ) + container.appendChild(cl) + lids = sorted(interface_data.library_map, key=def(x): return interface_data.library_map[x];) + for library_id in lids: + library_name = interface_data.library_map[library_id] + if library_name: + cl.appendChild(E.div(E.a(library_name, href='javascript: void(0)', title=library_name, data_lid=library_id, onclick=def(ev): + lib_id = ev.currentTarget.dataSet.lid + show_panel('book_list', {'library_id': lib_id}) + ))) + + set_default_panel_handler(init) diff --git a/src/pyj/book_list/library_data.pyj b/src/pyj/book_list/library_data.pyj new file mode 100644 index 0000000000..0ae6d2ba59 --- /dev/null +++ b/src/pyj/book_list/library_data.pyj @@ -0,0 +1,46 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2017, Kovid Goyal +from __python__ import hash_literals, bound_methods + +from ajax import ajax +from session import get_interface_data +from utils import parse_url_params + + +load_status = {'loading':True, 'ok':False, 'error_html':None, 'current_fetch': None} +library_data = {} +current_fetch = None + + +def current_library_id(): + return parse_url_params().library_id or get_interface_data().library_id + + +def update_library_data(data): + load_status.loading = False + load_status.ok = True + load_status.error_html = None + for key in 'search_result sortable_fields field_metadata metadata'.split(' '): + library_data[key] = data[key] + + +def on_data_loaded(end_type, xhr, ev): + load_status.current_fetch = None + if end_type is 'load': + data = JSON.parse(xhr.responseText) + update_library_data(data) + else: + load_status.ok = False + load_status.loading = False + load_status.error_html = xhr.error_html + + +def fetch_init_data(): + if load_status.current_fetch: + load_status.current_fetch.abort() + query = {'library_id': current_library_id()} + url_query = parse_url_params() + for key in url_query: + query[key] = url_query[key] + load_status.current_fetch = ajax('interface-data/books-init', on_data_loaded, query=query) + load_status.current_fetch.send() diff --git a/src/pyj/book_list/main.pyj b/src/pyj/book_list/main.pyj index c36b0dcf6b..88937c6c8c 100644 --- a/src/pyj/book_list/main.pyj +++ b/src/pyj/book_list/main.pyj @@ -13,14 +13,16 @@ from popups import install_event_filters from utils import parse_url_params from book_list.constants import book_list_container_id, read_book_container_id +from book_list.library_data import fetch_init_data from book_list.theme import get_color -from book_list.router import update_window_title, set_default_mode_handler, apply_url, set_mode_handler +from book_list.router import update_window_title, set_default_mode_handler, apply_url, set_mode_handler, on_pop_state from book_list.globals import get_db, set_session_data from book_list.ui import apply_url_state as book_list_mode_handler from read_book.ui import ReadUI # Register the various panels import book_list.home # noqa: unused-import +import book_list.views # noqa: unused-import def remove_initial_progress_bar(): @@ -48,6 +50,10 @@ def init_ui(): install_event_filters() set_default_mode_handler(book_list_mode_handler) window.onerror = onerror + setTimeout(def(): + window.onpopstate = on_pop_state + , 0) # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load + translations = get_translations() if translations: install(translations) @@ -65,6 +71,7 @@ def init_ui(): set_mode_handler('read_book', read_ui.apply_url_state.bind(read_ui)) apply_url() + def on_data_loaded(end_type, xhr, ev): remove_initial_progress_bar() if end_type is 'load': @@ -84,11 +91,13 @@ def on_data_loaded(end_type, xhr, ev): p.innerHTML = xhr.error_html document.body.appendChild(p) + def on_data_load_progress(loaded, total): p = document.querySelector('#page_load_progress > progress') p.max = total p.value = loaded + def load_interface_data(): temp = UserSessionData(None, {}) # So that settings for anonymous users are preserved query = {} @@ -102,7 +111,7 @@ def load_interface_data(): ajax('interface-data/init', on_data_loaded, on_data_load_progress, query=query).send() -def on_update_interface_data(): +def do_update_interface_data(): ajax('interface-data/update', def (end_type, xhr, ev): if end_type is 'load': data = JSON.parse(xhr.responseText) @@ -117,5 +126,6 @@ def main(): else: sd = UserSessionData(interface_data.username, interface_data.user_session_data) set_session_data(sd) - on_update_interface_data() + do_update_interface_data() + fetch_init_data() init_ui() diff --git a/src/pyj/book_list/router.pyj b/src/pyj/book_list/router.pyj index 9ec7d3ae6e..953e6205e5 100644 --- a/src/pyj/book_list/router.pyj +++ b/src/pyj/book_list/router.pyj @@ -2,6 +2,8 @@ # License: GPL v3 Copyright: 2017, Kovid Goyal from __python__ import hash_literals, bound_methods +from ajax import encode_query + from book_list.constants import read_book_container_id, book_list_container_id from book_list.globals import get_current_query from utils import parse_url_params @@ -26,7 +28,7 @@ def update_window_title(subtitle, title='calibre', sep=' :: '): def is_reading_book(): - cq = get_current_query + cq = get_current_query() return cq and cq.mode is read_book_mode @@ -36,17 +38,42 @@ def apply_mode(mode): div.style.display = 'block' if div.id is divid else 'none' -def apply_url(): +def apply_url(ignore_handler): data = parse_url_params() data.mode = data.mode or 'book_list' get_current_query(data) apply_mode() - handler = mode_handlers[data.mode] or default_mode_handler - handler(data) + if not ignore_handler: + handler = mode_handlers[data.mode] or default_mode_handler + handler(data) -def push_state(query, replace=False, mode='book_list'): +history_count = 0 + + +def push_state(query, replace=False, mode='book_list', call_handler=True): + nonlocal history_count query = {k:query[k] for k in query} if mode is not 'book_list': query.mode = mode - # TODO: Implement this (see push_state in boos.pyj) + query = encode_query(query) or '?' + if replace: + window.history.replaceState(None, '', query) + else: + window.history.pushState(None, '', query) + history_count += 1 + apply_url(not call_handler) + + +def on_pop_state(ev): + nonlocal history_count + history_count = max(0, history_count - 1) + apply_url() + + +def back(): + nonlocal history_count + if history_count > 0: + window.back() + else: + push_state({}, replace=True) diff --git a/src/pyj/book_list/ui.pyj b/src/pyj/book_list/ui.pyj index 9a633e839e..6c0c1c873a 100644 --- a/src/pyj/book_list/ui.pyj +++ b/src/pyj/book_list/ui.pyj @@ -7,6 +7,8 @@ from elementmaker import E from book_list.constants import book_list_container_id from book_list.globals import get_current_query +from book_list.router import push_state, back as router_back +from book_list.library_data import current_library_id panel_handlers = {} @@ -38,13 +40,51 @@ def develop_panel(container): set_panel_handler('develop-widgets', develop_panel) +def currently_showing_panel(): + c = document.getElementById(book_list_container_id) + return c.dataset.panel + +def number_of_subpanels(): + c = document.getElementById(book_list_container_id) + return c.lastChild.childNodes.length + + +def show_panel(panel, query_data, replace=False): + if currently_showing_panel() is panel: + return + query = {k:query_data[k] for k in query} + if panel is not 'home': + lid = current_library_id() + if lid: + query.library_id = lid + push_state(query, replace=replace) + + +def close_subpanel(): + c = document.getElementById(book_list_container_id).lastChild + panel_to_remove = c.lastChild + if panel_to_remove: + c.remove(panel_to_remove) + if c.lastChild: + c.lastChild.style.display = 'block' + else: + c.style.display = 'none' + + +def back(): + if number_of_subpanels() > 0: + return close_subpanel() + router_back() + + def apply_url_state(state): panel = state.panel or 'home' - c = document.getElementById(book_list_container_id) - if c.dataset.panel is panel: + if currently_showing_panel() is panel: return + c = document.getElementById(book_list_container_id) clear(c) - c.appendChild(E.div()), c.appendChild(E.div(style='display:none')) + c.appendChild(E.div()) + c.appendChild(E.div(style='display:none')) c.dataset.panel = panel handler = panel_handlers[panel] or default_panel_handler handler(ensure_id(c.firstChild, 'panel')) diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj index 8f2b2d3854..660379996b 100644 --- a/src/pyj/book_list/views.pyj +++ b/src/pyj/book_list/views.pyj @@ -4,25 +4,31 @@ from __python__ import hash_literals import traceback from ajax import ajax_send -from dom import set_css, add_extra_css, unique_id +from dom import set_css, add_extra_css from elementmaker import E from gettext import gettext as _ from modals import error_dialog, ajax_progress_dialog -from book_list.globals import get_session_data, get_boss +from book_list.globals import get_session_data from book_list.cover_grid import cover_grid_css, create_item as create_cover_grid_item, init as init_cover_grid, append_item as cover_grid_append_item +from book_list.top_bar import create_top_bar +from book_list.ui import back, set_panel_handler from widgets import create_button, create_spinner -bv_counter = 0 - -CLASS_NAME = 'books-main-list' -COVER_GRID = unique_id('cover-grid') +COVER_GRID_CLASS = 'book-list-cover-grid' add_extra_css(def(): - ans = cover_grid_css.call(COVER_GRID) + ans = cover_grid_css.call(COVER_GRID_CLASS) return ans ) +def init(container_id): + create_top_bar(container_id, title=_('Books'), action=back, icon='close') + + +set_panel_handler('book_list', init) + + class BooksView: def __init__(self, interface_data, book_list_container): diff --git a/src/pyj/utils.pyj b/src/pyj/utils.pyj index a40ac2de47..3bb8fe40a1 100644 --- a/src/pyj/utils.pyj +++ b/src/pyj/utils.pyj @@ -25,13 +25,18 @@ def debounce(func, wait, immediate=False): func.apply(context, args) def parse_url_params(url=None, allow_multiple=False): + cache = parse_url_params.cache url = url or window.location.href + if cache[url]: + return parse_url_params.cache[url] qs = url.indexOf('?') ans = {} if qs < 0: + cache[url] = ans return ans q = url.slice(qs + 1, ((url.indexOf('#') + 1) or (url.length + 1))) if not q: + cache[url] = ans return ans pairs = q.replace(/\+/g, " ").split("&") for pair in pairs: @@ -43,7 +48,9 @@ def parse_url_params(url=None, allow_multiple=False): ans[key].append(val) else: ans[key] = val + cache[url] = ans return ans +parse_url_params.cache = {} _roman = list(zip( [1000,900,500,400,100,90,50,40,10,9,5,4,1],