Start work on book details panel

This commit is contained in:
Kovid Goyal 2016-02-13 14:41:56 +05:30
parent 14df859c63
commit a3f1ee233d
7 changed files with 173 additions and 24 deletions

View File

@ -216,6 +216,19 @@ def get_books(ctx, rd):
mdata[book_id] = data
return ans
@endpoint('/interface-data/book-metadata/{book_id}', postprocess=json, types={'book_id': int})
def book_metadata(ctx, rd, book_id):
'''
Get metadata for the specified book
Optional: ?library_id=<default library>
'''
library_id, db = get_basic_query_data(ctx, rd.query)[:2]
data = book_as_json(db, book_id)
if data is None:
raise HTTPNotFound('No book with id: %d in library' % book_id)
return data
@endpoint('/interface-data/tag-browser')
def tag_browser(ctx, rd):
'''

View File

@ -135,7 +135,7 @@ class Route(object):
if argspec.args[2:len(self.names)+2] != self.names:
raise route_error('Function\'s argument names do not match the variable names in the route')
if not frozenset(self.type_checkers).issubset(frozenset(self.names)):
raise route_error('There exist type checkers that do not correspond to route variables')
raise route_error('There exist type checkers that do not correspond to route variables: %r' % (set(self.type_checkers) - set(self.names)))
self.min_size = found_optional_part if found_optional_part is not False else len(matchers)
self.max_size = sys.maxsize if self.soak_up_extra else len(matchers)

View File

@ -0,0 +1,111 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from ajax import ajax
from book_list.globals import get_current_query
from dom import clear
from elementmaker import E
from gettext import gettext as _
from book_list.globals import get_boss
from modals import error_dialog
from widgets import create_spinner
bd_counter = 0
class BookDetailsPanel:
def __init__(self, interface_data, book_list_container):
nonlocal bd_counter
bd_counter += 1
self.container_id = 'book-details-panel-' + bd_counter
style = ''
div = E.div(
id=self.container_id, style='display:none',
E.style(style, type='text/css')
)
book_list_container.appendChild(div)
self.interface_data = interface_data
@property
def container(self):
return document.getElementById(self.container_id)
@property
def is_visible(self):
self.container.style.display == 'block'
@is_visible.setter
def is_visible(self, val):
self.container.style.display = 'block' if val else 'none'
def init(self, data):
c = self.container
clear(c)
book_id = get_current_query()['book-id']
if book_id is undefined or book_id is None:
self.no_book()
elif book_id in self.interface_data.metadata:
self.render_book(book_id)
else:
self.fetch_metadata(book_id)
def no_book(self, book_id):
self.container.appendChild(E.div(
style='margin: 1ex 1em',
_('No book found')
))
def fetch_metadata(self, book_id):
if self.is_fetching:
self.is_fetching.abort()
def fetched(end_type, xhr, ev):
self.metadata_fetched(book_id, end_type, xhr, ev)
self.is_fetching = ajax('interface-data/book-metadata/' + book_id, fetched,
query={'library_id':self.interface_data.library_id})
self.container.appendChild(E.div(
style='margin: 1ex 1em',
create_spinner(), '\xa0' + _('Fetching metadata for the book, please wait') + '…',
))
def metadata_fetched(self, book_id, 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
c = self.container
if end_type == 'load':
try:
data = JSON.parse(xhr.responseText)
except Exception as err:
error_dialog(_('Could not fetch metadata for book'), _('Server returned an invalid response'), err.stack or err.toString())
return
clear(c)
self.interface_data.metadata[book_id] = data
self.render_book(book_id)
elif end_type != 'abort':
clear(c)
c.appendChild(E.div(
style='margin: 1ex 1em',
_('Could not fetch metadata for book'),
E.div(style='margin: 1ex 1em')
))
c.lastChild.lastChild.innerHTML = xhr.error_html
def render_book(self, book_id):
metadata = self.interface_data.metadata[book_id]
get_boss().ui.set_title(metadata.title)
cover_url = str.format('get/cover/{}/{}', book_id, self.interface_data['library_id'])
alt = str.format(_('{} by {}'), metadata['title'], metadata['authors'].join(' & '))
img = E.img(
src=cover_url, alt=alt, title=alt, data_title=metadata['title'], data_authors=metadata['authors'].join(' & '),
style='margin-left: 1em; max-width: 45vw; max-height: 95vh; display: block; width:auto; height:auto'
)
img.onerror = self.on_img_err.bind(self)
c = self.container
c.appendChild(E.div(
E.div(),
img
))
def on_img_err(self, err):
img = err.target
img.style.display = 'none'

View File

@ -82,8 +82,11 @@ class Boss:
self.interface_data.search_result = data.search_result
self.ui.refresh_books_view()
def push_state(self, replace=False):
def push_state(self, replace=False, extra_query_data=None):
query = {}
if extra_query_data:
for k in extra_query_data:
query[k] = extra_query_data[k]
if self.current_mode == 'book_list':
if self.ui.current_panel != self.ui.ROOT_PANEL:
query.panel = self.ui.current_panel
@ -95,8 +98,8 @@ class Boss:
sq = idata.search_result.query
if sq:
query.search = sq
query = encode_query(query) or '?'
set_current_query(query)
query = encode_query(query) or '?'
if replace:
window.history.replaceState(None, '', query)
else:

View File

@ -114,3 +114,7 @@ class TopBar:
clear(right)
for button in buttons:
self.add_button(**button)
def set_title(self, text):
for bar in self.bar, self.dummy_bar:
bar.firstChild.firstChild.nextSibling.textContent = text

View File

@ -7,6 +7,7 @@ from book_list.top_bar import TopBar
from book_list.views import BooksView
from book_list.item_list import ItemsView, create_item
from book_list.prefs import PrefsPanel
from book_list.book_details import BookDetailsPanel
from gettext import gettext as _
from utils import debounce
@ -76,7 +77,8 @@ class UI:
self.items_view = ItemsView(interface_data, book_list_container)
self.prefs_panel = PrefsPanel(interface_data, book_list_container)
self.search_panel = SearchPanel(interface_data, book_list_container)
self.panels = [self.books_view, self.items_view, self.search_panel, self.prefs_panel]
self.book_details_panel = BookDetailsPanel(interface_data, book_list_container)
self.panels = [self.books_view, self.items_view, self.search_panel, self.prefs_panel, self.book_details_panel]
self.panel_map = {self.ROOT_PANEL: UIState(create_book_view_top_bar_state(self.books_view), main_panel=self.books_view)}
self.current_panel = self.ROOT_PANEL
window.addEventListener('resize', debounce(self.on_resize.bind(self), 250))
@ -98,11 +100,16 @@ class UI:
bss.add_button(icon_name='cogs', tooltip=_('Configure Tag Browser'), action=show_panel_action('booklist-config-tb'))
self.panel_map['booklist-search'] = UIState(bss, main_panel=self.search_panel)
self.panel_map['book-details'] = UIState(ClosePanelBar(_('Book Details')), main_panel=self.book_details_panel)
def create_prefences_panel(self, title, close_callback=None, panel_data=None):
ans = UIState(ClosePanelBar(title), close_callback=close_callback, main_panel=self.prefs_panel, panel_data=panel_data)
ans.add_button(icon_name='refresh', tooltip=_('Restore default settings'), action=self.prefs_panel.reset_to_defaults.bind(self.prefs_panel))
return ans
def set_title(self, text):
self.top_bar.set_title(text)
def on_resize(self):
pass
@ -126,16 +133,20 @@ class UI:
self.show_panel(self.ROOT_PANEL)
def replace_panel(self, panel_name, force=False):
get_boss().push_state(replace=True)
if force or panel_name != self.current_panel:
action_needed = force or panel_name != self.current_panel
if action_needed:
self.current_panel = panel_name or self.ROOT_PANEL
get_boss().push_state(replace=True)
if action_needed:
self.apply_state()
def show_panel(self, panel_name, push_state=True, force=False):
if push_state:
get_boss().push_state()
if force or panel_name != self.current_panel:
def show_panel(self, panel_name, push_state=True, force=False, extra_query_data=None):
action_needed = force or panel_name != self.current_panel
if action_needed:
self.current_panel = panel_name or self.ROOT_PANEL
if push_state:
get_boss().push_state(extra_query_data=extra_query_data)
if action_needed:
self.apply_state()
def refresh_books_view(self):

View File

@ -157,33 +157,37 @@ class BooksView:
set_css(div, display='flex', flex_wrap='wrap', justify_content='flex-start', align_items='flex-end', align_content='flex-start', user_select='none')
div.setAttribute('class', 'cover_grid')
def on_cover_grid_img_err(self, err):
img = err.target
div = img.parentNode
if not div:
return
clear(div)
div.appendChild(E.div(
style='position:relative; top:-50%; transform: translateY(50%)',
E.h2(img.getAttribute('data-title'), style='text-align:center; font-size:larger; font-weight: bold'),
E.div(_('by'), style='text-align: center'),
E.h2(img.getAttribute('data-authors'), style='text-align:center; font-size:larger; font-weight: bold')
))
set_css(div, border='dashed 1px currentColor', border_radius='10px')
def cover_grid_item(self, book_id):
cover_url = str.format('get/thumb/{}/{}?sz={}x{}', book_id, self.interface_data['library_id'], THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT)
metadata = self.interface_data['metadata'][book_id]
alt = str.format(_('{} by {}'), metadata['title'], metadata['authors'].join(' & '))
img = E.img(src=cover_url, alt=alt, title=alt, data_title=metadata['title'], data_authors=metadata['authors'].join(' & '),
style='max-width: 100%; max-height: 100%; display: block; width:auto; height:auto')
img.onerror = def(err):
img = err.target
div = img.parentNode
if not div:
return
clear(div)
div.appendChild(E.div(
style='position:relative; top:-50%; transform: translateY(50%)',
E.h2(img.getAttribute('data-title'), style='text-align:center; font-size:larger; font-weight: bold'),
E.div(_('by'), style='text-align: center'),
E.h2(img.getAttribute('data-authors'), style='text-align:center; font-size:larger; font-weight: bold')
))
set_css(div, border='dashed 1px currentColor', border_radius='10px')
img.onerror = self.on_cover_grid_img_err.bind(self)
return E.div(
ans = E.div(
style=str.format(('margin: 10px; display: flex; align-content: flex-end; align-items: flex-end; justify-content: space-around;'
'width: 21vw; height: 28vw; max-width: {}px; max-height: {}px; min-width: {}px; min-height: {}px; cursor:pointer'),
THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT, THUMBNAIL_MAX_WIDTH // 2, THUMBNAIL_MAX_HEIGHT // 2),
data_book_id=str(book_id),
img
)
ans.addEventListener('click', def(ev): self.show_book_details(book_id);)
return ans
# }}}
@ -261,3 +265,6 @@ class BooksView:
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})