From e7af7af508fa33b486b58d67c2ccb80a4535334f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jul 2017 20:23:50 +0530 Subject: [PATCH] GUI for creating custom list templates --- src/calibre/gui2/preferences/server.py | 110 ++++++++++++++++++++++++- src/calibre/srv/embedded.py | 22 ++++- src/pyj/book_list/custom_list.pyj | 5 +- 3 files changed, 133 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py index b6ea22a6a7..31a4a49ffc 100644 --- a/src/calibre/gui2/preferences/server.py +++ b/src/calibre/gui2/preferences/server.py @@ -2,6 +2,8 @@ # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # License: GPLv3 Copyright: 2010, Kovid Goyal +import errno +import json import os import textwrap import time @@ -19,6 +21,8 @@ from calibre.gui2 import ( config, error_dialog, gprefs, info_dialog, open_url, warning_dialog ) from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget +from calibre.srv.code import custom_list_template as default_custom_list_template +from calibre.srv.embedded import custom_list_template from calibre.srv.library_broker import load_gui_libraries from calibre.srv.opts import change_settings, options, server_config from calibre.srv.users import ( @@ -26,7 +30,6 @@ from calibre.srv.users import ( ) from calibre.utils.icu import primary_sort_key - # Advanced {{{ @@ -729,6 +732,104 @@ class Users(QWidget): # }}} +class CustomList(QWidget): # {{{ + + changed_signal = pyqtSignal() + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.default_template = default_custom_list_template() + self.l = l = QFormLayout(self) + l.setFieldGrowthPolicy(l.AllNonFixedFieldsGrow) + self.la = la = QLabel('

' + _( + 'Here you can create a template to control what data is shown when' + ' using the Custom list mode for the book list')) + la.setWordWrap(True) + l.addRow(la) + self.thumbnail = t = QCheckBox(_('Show a cover &thumbnail')) + self.thumbnail_height = th = QSpinBox(self) + th.setSuffix(' px'), th.setRange(60, 600) + self.entry_height = eh = QLineEdit(self) + l.addRow(t), l.addRow(_('Thumbnail &height:'), th) + l.addRow(_('Entry &height:'), eh) + t.stateChanged.connect(self.changed_signal) + th.valueChanged.connect(self.changed_signal) + eh.textChanged.connect(self.changed_signal) + eh.setToolTip(textwrap.fill(_( + 'The height for each entry. The special value "auto" causes a height to be calculated' + ' based on the number of lines in the template. Otherwise, use a CSS length, such as' + ' 100px or 15ex'))) + t.stateChanged.connect(self.thumbnail_state_changed) + th.setVisible(False) + + self.comments_fields = cf = QLineEdit(self) + l.addRow(_('&Long text fields:'), cf) + cf.setToolTip(textwrap.fill(_( + 'A comma separated list of fields that will be added at the bottom of every entry.' + ' These fields are interpreted as containing HTML, not plain text.'))) + cf.textChanged.connect(self.changed_signal) + + self.la1 = la = QLabel('

' + _( + 'The template below will be interpreted as HTML and all {{fields}} will be replaced' + ' by the actual metadata, if available. You can use {0} as a separator' + ' to split a line into multiple columns.').format('|||')) + la.setWordWrap(True) + l.addRow(la) + self.template = t = QPlainTextEdit(self) + l.addRow(t) + t.textChanged.connect(self.changed_signal) + + def thumbnail_state_changed(self): + is_enabled = bool(self.thumbnail.isChecked()) + for w, x in [(self.thumbnail_height, True), (self.entry_height, False)]: + w.setVisible(is_enabled is x) + self.layout().labelForField(w).setVisible(is_enabled is x) + + def genesis(self): + self.current_template = custom_list_template() or self.default_template + + @property + def current_template(self): + return { + 'thumbnail': self.thumbnail.isChecked(), + 'thumbnail_height': self.thumbnail_height.value(), + 'height': self.entry_height.text().strip() or 'auto', + 'comments_fields': [x.strip() for x in self.comments_fields.text().split(',') if x.strip()], + 'lines': [x.strip() for x in self.template.toPlainText().splitlines()] + } + + @current_template.setter + def current_template(self, template): + self.thumbnail.setChecked(bool(template.get('thumbnail'))) + try: + th = int(template['thumbnail_height']) + except Exception: + th = self.default_template['thumbnail_height'] + self.thumbnail_height.setValue(th) + self.entry_height.setText(template.get('height') or 'auto') + self.comments_fields.setText(', '.join(template.get('comments_fields') or ())) + self.template.setPlainText('\n'.join(template.get('lines') or ())) + + def restore_defaults(self): + self.current_template = self.default_template + + def commit(self): + template = self.current_template + if template == self.default_template: + try: + os.remove(custom_list_template.path) + except EnvironmentError as err: + if err.errno != errno.ENOENT: + raise + else: + raw = json.dumps(template, sort_keys=True, indent=4, separators=(',', ': ')) + with lopen(custom_list_template.path, 'wb') as f: + f.write(raw) + return True + +# }}} + + class ConfigWidget(ConfigWidgetBase): def __init__(self, *args, **kw): @@ -750,6 +851,10 @@ class ConfigWidget(ConfigWidgetBase): sa = QScrollArea(self) sa.setWidget(a), sa.setWidgetResizable(True) t.addTab(sa, _('&Advanced')) + self.custom_list_tab = clt = CustomList(self) + sa = QScrollArea(self) + sa.setWidget(clt), sa.setWidgetResizable(True) + t.addTab(sa, _('Book &list template')) for tab in self.tabs: if hasattr(tab, 'changed_signal'): tab.changed_signal.connect(self.changed_signal.emit) @@ -882,6 +987,8 @@ class ConfigWidget(ConfigWidgetBase): ) self.tabs_widget.setCurrentWidget(self.users_tab) return False + if not self.custom_list_tab.commit(): + return False ConfigWidgetBase.commit(self) change_settings(**settings) UserManager().user_data = users @@ -902,6 +1009,7 @@ class ConfigWidget(ConfigWidgetBase): def refresh_gui(self, gui): if self.server: self.server.user_manager.refresh() + self.server.ctx.custom_list_template = custom_list_template() if __name__ == '__main__': diff --git a/src/calibre/srv/embedded.py b/src/calibre/srv/embedded.py index 2d3233f966..2c75b9008c 100644 --- a/src/calibre/srv/embedded.py +++ b/src/calibre/srv/embedded.py @@ -4,11 +4,12 @@ from __future__ import absolute_import, division, print_function, unicode_literals import errno +import json import os from threading import Thread from calibre import as_unicode -from calibre.constants import cache_dir, is_running_from_develop +from calibre.constants import cache_dir, config_dir, is_running_from_develop from calibre.srv.bonjour import BonJour from calibre.srv.handler import Handler from calibre.srv.http_response import create_http_handler @@ -23,6 +24,20 @@ def log_paths(): ) +def custom_list_template(): + try: + with lopen(custom_list_template.path, 'rb') as f: + raw = f.read() + except EnvironmentError as err: + if err.errno != errno.ENOENT: + raise + return + return json.loads(raw) + + +custom_list_template.path = os.path.join(config_dir, 'server-custom-list-template.json') + + class Server(object): loop = current_thread = exception = None @@ -46,6 +61,11 @@ class Server(object): self.opts = opts self.log, self.access_log = log, access_log self.handler.set_log(self.log) + self.handler.router.ctx.custom_list_template = custom_list_template() + + @property + def ctx(self): + return self.handler.router.ctx @property def user_manager(self): diff --git a/src/pyj/book_list/custom_list.pyj b/src/pyj/book_list/custom_list.pyj index 5605218c3c..0cbf5d1b23 100644 --- a/src/pyj/book_list/custom_list.pyj +++ b/src/pyj/book_list/custom_list.pyj @@ -187,7 +187,7 @@ def render_part(part, template, book_id, metadata): if not n: break rendered = E.span() - for field in n.nodeValue.split(/({[_a-z0-9]+})/): + for field in n.nodeValue.split(/({#?[_a-z0-9]+})/): if field[0] is '{' and field[-1] is '}': count += 1 val = render_field(field[1:-1], metadata, book_id) @@ -272,7 +272,8 @@ def create_item(book_id, metadata, create_image, show_book_details): height = f'{template.thumbnail_height}px' else: if template.height is 'auto': - height = (template.lines.length * 2.5 + 1) + 'ex' + extra = 5 if template.comments_fields.length else 1 + height = (template.lines.length * 2.5 + extra) + 'ex' else: height = template.height if jstype(height) is 'number':