Books list render again

This commit is contained in:
Kovid Goyal 2017-02-06 13:32:50 +05:30
parent fd178ed81f
commit 421c99e2f7
5 changed files with 116 additions and 239 deletions

View File

@ -6,12 +6,15 @@ from dom import clear, set_css, build_rule
from elementmaker import E from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from book_list.library_data import cover_url, book_metadata
COVER_GRID_CLASS = 'book-list-cover-grid'
THUMBNAIL_MAX_WIDTH = 300 THUMBNAIL_MAX_WIDTH = 300
THUMBNAIL_MAX_HEIGHT = 400 THUMBNAIL_MAX_HEIGHT = 400
BORDER_RADIUS = 10 BORDER_RADIUS = 10
def cover_grid_css(): def cover_grid_css():
sel = '.' + this sel = '.' + COVER_GRID_CLASS
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') 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 # Container for an individual cover
@ -34,7 +37,7 @@ def cover_grid_css():
def init(container): def init(container):
clear(container) clear(container)
container.appendChild(E.div(id=this)) container.appendChild(E.div(class_=COVER_GRID_CLASS))
for i in range(12): for i in range(12):
container.lastChild.appendChild(E.div(class_='cover-grid-filler')) container.lastChild.appendChild(E.div(class_='cover-grid-filler'))
@ -51,15 +54,14 @@ def on_img_load_error(err):
)) ))
set_css(div, border='dashed 1px currentColor', border_radius=BORDER_RADIUS+'px') set_css(div, border='dashed 1px currentColor', border_radius=BORDER_RADIUS+'px')
def create_item(book_id, interface_data, onclick): def create_item(book_id, show_book_details):
cover_url = 'get/thumb/{}/{}?sz={}x{}'.format(book_id, interface_data['library_id'], Math.ceil(THUMBNAIL_MAX_WIDTH*window.devicePixelRatio), Math.ceil(THUMBNAIL_MAX_HEIGHT*window.devicePixelRatio)) curl = cover_url(book_id, THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT)
metadata = interface_data['metadata'][book_id] metadata = book_metadata(book_id)
alt = _('{} by {}').format(metadata['title'], metadata['authors'].join(' & ')) alt = _('{} by {}').format(metadata.title, metadata.authors.join(' & '))
img = E.img(src=cover_url, alt=alt, title=alt, data_title=metadata['title'], data_authors=metadata['authors'].join(' & ')) img = E.img(src=curl, alt=alt, title=alt, data_title=metadata.title, data_authors=metadata.authors.join(' & '))
img.onerror = on_img_load_error img.onerror = on_img_load_error
ans = E.div(img, data_book_id=str(book_id)) ans = E.div(img, data_book_id=str(book_id), onclick=show_book_details)
ans.addEventListener('click', onclick)
return ans return ans
def append_item(container, item): def append_item(container, item):

View File

@ -7,7 +7,7 @@ from session import get_interface_data
from utils import parse_url_params from utils import parse_url_params
load_status = {'loading':True, 'ok':False, 'error_html':None, 'current_fetch': None} load_status = {'loading':True, 'ok':False, 'error_html':None, 'current_fetch': None, 'library_id': None}
library_data = {} library_data = {}
current_fetch = None current_fetch = None
@ -38,9 +38,24 @@ def on_data_loaded(end_type, xhr, ev):
def fetch_init_data(): def fetch_init_data():
if load_status.current_fetch: if load_status.current_fetch:
load_status.current_fetch.abort() load_status.current_fetch.abort()
query = {'library_id': current_library_id()} load_status.library_id = current_library_id()
query = {'library_id': load_status.library_id}
url_query = parse_url_params() url_query = parse_url_params()
for key in url_query: for key in url_query:
query[key] = url_query[key] query[key] = url_query[key]
load_status.current_fetch = ajax('interface-data/books-init', on_data_loaded, query=query) load_status.current_fetch = ajax('interface-data/books-init', on_data_loaded, query=query)
load_status.current_fetch.send() load_status.current_fetch.send()
def cover_url(book_id, width, height):
return 'get/thumb/{}/{}?sz={}x{}'.format(book_id, current_library_id, Math.ceil(width * window.devicePixelRatio), Math.ceil(height * window.devicePixelRatio))
def book_metadata(book_id):
return library_data.metadata[book_id]
def ensure_current_library_data():
# TODO: Handle search/sort parameters as well
if load_status.library_id != current_library_id():
fetch_init_data()

View File

@ -74,6 +74,6 @@ def on_pop_state(ev):
def back(): def back():
nonlocal history_count nonlocal history_count
if history_count > 0: if history_count > 0:
window.back() window.history.back()
else: else:
push_state({}, replace=True) push_state({}, replace=True)

View File

@ -50,10 +50,9 @@ def number_of_subpanels():
def show_panel(panel, query_data, replace=False): def show_panel(panel, query_data, replace=False):
if currently_showing_panel() is panel: query = {k:query_data[k] for k in query_data}
return
query = {k:query_data[k] for k in query}
if panel is not 'home': if panel is not 'home':
query.panel = panel
lid = current_library_id() lid = current_library_id()
if lid: if lid:
query.library_id = lid query.library_id = lid
@ -79,8 +78,6 @@ def back():
def apply_url_state(state): def apply_url_state(state):
panel = state.panel or 'home' panel = state.panel or 'home'
if currently_showing_panel() is panel:
return
c = document.getElementById(book_list_container_id) c = document.getElementById(book_list_container_id)
clear(c) clear(c)
c.appendChild(E.div()) c.appendChild(E.div())

View File

@ -2,12 +2,9 @@
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import hash_literals from __python__ import hash_literals
import traceback from dom import add_extra_css, clear, ensure_id
from ajax import ajax_send
from dom import set_css, add_extra_css
from elementmaker import E from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from modals import error_dialog, ajax_progress_dialog
from utils import conditional_timeout from utils import conditional_timeout
from session import get_interface_data from session import get_interface_data
@ -15,241 +12,107 @@ 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.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.top_bar import create_top_bar
from book_list.ui import back, set_panel_handler from book_list.ui import back, set_panel_handler
from book_list.library_data import current_library_id, load_status from book_list.library_data import current_library_id, load_status, ensure_current_library_data, library_data
from widgets import create_button, create_spinner
COVER_GRID_CLASS = 'book-list-cover-grid' ALLOWED_MODES = {'cover_grid'}
DEFAULT_MODE = 'cover_grid'
add_extra_css(def(): add_extra_css(def():
ans = cover_grid_css.call(COVER_GRID_CLASS) ans = cover_grid_css.call()
return ans return ans
) )
book_list_data = {
'container_id': None, 'shown_book_ids': set(), 'is_fetching': False, 'mode': None,
}
def clear_grid():
container = document.getElementById(book_list_data.container_id)
# We replace the div entirely so that any styles associated with it are also removed
container.removeChild(container.lastChild.previousSibling)
container.insertBefore(E.div(), container.lastChild)
book_list_data.shown_book_ids = set()
book_list_data.init_grid(container.lastChild.previousSibling)
def show_book_details():
pass
def render_id(book_id):
l = book_list_data.shown_book_ids.length
book_list_data.shown_book_ids.add(book_id)
if l < book_list_data.shown_book_ids.length:
return book_list_data.render_book(book_id, show_book_details.bind(book_id))
def render_ids(book_ids):
book_ids = book_ids or library_data.search_result.book_ids
div = document.getElementById(book_list_data.container_id).lastChild.previousSibling
for book_id in book_ids:
child = render_id(book_id)
if child is not None:
book_list_data.append_item(div, child)
def apply_view_mode(mode=DEFAULT_MODE):
if book_list_data.mode is mode:
return
if mode not in ALLOWED_MODES:
mode = 'cover_grid'
book_list_data.mode = mode
if mode is 'cover_grid':
book_list_data.render_book = create_cover_grid_item
book_list_data.init_grid = init_cover_grid
book_list_data.append_item = cover_grid_append_item
clear_grid()
render_ids()
def create_books_list(container):
book_list_data.container_id = ensure_id(container)
book_list_data.shown_book_ids = set()
book_list_data.is_fetching = False
book_list_data.mode = None
container.appendChild(E.div(style='display:none'))
container.appendChild(E.div()), container.appendChild(E.div())
apply_view_mode(get_session_data().get('view_mode'))
def check_for_books_loaded(): def check_for_books_loaded():
container = this container = this
if load_status.loading: if load_status.loading:
conditional_timeout(container.id, 5, check_for_books_loaded) conditional_timeout(container.id, 5, check_for_books_loaded)
return return
container = container.lastChild
clear(container)
if not load_status.ok:
err = E.div()
err.innerHTML = load_status.error_html
container.appendChild(E.div(
style='margin: 1ex 1em',
E.div(_('Failed to load books from calibre library, with error:')),
err,
E.div(
style='margin-top: 1em; border-top: solid 1px currentColor; padding-top: 1ex;',
E.a(onclick=back, href='javascript: void(0)', style='color: blue', _('Go back to the home page')))
),
)
return
create_books_list(container)
def init(container_id): def init(container_id):
create_top_bar(container_id, title=_('Books'), action=back, icon='close') interface_data = get_interface_data()
ensure_current_library_data()
container = document.getElementById(container_id) container = document.getElementById(container_id)
lid = container.dataset.library_id = current_library_id() lid = container.dataset.library_id = current_library_id()
container.appendChild(E.div(_('Loading books from the {} calibre library, please wait...').format(get_interface_data().library_map[lid]), style='margin: 1ex 1em')) title = interface_data.library_map[lid]
create_top_bar(container_id, title=title, action=back, icon='close')
container.appendChild(E.div())
container.lastChild.appendChild(E.div(_('Loading books from the {} calibre library, please wait...').format(title), style='margin: 1ex 1em'))
conditional_timeout(container_id, 5, check_for_books_loaded) conditional_timeout(container_id, 5, check_for_books_loaded)
set_panel_handler('book_list', init) set_panel_handler('book_list', init)
class BooksView:
def __init__(self, interface_data, book_list_container):
nonlocal bv_counter
bv_counter += 1
self.interface_data = interface_data
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
# on the img itself
div = E.div(
id=self.container_id, style='display:block', class_=CLASS_NAME,
E.div(),
E.div()
)
book_list_container.appendChild(div)
self.set_view_mode(get_session_data().get('view_mode'))
self.create_more_button(div)
def create_more_button(self, div):
more = div.lastChild
more.appendChild(create_button(
_('Show more books'), 'cloud-download', def():self.get_more_books()
))
more.lastChild.setAttribute('rel', 'next')
set_css(more.firstChild, display='block', margin_left='auto', margin_right='auto')
set_css(more, font_size='1.5rem', padding_top='1.5rem', margin_bottom='1.5rem', text_align='center', display='flex')
more.appendChild(E.div(
create_spinner(), '\xa0' + _('Fetching metadata for more books, please wait') + '…',
style='margin-left:auto; margin-right:auto; display:none')
)
self.update_fetching_status()
def set_view_mode(self, mode='cover_grid'):
if self.mode is mode:
return
if mode not in v"['cover_grid']":
mode = 'cover_grid'
self.mode = mode
if mode is 'cover_grid':
self.render_book = create_cover_grid_item
self.init_grid = init_cover_grid.bind(COVER_GRID)
self.append_item = cover_grid_append_item
self.clear()
self.render_ids()
@property
def container(self):
return document.getElementById(self.container_id)
@property
def grid(self):
return self.container.lastChild.previousSibling
@property
def is_visible(self):
self.container.style.display is 'block'
@is_visible.setter
def is_visible(self, val):
self.container.style.display = 'block' if val else 'none'
def clear(self):
# We replace the div entirely so that any styles associated with it are
# also removed
c = self.container
c.removeChild(self.grid)
c.insertBefore(E.div(), c.lastChild)
self.shown_book_ids.clear()
self.init_grid(self.grid)
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, self.interface_data, self.show_book_details.bind(self, 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:
child = self.render_id(book_id)
if child is not None:
self.append_item(div, child)
def update_fetching_status(self):
c = self.container
more = c.lastChild
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.shown_book_ids.length:
more.firstChild.style.display = 'block'
more.lastChild.style.display = 'none'
else:
more.firstChild.style.display = 'none'
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 is '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:
error_dialog(_('Could not get more books'), _('Server returned an invalid response'), traceback.format_exc())
elif end_type is not 'abort':
error_dialog(_('Could not get more books'), xhr.error_html)
def sort_panel_data(self, create_item):
current_sorted_field = self.interface_data.search_result.sort.partition(',')[0]
current_sorted_field_order = self.interface_data.search_result.sort_order.partition(',')[0]
new_sort_order = 'desc' if current_sorted_field_order is 'asc' else 'asc'
if current_sorted_field is '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 is current_sorted_field:
subtitle = _('Reverse current sort order')
icon_name = 'sort-amount-asc' if current_sorted_field_order is '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):
self.abort_get_more_books()
key = 'sort-order-for-' + field
sd = get_session_data()
order = order or sd.get(key, 'asc')
order = 'asc' if order is '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 is '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=traceback.format_exc())
boss.ui.show_panel(boss.ui.ROOT_PANEL)
window.scrollTo(0, 0)
elif end_type is not 'abort':
error_dialog(_('Could not change sort order'), xhr.error_html)
def change_search(self, query, push_state=True, panel_to_show=None):
self.abort_get_more_books()
query = query or ''
sd = get_session_data()
data = {'search':query, 'sort':sd.get_library_option(self.interface_data.library_id, 'sort'), 'library_id':self.interface_data.library_id}
ajax_progress_dialog('interface-data/get-books', self.search_change_completed.bind(self), _(
'Fetching data from server, please wait') + '…', query=data, extra_data_for_callback={'push_state':push_state, 'panel_to_show': panel_to_show})
def search_change_completed(self, end_type, xhr, ev):
if end_type is 'load':
boss = get_boss()
try:
data = JSON.parse(xhr.responseText)
boss.change_books(data)
except Exception as err:
return error_dialog(_('Could not change search query'), err + '', details=traceback.format_exc())
self.update_fetching_status()
ed = xhr.extra_data_for_callback
boss.ui.show_panel(ed.panel_to_show or boss.ui.ROOT_PANEL, push_state=ed.push_state)
window.scrollTo(0, 0)
elif end_type is not 'abort':
msg = xhr.error_html
if xhr.status is 400 and xhr.responseText.startswith('Invalid search expression:'):
msg = _('The search expression could not be parsed: ') + xhr.responseText
error_dialog(_('Could not change search query'), msg)
def refresh(self):
self.clear()
self.render_ids()
def show_book_details(self, book_id):
get_boss().ui.show_panel('book-details', extra_query_data={'book-id':'' + book_id})