More work on the book details panel

This commit is contained in:
Kovid Goyal 2016-02-16 19:02:17 +05:30
parent b96602b350
commit 7dfeea3d13
4 changed files with 211 additions and 8 deletions

View File

@ -522,7 +522,7 @@ def identify(log, abort, # {{{
# }}}
def urls_from_identifiers(identifiers): # {{{
identifiers = dict([(k.lower(), v) for k, v in identifiers.iteritems()])
identifiers = {k.lower():v for k, v in identifiers.iteritems()}
ans = []
for plugin in all_metadata_plugins():
try:

View File

@ -94,7 +94,7 @@ def interface_data(ctx, rd):
Optional: ?num=50&sort=timestamp.desc&library_id=<default library>
&search=''&extra_books=''
'''
ans = {'username':rd.username, 'output_format':prefs['output_format'].upper(), 'input_formats':tuple(x.upper() for x in available_input_formats())}
ans = {'username':rd.username, 'output_format':prefs['output_format'].upper(), 'input_formats':{x.upper():True for x in available_input_formats()}}
ans['library_map'], ans['default_library'] = ctx.library_map
ud = {}
if rd.username:

View File

@ -14,11 +14,14 @@ from urllib import quote
from calibre.constants import config_dir
from calibre.db.categories import Tag
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers
from calibre.utils.date import isoformat, UNDEFINED_DATE, local_tz
from calibre.utils.config import tweaks, JSONConfig
from calibre.utils.formatter import EvalFormatter
from calibre.utils.file_type_icons import EXT_MAP
from calibre.utils.icu import collation_order
from calibre.utils.localization import calibre_langcode_to_name
from calibre.library.comments import comments_to_html
from calibre.library.field_metadata import category_icon_map
IGNORED_FIELDS = frozenset('cover ondevice path marked au_map'.split())
@ -45,6 +48,10 @@ def add_field(field, db, book_id, ans, field_metadata):
val = encode_datetime(val)
if val is None:
return
elif datatype == 'comments' or field == 'comments':
val = comments_to_html(val)
elif datatype == 'composite' and field_metadata['display'].get('contains_html'):
val = comments_to_html(val)
ans[field] = val
def book_as_json(db, book_id):
@ -57,6 +64,12 @@ def book_as_json(db, book_id):
for field in fm.all_field_keys():
if field not in IGNORED_FIELDS:
add_field(field, db, book_id, ans, fm[field])
ids = ans.get('identifiers')
if ids:
ans['urls_from_identifiers'] = urls_from_identifiers(ids)
langs = ans.get('languages')
if langs:
ans['lang_names'] = {l:calibre_langcode_to_name(l) for l in langs}
return ans
_include_fields = frozenset(Tag.__slots__) - frozenset({

View File

@ -26,7 +26,6 @@ def sort_formats_key(fmt):
def get_preferred_format(metadata, output_format, input_formats):
formats = (metadata and metadata.formats) or v'[]'
formats = [f.toUpperCase() for f in formats]
input_formats = {x.toUpperCase():True for x in input_formats}
fmt = 'EPUB' if output_format == 'PDF' else output_format
if formats.length and formats.indexOf(fmt) == -1:
for q in sorted(formats, key=sort_formats_key):
@ -35,6 +34,184 @@ def get_preferred_format(metadata, output_format, input_formats):
break
return fmt.toUpperCase()
IGNORED_FIELDS = {'title', 'id', 'urls_from_identifiers', 'lang_names'}
def allowed_fields(field):
if str.endswith(field, '_index'):
return False
if str.startswith(field, '#'):
return True
if field in IGNORED_FIELDS or str.endswith(field, '_sort'):
return False
return True
default_sort = {f:i+1 for i, f in enumerate(('title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', 'identifiers', 'languages', 'publisher'))}
default_sort['formats'] = 999
def field_sorter(field_metadata):
return def(field):
lvl = str.format('{:03d}', default_sort[field] or 998)
fm = (field_metadata[field] or {})[field] or {}
return lvl + (fm.name or 'zzzzz')
def execute_search(ev):
name, val = JSON.parse(ev.currentTarget.getAttribute('data-search'))
search = str.format('{}:"={}"', name, str.replace(val, '"', r'\"'))
get_boss().ui.books_view.change_search(search)
def download_format(ev):
fmt = ev.currentTarget.getAttribute('data-format')
get_boss().ui.book_details_panel.download_format(fmt)
def read_format(ev):
fmt = ev.currentTarget.getAttribute('data-format')
get_boss().ui.book_details_panel.read_format(fmt)
def render_metadata(mi, interface_data, table, field_list=None):
fields = field_list or sorted(filter(allowed_fields, mi), key=field_sorter(interface_data.field_metadata))
comments = []
def add_row(name, val, is_searchable=False, is_html=False, join=None):
def add_val(v):
v += ''
if is_searchable:
table.lastChild.lastChild.appendChild(E.a(
data_search=JSON.stringify([name, v]), onclick=execute_search,
title=str.format(_('Click to see books with {0}: {1}'), name, v), href='javascript: void(0)', v))
else:
table.lastChild.lastChild.appendChild(document.createTextNode(val))
table.appendChild(E.tr(E.td(name + ':'), E.td()))
if is_html:
table.lastChild.lastChild.innerHTML = val + ''
else:
if join is None:
add_val(val + '')
else:
for v in val:
add_val(v)
if v is not val[-1]:
table.lastChild.lastChild.appendChild(document.createTextNode(join))
def process_composite(field, fm, name, val):
if fm.display and fm.display.contains_html:
add_row(name, val, is_html=True)
return
if fm.is_multiple and fm.is_multiple.list_to_ui:
all_vals = filter(None, map(str.strip, str.split(val, fm.is_multiple.list_to_ui)))
add_row(name, all_vals, is_searchable=True, join=fm.is_multiple.list_to_ui)
else:
add_row(name, val, is_searchable=True)
def process_authors(field, fm, name, val):
add_row(name, val, is_searchable=True, join=' & ')
def process_publisher(field, fm, name, val):
add_row(name, val, is_searchable=True)
def process_formats(field, fm, name, val):
table.appendChild(E.tr(E.td(name + ':'), E.td()))
for fmt in val:
td = table.lastChild.lastChild
td.appendChild(E.span(fmt, style='white-space: nowrap'))
if interface_data.input_formats[fmt]:
td.lastChild.appendChild(E.a(
title=str.format(_('Read this book in the {} format'), fmt),
href='javascript:void(0)', style='padding-left: 1em',
E.i(class_='fa fa-book'),
onclick=read_format, data_format=fmt
))
td.lastChild.appendChild(E.a(
title=str.format(_('Download the {} format of this book'), fmt),
href='javascript:void(0)', style='padding-left: 1em',
E.i(class_='fa fa-cloud-download'),
onclick=download_format, data_format=fmt
))
if fmt is not val[-1]:
td.lastChild.appendChild(document.createTextNode(','))
td.appendChild(document.createTextNode(' '))
def process_rating(field, fm, name, val):
try:
val = '★'.repeat(int(val // 2))
except Exception:
return
add_row(name, val)
def process_identifiers(field, fm, name, val):
if val and val.length:
table.appendChild(E.tr(E.td(name + ':'), E.td()))
url_map = {k:v'[text, url]' for text, k, val, url in mi.urls_from_identifiers or v'[]'}
td = table.lastChild.lastChild
keys = Object.keys(val)
for k in keys:
idval = val[k]
x = url_map[k]
if isinstance(x, list) and x.length == 2:
td.appendChild(E.a(title=str.format('{}:{}', k, idval), target='_new', href=x[1], x[0]))
else:
td.appendChild(E.span(k, ':', idval))
if k is not keys[-1]:
td.appendChild(document.createTextNode(', '))
def process_languages(field, fm, name, val):
if val and val.length:
table.appendChild(E.tr(E.td(name + ':'), E.td()))
td = table.lastChild.lastChild
for k in val:
lang = mi.lang_names[k] or k
td.appendChild(E.a(lang,
title=str.format(_('Click to see books with language: {}'), lang), href='javascript: void(0)',
data_search=JSON.stringify([field, k]), onclick=execute_search
))
if k is not val[-1]:
td.appendChild(document.createTextNode(', '))
def process_field(field, fm):
name = fm.name or field
datatype = fm.datatype
val = mi[field]
if field == 'comments' or datatype == 'comments':
comments.append(val)
return
func = None
if datatype == 'composite':
func = process_composite
elif field == 'formats':
func = process_formats
elif datatype == 'rating':
func = process_rating
elif field == 'identifiers':
func = process_identifiers
elif field == 'authors':
func = process_authors
elif field == 'publisher':
func = process_publisher
elif field == 'languages':
func = process_languages
if func:
func(field, fm, name, val)
else:
pass
for field in fields:
fm = interface_data.field_metadata[field]
if not fm:
continue
try:
process_field(field, fm)
except Exception as err:
print('Failed to render metadata field: ' + field)
print(err.toString())
print(err.stack)
for i, comment in enumerate(comments):
div = E.div()
div.innerHTML = comment
table.parentNode.appendChild(div)
if i == 0:
div.style.marginTop = '2ex'
class BookDetailsPanel:
def __init__(self, interface_data, book_list_container):
@ -104,7 +281,7 @@ class BookDetailsPanel:
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())
error_dialog(_('Could not fetch metadata for book'), _('Server returned an invalid response'), err.toString())
return
clear(c)
book_id = data['id']
@ -127,7 +304,7 @@ class BookDetailsPanel:
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=str.format('max-width: calc(50vw - 3em); max-height: calc(100vh - 4ex - {}); display: block; width:auto; height:auto; float:right', get_font_size('title'))
style=str.format('border-radius: 20px; max-width: calc(50vw - 3em); max-height: calc(100vh - 4ex - {}); display: block; width:auto; height:auto; float:left', get_font_size('title'))
)
img.onerror = self.on_img_err.bind(self)
c = self.container
@ -144,18 +321,31 @@ class BookDetailsPanel:
if not metadata.formats or not metadata.formats.length:
row.style.display = 'none'
container.appendChild(row)
md = E.div(style='max-width:500px')
table = E.table(class_='metadata')
container.appendChild(md)
md.appendChild(table)
render_metadata(metadata, self.interface_data, table)
def on_img_err(self, err):
img = err.target
img.style.display = 'none'
def preferred_format(self, book_id):
return get_preferred_format(self.interface_data.metadata[book_id], self.interface_data['output_format'], self.interface_data['input_formats'])
return get_preferred_format(self.interface_data.metadata[book_id], self.interface_data.output_format, self.interface_data.input_formats)
def download_format(self, fmt):
window.location = str.format('get/{}/{}/{}', fmt, self.current_book_id, self.interface_data.library_id)
def download_book(self):
book_id = self.current_book_id
fmt = self.preferred_format(book_id)
window.location = str.format('get/{}/{}/{}', fmt, book_id, self.interface_data.library_id)
self.download_format(fmt)
def read_format(self, fmt):
pass
def read_book(self):
pass
book_id = self.current_book_id
fmt = self.preferred_format(book_id)
self.read_format(fmt)