This commit is contained in:
Kovid Goyal 2025-02-08 09:26:21 +05:30
commit 78be0f69a4
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 165 additions and 41 deletions

View File

@ -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):

View File

@ -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 = '<a href="{}" title="{}">{}</a>'.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 = '<a href="{}" title="{}">{}</a>'.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 = ['<a href="{}" title="{}">{}</a>'.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('<a href="{}" title="{}">{}</a>'.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 <a href="%(href)s" title="%(tt)s">'
'<span class="%(cls)s">%(series)s</span></a>') % 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 <a href="%(href)s" title="%(tt)s">'
'<span class="%(cls)s">%(series)s</span></a>') % 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 <a href="%(href)s" title="%(tt)s">'
'<span class="%(cls)s">%(series)s</span></a>') % 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 = '<a href="{}" title="{}">{}</a>'.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 = '<a href="{}" title="{}">{}</a>'.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 = '<a href="{}" title="{}">{}</a>'.format(u, v, p(x))
else:
v = '<a href="{}" title="{}">{}</a>'.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 = '<a href="{}" title="{}">{}</a>'.format(u, v, p(x))
else:
v = '<a href="{}" title="{}">{}</a>'.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 = '<a href="{}" title="{}">{}</a>'.format(u, v, p(x)) + add_other_links(field, val)
elif metadata['datatype'] == 'bool':
val = '<a href="{}" title="{}">{}</a>'.format(
search_action_with_data(field, val, book_id, None), a(

View File

@ -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':

View File

@ -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('<p>' + _('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.') + '</p>')
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('<p>' + _(
"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.") + '</p>')
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.') + '</p>'
)
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] = {

View File

@ -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