Work on refactoring the books view

This commit is contained in:
Kovid Goyal 2017-01-31 12:00:43 +05:30
parent 8cb4545b96
commit ab332d0b99
9 changed files with 224 additions and 45 deletions

View File

@ -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=<default library>
&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

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,46 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
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()

View File

@ -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()

View File

@ -2,6 +2,8 @@
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@ -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'))

View File

@ -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):

View File

@ -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],