mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start work on book details panel
This commit is contained in:
parent
14df859c63
commit
a3f1ee233d
@ -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):
|
||||
'''
|
||||
|
@ -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)
|
||||
|
||||
|
111
src/pyj/book_list/book_details.pyj
Normal file
111
src/pyj/book_list/book_details.pyj
Normal 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'
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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})
|
||||
|
Loading…
x
Reference in New Issue
Block a user