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): # {{{
|
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 = []
|
ans = []
|
||||||
for plugin in all_metadata_plugins():
|
for plugin in all_metadata_plugins():
|
||||||
try:
|
try:
|
||||||
|
@ -94,7 +94,7 @@ def interface_data(ctx, rd):
|
|||||||
Optional: ?num=50&sort=timestamp.desc&library_id=<default library>
|
Optional: ?num=50&sort=timestamp.desc&library_id=<default library>
|
||||||
&search=''&extra_books=''
|
&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
|
ans['library_map'], ans['default_library'] = ctx.library_map
|
||||||
ud = {}
|
ud = {}
|
||||||
if rd.username:
|
if rd.username:
|
||||||
|
@ -14,11 +14,14 @@ from urllib import quote
|
|||||||
|
|
||||||
from calibre.constants import config_dir
|
from calibre.constants import config_dir
|
||||||
from calibre.db.categories import Tag
|
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.date import isoformat, UNDEFINED_DATE, local_tz
|
||||||
from calibre.utils.config import tweaks, JSONConfig
|
from calibre.utils.config import tweaks, JSONConfig
|
||||||
from calibre.utils.formatter import EvalFormatter
|
from calibre.utils.formatter import EvalFormatter
|
||||||
from calibre.utils.file_type_icons import EXT_MAP
|
from calibre.utils.file_type_icons import EXT_MAP
|
||||||
from calibre.utils.icu import collation_order
|
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
|
from calibre.library.field_metadata import category_icon_map
|
||||||
|
|
||||||
IGNORED_FIELDS = frozenset('cover ondevice path marked au_map'.split())
|
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)
|
val = encode_datetime(val)
|
||||||
if val is None:
|
if val is None:
|
||||||
return
|
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
|
ans[field] = val
|
||||||
|
|
||||||
def book_as_json(db, book_id):
|
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():
|
for field in fm.all_field_keys():
|
||||||
if field not in IGNORED_FIELDS:
|
if field not in IGNORED_FIELDS:
|
||||||
add_field(field, db, book_id, ans, fm[field])
|
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
|
return ans
|
||||||
|
|
||||||
_include_fields = frozenset(Tag.__slots__) - frozenset({
|
_include_fields = frozenset(Tag.__slots__) - frozenset({
|
||||||
|
@ -26,7 +26,6 @@ def sort_formats_key(fmt):
|
|||||||
def get_preferred_format(metadata, output_format, input_formats):
|
def get_preferred_format(metadata, output_format, input_formats):
|
||||||
formats = (metadata and metadata.formats) or v'[]'
|
formats = (metadata and metadata.formats) or v'[]'
|
||||||
formats = [f.toUpperCase() for f in formats]
|
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
|
fmt = 'EPUB' if output_format == 'PDF' else output_format
|
||||||
if formats.length and formats.indexOf(fmt) == -1:
|
if formats.length and formats.indexOf(fmt) == -1:
|
||||||
for q in sorted(formats, key=sort_formats_key):
|
for q in sorted(formats, key=sort_formats_key):
|
||||||
@ -35,6 +34,184 @@ def get_preferred_format(metadata, output_format, input_formats):
|
|||||||
break
|
break
|
||||||
return fmt.toUpperCase()
|
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:
|
class BookDetailsPanel:
|
||||||
|
|
||||||
def __init__(self, interface_data, book_list_container):
|
def __init__(self, interface_data, book_list_container):
|
||||||
@ -104,7 +281,7 @@ class BookDetailsPanel:
|
|||||||
try:
|
try:
|
||||||
data = JSON.parse(xhr.responseText)
|
data = JSON.parse(xhr.responseText)
|
||||||
except Exception as err:
|
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
|
return
|
||||||
clear(c)
|
clear(c)
|
||||||
book_id = data['id']
|
book_id = data['id']
|
||||||
@ -127,7 +304,7 @@ class BookDetailsPanel:
|
|||||||
alt = str.format(_('{} by {}'), metadata['title'], metadata['authors'].join(' & '))
|
alt = str.format(_('{} by {}'), metadata['title'], metadata['authors'].join(' & '))
|
||||||
img = E.img(
|
img = E.img(
|
||||||
src=cover_url, alt=alt, title=alt, data_title=metadata['title'], data_authors=metadata['authors'].join(' & '),
|
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)
|
img.onerror = self.on_img_err.bind(self)
|
||||||
c = self.container
|
c = self.container
|
||||||
@ -144,18 +321,31 @@ class BookDetailsPanel:
|
|||||||
if not metadata.formats or not metadata.formats.length:
|
if not metadata.formats or not metadata.formats.length:
|
||||||
row.style.display = 'none'
|
row.style.display = 'none'
|
||||||
container.appendChild(row)
|
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):
|
def on_img_err(self, err):
|
||||||
img = err.target
|
img = err.target
|
||||||
img.style.display = 'none'
|
img.style.display = 'none'
|
||||||
|
|
||||||
def preferred_format(self, book_id):
|
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):
|
def download_book(self):
|
||||||
book_id = self.current_book_id
|
book_id = self.current_book_id
|
||||||
fmt = self.preferred_format(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):
|
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