mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
More work on the book details panel
This commit is contained in:
parent
b96602b350
commit
7dfeea3d13
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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({
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user