diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 08e79b76cf..a64be2654e 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -753,6 +753,9 @@ class Metadata: res = human_readable(res) return (name, str(res), orig_res, fmeta) + if self.get(key, None): + return (key, str(self.get(key)), self.get(key), None) + return (None, None, None, None) def __unicode__representation__(self): diff --git a/src/calibre/ebooks/metadata/book/render.py b/src/calibre/ebooks/metadata/book/render.py index 0ccf37240d..11c5ba445d 100644 --- a/src/calibre/ebooks/metadata/book/render.py +++ b/src/calibre/ebooks/metadata/book/render.py @@ -12,6 +12,7 @@ from calibre import force_unicode, prepare_string_for_xml from calibre.constants import filesystem_encoding from calibre.db.constants import DATA_DIR_NAME from calibre.ebooks.metadata import fmt_sidx, rating_to_stars +from calibre.ebooks.metadata.book.formatter import SafeFormat from calibre.ebooks.metadata.search_internet import DEFAULT_AUTHOR_SOURCE, name_for, qquote, url_for_author_search, url_for_book_search from calibre.ebooks.metadata.sources.identify import urls_from_identifiers from calibre.library.comments import comments_to_html, markdown @@ -66,6 +67,19 @@ def search_action_with_data(search_term, value, book_id, field=None, **k): return search_action(search_term, value, field=field, book_id=book_id, **k) +def cc_search_action_with_data(search_term, value, book_id, fm, mi, field=None, **k): + if mi is not None and fm is not None: + template = fm.get('display', {}).get('web_search_template') + if template: + formatter = SafeFormat() + mi.set('item_value', value) + u = formatter.safe_format(template, mi, "BOOK DETAILS WEB LINK", mi) + if u: + v = prepare_string_for_xml(_('Click to browse to {0}').format(u), attribute=True) + return action('cc_url', url=u),v + t = _('Click to see books with {0}: {1}').format(mi.get('name', search_term), prepare_string_for_xml(value)) + return search_action_with_data(search_term, value, book_id, **k), t + def notes_action(**keys): return 'notes:' + as_hex_unicode(json_dumps(keys)) @@ -211,16 +225,16 @@ def mi_to_html( ans.append((field, row % (name, comments_to_html(val)))) else: if not metadata['is_multiple']: - val = '{}'.format( - search_action(field, val, book_id=book_id), - _('Click to see books with {0}: {1}').format(metadata['name'], a(val)), p(val)) + u,v = cc_search_action_with_data(field, val, book_id, metadata, mi, field) + val = '{}'.format(u, v, p(val)) else: all_vals = [v.strip() for v in val.split(metadata['is_multiple']['cache_to_list']) if v.strip()] if show_links: - links = ['{}'.format( - search_action(field, x, book_id=book_id), _('Click to see books with {0}: {1}').format( - metadata['name'], a(x)), p(x)) for x in all_vals] + links = [] + for x in all_vals: + u,v = cc_search_action_with_data(field, x, book_id, metadata, mi, field) + links.append('{}'.format(u, v, p(x))) else: links = all_vals val = value_list(metadata['is_multiple']['list_to_ui'], links) @@ -349,12 +363,19 @@ def mi_to_html( except Exception: st = field series = getattr(mi, field) - val = _( - '%(sidx)s of ' - '%(series)s') % dict( - sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls='series_name', - series=p(series), href=search_action_with_data(st, series, book_id, field), - tt=p(_('Click to see books in this series'))) + if metadata.get('display', {}).get('web_search_template'): + u,v = cc_search_action_with_data(st, series, book_id, metadata, mi, field) + val = _('%(sidx)s of ' + '%(series)s') % dict( + sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls='series_name', + series=p(series), href=u, tt=v) + else: + val = _( + '%(sidx)s of ' + '%(series)s') % dict( + sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls='series_name', + series=p(series), href=search_action_with_data(st, series, book_id, field), + tt=p(_('Click to see books in this series'))) val += add_other_links(field, series) elif metadata['datatype'] == 'datetime': aval = getattr(mi, field) @@ -371,32 +392,51 @@ def mi_to_html( search_action_with_data(key, str(aval), book_id, None, original_value=val), a( _('Click to see books with {0}: {1} (derived from {2})').format( metadata['name'] or field, aval, val)), val) - elif metadata['datatype'] == 'text' and metadata['is_multiple']: - try: - st = metadata['search_terms'][0] - except Exception: - st = field - all_vals = mi.get(field) - if not metadata.get('display', {}).get('is_names', False): - all_vals = sorted(all_vals, key=sort_key) - links = [] - for x in all_vals: - v = '{}'.format( - search_action_with_data(st, x, book_id, field), _('Click to see books with {0}: {1}').format( - metadata['name'] or field, a(x)), p(x)) - v += add_other_links(field, x) - links.append(v) - val = value_list(metadata['is_multiple']['list_to_ui'], links) - elif metadata['datatype'] == 'text' or metadata['datatype'] == 'enumeration': - # text/is_multiple handled above so no need to add the test to the if - try: - st = metadata['search_terms'][0] - except Exception: - st = field - v = '{}'.format( - search_action_with_data(st, unescaped_val, book_id, field), a( - _('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val) - val = v + add_other_links(field, val) + elif metadata['datatype'] == 'text': + if metadata['is_multiple']: + try: + st = metadata['search_terms'][0] + except Exception: + st = field + all_vals = mi.get(field) + if not metadata.get('display', {}).get('is_names', False): + all_vals = sorted(all_vals, key=sort_key) + links = [] + if show_links: + for x in all_vals: + if metadata['is_custom']: + u,v = cc_search_action_with_data(field, x, book_id, metadata, mi, field) + v = '{}'.format(u, v, p(x)) + else: + v = '{}'.format( + search_action_with_data(st, x, book_id, field), + _('Click to see books with {0}: {1}').format( + metadata['name'] or field, a(x)), p(x)) + v += add_other_links(field, x) + links.append(v) + else: + links = all_vals + val = value_list(metadata['is_multiple']['list_to_ui'], links) + else: + try: + st = metadata['search_terms'][0] + except Exception: + st = field + if show_links: + if metadata['is_custom']: + u,v = cc_search_action_with_data(st, x, book_id, metadata, mi, field) + v = '{}'.format(u, v, p(x)) + else: + v = '{}'.format( + search_action_with_data(st, x, book_id, field), + _('Click to see books with {0}: {1}').format( + metadata['name'] or field, a(x)), p(x)) + else: + v = val + val = v + add_other_links(field, val) + elif metadata['datatype'] == 'enumeration': + u,v = cc_search_action_with_data(field, x, book_id, metadata, mi, field) + val = '{}'.format(u, v, p(x)) + add_other_links(field, val) elif metadata['datatype'] == 'bool': val = '{}'.format( search_action_with_data(field, val, book_id, None), a( diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 538e0c9dd3..cc9884600f 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -1479,6 +1479,9 @@ class BookDetails(DetailsLayout, DropMixin): # {{{ if dt == 'search': field = data.get('field') search_term(data['term'], data['value']) + elif dt == 'cc_url': + if data['url']: + browse(data['url']) elif dt == 'author': url = data['url'] if url == 'calibre': diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index c51a8cdd9f..2ec2ecf96f 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -26,11 +26,13 @@ from qt.core import ( QRadioButton, QSpinBox, Qt, + QToolButton, QVBoxLayout, QWidget, ) from calibre.gui2 import error_dialog +from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor from calibre.utils.date import UNDEFINED_DATE, parse_date from calibre.utils.localization import ngettext @@ -231,6 +233,7 @@ class CreateCustomColumn(QDialog): elif ct == '*text': self.is_names.setChecked(c['display'].get('is_names', False)) self.description_box.setText(c['display'].get('description', '')) + self.web_search_template.setText(c['display'].get('web_search_template', '')) self.decimals_box.setValue(min(9, max(1, int(c['display'].get('decimals', 2))))) all_colors = [str(s) for s in list(QColor.colorNames())] @@ -486,6 +489,7 @@ class CreateCustomColumn(QDialog): l.addWidget(cch) l.addStretch() add_row(None, l) + l = QHBoxLayout() self.composite_in_comments_box = cmc = QCheckBox(_('Show with comments in Book details')) cmc.setToolTip('

' + _('If you check this box then the column contents ' @@ -533,9 +537,45 @@ class CreateCustomColumn(QDialog): 'Rating columns enter a number between 0 and 5.') + '

') self.default_value_label = add_row(_('&Default value:'), dv) + l = QHBoxLayout() + self.web_search_label = QLabel(_('Search tem&plate:')) + l.addWidget(self.web_search_label) + wst = self.web_search_template = QLineEdit() + wst.setToolTip('

' + _( + "Fill in this box if you want clicking on the value in book details to do a " + "web search instead of searching your calibre library. The book's metadata are " + "available to the template. An additional field 'item_value' is available to the " + "template. For multiple-valued (tags-like) columns it is the value being examined, " + "telling you which value to use to generate the link.") + '

') + l.addWidget(wst) + self.web_search_label.setBuddy(wst) + wst_tb = self.web_search_toolbutton = QToolButton() + wst_tb.setIcon(QIcon.ic('edit_input.png')) + l.addWidget(wst_tb) + wst_tb.clicked.connect(self.cws_template_button_clicked) + add_row(None, l) + self.resize(self.sizeHint()) # }}} + def cws_template_button_clicked(self): + db = self.gui.current_db.new_api + lv = self.gui.library_view + rows = lv.selectionModel().selectedRows() + if not self.editing_col or not rows: + vals = [{'value': _('Value'), 'lookup_name': _('Lookup name'), 'author': _('Author'), + 'title': _('Title'), 'author_sort': _('Author sort')}] + else: + vals = [] + for row in rows: + book_id = lv.model().id(row) + mi = db.new_api.get_metadata(book_id) + mi.set('item_value', 'Item Value') + vals.append(mi) + d = TemplateDialog(parent=self, text=self.web_search_template.text(), mi=vals) + if d.exec() == QDialog.DialogCode.Accepted: + self.web_search_template.setText(d.rule[1]) + def bool_radio_button_clicked(self, button, clicked): if clicked: self.bool_button_group.setFocusProxy(button) @@ -617,7 +657,7 @@ class CreateCustomColumn(QDialog): 'after the decimal point and thousands separated by commas.') + '

' ) self.format_label.setText(l), self.format_default_label.setText(dl) - for x in ('in_comments_box', 'heading_position', 'heading_position_label'): + for x in ('in_comments_box', 'heading_position', 'heading_position_label',): getattr(self, 'composite_'+x).setVisible(col_type == 'composite') for x in ('box', 'default_label', 'label', 'sort_by', 'sort_by_label', 'make_category', 'contains_html'): @@ -638,6 +678,13 @@ class CreateCustomColumn(QDialog): self.comments_heading_position_label.setVisible(is_comments) self.comments_type.setVisible(is_comments) self.comments_type_label.setVisible(is_comments) + + has_url_template = not is_comments and col_type in ('text', '*text', 'composite', '*composite', + 'series', 'enumeration') + self.web_search_label.setVisible(has_url_template) + self.web_search_template.setVisible(has_url_template) + self.web_search_toolbutton.setVisible(has_url_template) + self.allow_half_stars.setVisible(col_type == 'rating') is_bool = col_type == 'bool' @@ -811,6 +858,7 @@ class CreateCustomColumn(QDialog): display_dict['default_value'] = default_val display_dict['description'] = self.description_box.text().strip() + display_dict['web_search_template'] = self.web_search_template.text().strip() if not self.editing_col: self.caller.custcols[key] = { diff --git a/src/calibre/gui2/preferences/look_feel_tabs/__init__.py b/src/calibre/gui2/preferences/look_feel_tabs/__init__.py index ac16fefc9e..a3693c3bad 100644 --- a/src/calibre/gui2/preferences/look_feel_tabs/__init__.py +++ b/src/calibre/gui2/preferences/look_feel_tabs/__init__.py @@ -7,14 +7,17 @@ __docformat__ = 'restructuredtext en' import json -from qt.core import QAbstractListModel, QComboBox, QFormLayout, QIcon, QItemSelectionModel, QLineEdit, Qt, QVBoxLayout, QWidget, pyqtSignal +from qt.core import QAbstractListModel, QComboBox, QDialog, QFormLayout, QHBoxLayout, QIcon, QItemSelectionModel, QLineEdit, Qt, QToolButton, QVBoxLayout, QWidget, pyqtSignal from calibre.ebooks.metadata.book.render import DEFAULT_AUTHOR_LINK +from calibre.ebooks.metadata.search_internet import qquote from calibre.gui2 import choose_files, choose_save_file, error_dialog from calibre.gui2.book_details import get_field_list +from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.preferences import LazyConfigWidgetBase from calibre.gui2.preferences.coloring import EditRules from calibre.gui2.ui import get_gui +from calibre.utils.formatter import EvalFormatter class DefaultAuthorLink(QWidget): @@ -44,6 +47,7 @@ class DefaultAuthorLink(QWidget): ]: c.addItem(text, data) l.addRow(_('Clicking on &author names should:'), c) + ul = QHBoxLayout() self.custom_url = u = QLineEdit(self) u.setToolTip(_( 'Enter the URL to search. It should contain the string {0}' @@ -51,8 +55,14 @@ class DefaultAuthorLink(QWidget): '\n{1}').format('{author}', 'https://en.wikipedia.org/w/index.php?search={author}')) u.textChanged.connect(self.changed_signal) u.setPlaceholderText(_('Enter the URL')) + ul.addWidget(u) + u = self.custom_url_button = QToolButton() + u.setIcon(QIcon.ic('edit_input.png')) + u.setToolTip(_('Click this button to open the template tester')) + u.clicked.connect(self.open_template_tester) + ul.addWidget(u) c.currentIndexChanged.connect(self.current_changed) - l.addRow(u) + l.addRow(ul) self.current_changed() c.currentIndexChanged.connect(self.changed_signal) @@ -71,9 +81,29 @@ class DefaultAuthorLink(QWidget): self.custom_url.setText(val) self.choices.setCurrentIndex(i) + def open_template_tester(self): + gui = get_gui() + db = gui.current_db.new_api + lv = gui.library_view + rows = lv.selectionModel().selectedRows() + if not rows: + vals = [{'author': qquote(_('Author')), 'title': _('Title'), 'author_sort': _('Author sort')}] + else: + vals = [] + for row in rows: + book_id = lv.model().id(row) + mi = db.new_api.get_proxy_metadata(book_id) + vals.append({'author': qquote(mi.authors[0]), + 'title': qquote(mi.title), + 'author_sort': qquote(mi.author_sort_map.get(mi.authors[0]))}) + d = TemplateDialog(parent=self, text=self.custom_url.text(), mi=vals, formatter=EvalFormatter) + if d.exec() == QDialog.DialogCode.Accepted: + self.custom_url.setText(d.rule[1]) + def current_changed(self): k = self.choices.currentData() self.custom_url.setVisible(k == 'url') + self.custom_url_button.setVisible(k == 'url') def restore_defaults(self): self.value = DEFAULT_AUTHOR_LINK