From 08da31ba3c2a07899d6981e05238f9d5392a3765 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 18 Jul 2017 10:29:49 +0530 Subject: [PATCH] More work on the custom list mode --- src/pyj/book_list/custom_list.pyj | 261 ++++++++++++++++++++++++++++-- src/pyj/book_list/views.pyj | 19 ++- 2 files changed, 264 insertions(+), 16 deletions(-) diff --git a/src/pyj/book_list/custom_list.pyj b/src/pyj/book_list/custom_list.pyj index 7f5f4a13cb..06bb5b4b9d 100644 --- a/src/pyj/book_list/custom_list.pyj +++ b/src/pyj/book_list/custom_list.pyj @@ -2,19 +2,256 @@ # License: GPL v3 Copyright: 2017, Kovid Goyal from __python__ import bound_methods, hash_literals +from elementmaker import E from gettext import gettext as _ +from book_list.details_list import THUMBNAIL_MAX_HEIGHT +from book_list.library_data import library_data +from date import format_date +from dom import build_rule, clear, set_css, svgicon +from session import get_interface_data +from utils import fmt_sidx, safe_set_inner_html, sandboxed_html + +CUSTOM_LIST_CLASS = 'book-list-custom-list' +ITEM_CLASS = CUSTOM_LIST_CLASS + '-item' +DESCRIPTION = _('A customizable list') + +def custom_list_css(): + ans = '' + sel = '.' + CUSTOM_LIST_CLASS + ans += build_rule(sel, cursor='pointer', user_select='none') + sel += ' .' + ITEM_CLASS + ans += build_rule(sel, margin='1ex 1em', padding_bottom='1ex', overflow='hidden', border_bottom='solid 1px currentColor') + + sel += ' iframe' + # To enable clicking anywhere on the item to load book details to work, we + # have to set pointer-events: none + # That has the side effect of disabling text selection + ans += build_rule(sel, flex_grow='10', cursor='pointer', pointer_events='none') + return ans + def default_template(): - return { - 'cover': True, - 'comments': False, - 'comments_fields': ['comments'], - 'height': '12.5ex', - 'lines': [ - _('{title} by {authors}'), - _('{series_index} of {series}') + '|||{rating}', - '{tags}', - _('Date: {timestamp} Published: {pubdate}'), - ] - } + if not default_template.ans: + default_template.ans = { + 'thumbnail': True, + 'thumbnail_height': THUMBNAIL_MAX_HEIGHT, + 'height': 'auto', + 'comments_fields': v"['comments']", + 'lines': [ + _('{title} by {authors}'), + _('{series_index} of {series}') + '|||{rating}', + '{tags}', + _('Date: {timestamp} Published: {pubdate}'), + ] + } + return default_template.ans + + +def render_field(field, mi, book_id): # {{{ + field_metadata = library_data.field_metadata + fm = field_metadata[field] + if not fm: + return + val = mi[field] + if val is undefined or val is None: + return + interface_data = get_interface_data() + + def add_val(val, is_html=False, join=None): + if is_html and /[<>]/.test(val + ''): + return safe_set_inner_html(E.span(), val) + if join: + val = val.join(join) + else: + val += '' + return val + + def process_composite(field, fm, name, val): + if fm.display and fm.display.contains_html: + return add_val(val, is_html=True) + if fm.is_multiple and fm.is_multiple.list_to_ui: + all_vals = filter(None, map(str.strip, val.split(fm.is_multiple.list_to_ui))) + return add_val(all_vals, join=fm.is_multiple.list_to_ui) + return add_val(val) + + def process_authors(field, fm, name, val): + return add_val(val, join=' & ') + + def process_publisher(field, fm, name, val): + return add_val(val) + + def process_formats(field, fm, name, val): + return add_val(val, join=', ') + + def process_rating(field, fm, name, val): + stars = E.span() + val = int(val or 0) + if val > 0: + for i in range(val // 2): + stars.appendChild(svgicon('star')) + if fm.display.allow_half_stars and (val % 2): + stars.appendChild(svgicon('star-half')) + return stars + + def process_identifiers(field, fm, name, val): + if val: + keys = Object.keys(val) + if keys.length: + ans = v'[]' + for key in keys: + ans.push(key + ':' + val[key]) + return add_val(ans, join=', ') + + def process_languages(field, fm, name, val): + if val and val.length: + langs = [mi.lang_names[k] for k in val] + return add_val(langs, join=', ') + + def process_datetime(field, fm, name, val): + if val: + fmt = interface_data['gui_' + field + '_display_format'] or (fm['display'] or {}).date_format + return add_val(format_date(val, fmt)) + + def process_series(field, fm, name, val): + if val: + return add_val(val) + + def process_series_index(field, fm, name, val): + sval = mi[field[:-6]] + if sval: + return fmt_sidx(val or 1, use_roman=interface_data.use_roman_numerals_for_series_number) + + name = fm.name or field + datatype = fm.datatype + if field is 'comments' or datatype is 'comments': + return + func = None + if datatype is 'composite': + func = process_composite + elif field is 'formats': + func = process_formats + elif datatype is 'rating': + func = process_rating + elif field is 'identifiers': + func = process_identifiers + elif field is 'authors': + func = process_authors + elif field is 'publisher': + func = process_publisher + elif field is 'languages': + func = process_languages + elif datatype is 'datetime': + func = process_datetime + elif datatype is 'series': + func = process_series + elif field.endswith('_index'): + func = process_series_index + ans = None + if func: + ans = func(field, fm, name, val) + else: + if datatype is 'text' or datatype is 'enumeration': + if val is not undefined and val is not None: + join = fm.is_multiple.list_to_ui if fm.is_multiple else None + ans = add_val(val, join=join) + elif datatype is 'bool': + ans = add_val(_('Yes') if val else _('No')) + elif datatype is 'int' or datatype is 'float': + if val is not undefined and val is not None: + fmt = (fm.display or {}).number_format + if fmt: + val = fmt.format(val) + else: + val += '' + ans = add_val(val) + return ans +# }}} + + +def render_part(part, template, book_id, metadata): + count = rendered_count = 0 + ans = E.div() + for field in part.split(/({[_a-z0-9]+})/): + if field[0] is '{' and field[-1] is '}': + count += 1 + val = render_field(field[1:-1], metadata, book_id) + if val: + rendered_count += 1 + if jstype(val) is 'string': + val = document.createTextNode(val) + ans.appendChild(val) + else: + ans.appendChild(document.createTextNode(field)) + + if count and not rendered_count: + return + return ans + + +def render_line(line, template, book_id, metadata): + parts = v'[]' + for p in line.split(/\|\|\|/): + part = render_part(p, template, book_id, metadata) + if part: + parts.push(part) + if not parts.length: + return + ans = E.div(class_='custom-line') + for p in parts: + ans.appendChild(p) + if parts.length > 1: + set_css(ans, display='flex', justify_content='space-between') + return ans + + +def render_template_text(template, book_id, metadata): + ans = E.div() + for line in template.lines: + ldiv = render_line(line, template, book_id, metadata) + if ldiv: + ans.appendChild(ldiv) + if template.comments_fields.length: + html = '' + for f in template.comments_fields: + val = metadata[f] + if val: + html += f'
{val}
' + + if html: + comments = sandboxed_html(html, 'html { overflow: hidden }') + ans.appendChild(comments) + return ans + + +def init(container): + clear(container) + container.appendChild(E.div(class_=CUSTOM_LIST_CLASS)) + + +def create_item(book_id, metadata, create_image, show_book_details): + template = default_template() + text_data = render_template_text(template, book_id, metadata) + text_data.style.flexGrow = '10' + if template.thumbnail: + height = f'{template.thumbnail_height}px' + else: + if template.height is 'auto': + height = (template.lines.length * 2.5 + 1) + 'ex' + else: + height = template.height + if jstype(height) is 'number': + height += 'px' + ans = E.div( + style=f'height:{height}; display: flex', + class_=ITEM_CLASS, + ) + if template.thumbnail: + pass + ans.appendChild(text_data) + ans.addEventListener('click', show_book_details, True) + return ans + + +def append_item(container, item): + container.lastChild.appendChild(item) diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj index 24ec13a63a..a44e64b110 100644 --- a/src/pyj/book_list/views.pyj +++ b/src/pyj/book_list/views.pyj @@ -11,6 +11,11 @@ from book_list.cover_grid import ( DESCRIPTION as COVER_GRID_DESCRIPTION, append_item as cover_grid_append_item, cover_grid_css, create_item as create_cover_grid_item, init as init_cover_grid ) +from book_list.custom_list import ( + DESCRIPTION as CUSTOM_LIST_DESCRIPTION, append_item as custom_list_append_item, + create_item as create_custom_list_item, custom_list_css, + init as init_custom_list +) from book_list.details_list import ( DESCRIPTION as DETAILS_LIST_DESCRIPTION, append_item as details_list_append_item, create_item as create_details_list_item, details_list_css, @@ -19,9 +24,9 @@ from book_list.details_list import ( from book_list.globals import get_session_data from book_list.item_list import create_item, create_item_list from book_list.library_data import ( - all_virtual_libraries, book_metadata, current_sorted_field, - ensure_current_library_data, library_data, load_status, loaded_books_query, - thumbnail_cache, url_books_query, add_more_books, current_book_ids + add_more_books, all_virtual_libraries, book_metadata, current_book_ids, + current_sorted_field, ensure_current_library_data, library_data, load_status, + loaded_books_query, thumbnail_cache, url_books_query ) from book_list.router import back, home, push_state, update_window_title from book_list.search import ( @@ -37,7 +42,7 @@ from widgets import create_button, create_spinner CLASS_NAME = 'book-list-container' ITEM_CLASS_NAME = 'book-list-item' -ALLOWED_MODES = {'cover_grid', 'details_list'} +ALLOWED_MODES = {'cover_grid', 'details_list', 'custom_list'} DEFAULT_MODE = 'cover_grid' add_extra_css(def(): @@ -45,6 +50,7 @@ add_extra_css(def(): ans = build_rule(sel + '[data-component="top_message"]', margin='1ex 1em') ans += cover_grid_css() ans += details_list_css() + ans += custom_list_css() return ans ) @@ -134,6 +140,10 @@ def setup_view_mode(mode, book_list_data): book_list_data.render_book = create_details_list_item book_list_data.init_grid = init_details_list book_list_data.append_item = details_list_append_item + elif mode is 'custom_list': + book_list_data.render_book = create_custom_list_item + book_list_data.init_grid = init_custom_list + book_list_data.append_item = custom_list_append_item return mode @@ -409,6 +419,7 @@ def create_mode_panel(container_id): ci(_('Cover grid'), COVER_GRID_DESCRIPTION, 'cover_grid') ci(_('Detailed list'), DETAILS_LIST_DESCRIPTION, 'details_list') + ci(_('Custom list'), CUSTOM_LIST_DESCRIPTION, 'custom_list') container.appendChild(E.div()) create_item_list(container.lastChild, items, _('Choose a display mode for the list of books from below'))