From 7dfeea3d1387e4872394e742623453fb181f0a56 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 16 Feb 2016 19:02:17 +0530 Subject: [PATCH] More work on the book details panel --- .../ebooks/metadata/sources/identify.py | 2 +- src/calibre/srv/code.py | 2 +- src/calibre/srv/metadata.py | 13 ++ src/pyj/book_list/book_details.pyj | 202 +++++++++++++++++- 4 files changed, 211 insertions(+), 8 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 4ac5ff72e9..1e033fad72 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -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: diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py index 32d34f486e..42f696de21 100644 --- a/src/calibre/srv/code.py +++ b/src/calibre/srv/code.py @@ -94,7 +94,7 @@ def interface_data(ctx, rd): Optional: ?num=50&sort=timestamp.desc&library_id= &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: diff --git a/src/calibre/srv/metadata.py b/src/calibre/srv/metadata.py index 3da5acab89..6b97847881 100644 --- a/src/calibre/srv/metadata.py +++ b/src/calibre/srv/metadata.py @@ -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({ diff --git a/src/pyj/book_list/book_details.pyj b/src/pyj/book_list/book_details.pyj index 62f8b542e8..3f937ae7a1 100644 --- a/src/pyj/book_list/book_details.pyj +++ b/src/pyj/book_list/book_details.pyj @@ -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)