mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
b3aafddf5a
@ -116,6 +116,16 @@ If you perform a search in calibre and want to generate a link for it you can
|
||||
do so by right clicking the search bar and choosing :guilabel:`Copy search as
|
||||
URL`.
|
||||
|
||||
Open a book details window on a book in some library
|
||||
------------------------------------------------------
|
||||
|
||||
The URL syntax is::
|
||||
|
||||
calibre://book-details/Library_Name/book_id
|
||||
|
||||
This opens a book details window on the specified book from the specified library without changing the
|
||||
current library or the selected book.
|
||||
|
||||
|
||||
.. _hex_encoding:
|
||||
|
||||
|
@ -1121,6 +1121,7 @@ class DB:
|
||||
CREATE TABLE %s(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
value %s NOT NULL %s,
|
||||
link TEXT NOT NULL DEFAULT "",
|
||||
UNIQUE(value));
|
||||
'''%(table, dt, collate),
|
||||
|
||||
|
@ -153,6 +153,7 @@ class Cache:
|
||||
self.format_metadata_cache = defaultdict(dict)
|
||||
self.formatter_template_cache = {}
|
||||
self.dirtied_cache = {}
|
||||
self.link_maps_cache = {}
|
||||
self.vls_for_books_cache = None
|
||||
self.vls_for_books_lib_in_process = None
|
||||
self.vls_cache_lock = Lock()
|
||||
@ -290,6 +291,14 @@ class Cache:
|
||||
self.format_metadata_cache.clear()
|
||||
if search_cache:
|
||||
self._clear_search_caches(book_ids)
|
||||
self.clear_link_map_cache(book_ids)
|
||||
|
||||
def clear_link_map_cache(self, book_ids=None):
|
||||
if book_ids is None:
|
||||
self.link_maps_cache = {}
|
||||
else:
|
||||
for book in book_ids:
|
||||
self.link_maps_cache.pop(book, None)
|
||||
|
||||
@write_api
|
||||
def reload_from_db(self, clear_caches=True):
|
||||
@ -382,6 +391,8 @@ class Cache:
|
||||
for key in composites:
|
||||
mi.set(key, val=self._composite_for(key, book_id, mi))
|
||||
|
||||
mi.link_maps = self.get_all_link_maps_for_book(book_id)
|
||||
|
||||
user_cat_vals = {}
|
||||
if get_user_categories:
|
||||
user_cats = self._pref('user_categories', {})
|
||||
@ -1483,6 +1494,7 @@ class Cache:
|
||||
if update_path and do_path_update:
|
||||
self._update_path(dirtied, mark_as_dirtied=False)
|
||||
self._mark_as_dirty(dirtied)
|
||||
self.clear_link_map_cache(dirtied)
|
||||
self.event_dispatcher(EventType.metadata_changed, name, dirtied)
|
||||
return dirtied
|
||||
|
||||
@ -1498,6 +1510,7 @@ class Cache:
|
||||
self.format_metadata_cache.pop(book_id, None)
|
||||
if mark_as_dirtied:
|
||||
self._mark_as_dirty(book_ids)
|
||||
self.clear_link_map_cache(book_ids)
|
||||
|
||||
@read_api
|
||||
def get_a_dirtied_book(self):
|
||||
@ -2157,6 +2170,7 @@ class Cache:
|
||||
for book_id in moved_books:
|
||||
self._set_field(f.index_field.name, {book_id:self._get_next_series_num_for(self._fast_field_for(f, book_id), field=field)})
|
||||
self._mark_as_dirty(affected_books)
|
||||
self.clear_link_map_cache(affected_books)
|
||||
self.event_dispatcher(EventType.items_renamed, field, affected_books, id_map)
|
||||
return affected_books, id_map
|
||||
|
||||
@ -2176,6 +2190,7 @@ class Cache:
|
||||
self._set_field(field.index_field.name, {bid:1.0 for bid in affected_books})
|
||||
else:
|
||||
self._mark_as_dirty(affected_books)
|
||||
self.clear_link_map_cache(affected_books)
|
||||
self.event_dispatcher(EventType.items_removed, field, affected_books, item_ids)
|
||||
return affected_books
|
||||
|
||||
@ -2310,6 +2325,7 @@ class Cache:
|
||||
self._set_field('author_sort', val_map)
|
||||
if changed_books:
|
||||
self._mark_as_dirty(changed_books)
|
||||
self.clear_link_map_cache(changed_books)
|
||||
return changed_books
|
||||
|
||||
@write_api
|
||||
@ -2320,6 +2336,100 @@ class Cache:
|
||||
changed_books |= self._books_for_field('authors', author_id)
|
||||
if changed_books:
|
||||
self._mark_as_dirty(changed_books)
|
||||
self.clear_link_map_cache(changed_books)
|
||||
return changed_books
|
||||
|
||||
@read_api
|
||||
def has_link_map(self, field):
|
||||
if field not in self.fields:
|
||||
raise ValueError(f'Lookup name {field} is not a valid name')
|
||||
table = self.fields[field].table
|
||||
return hasattr(table, 'link_map')
|
||||
|
||||
@read_api
|
||||
def get_link_map(self, for_field):
|
||||
'''
|
||||
Return a dict of links for the supplied field.
|
||||
|
||||
field: the lookup name of the field for which the link map is desired
|
||||
|
||||
returns {field_value:link_value, ...} for non-empty links
|
||||
'''
|
||||
if for_field not in self.fields:
|
||||
raise ValueError(f'Lookup name {for_field} is not a valid name')
|
||||
table = self.fields[for_field].table
|
||||
if not hasattr(table, 'link_map'):
|
||||
raise ValueError(f"Lookup name {for_field} doesn't have a link map")
|
||||
lm = table.link_map
|
||||
vm = table.id_map
|
||||
return dict({vm.get(fid, None):v for fid,v in lm.items() if v})
|
||||
|
||||
@read_api
|
||||
def get_all_link_maps_for_book(self, book_id):
|
||||
'''
|
||||
Returns all links for all fields referenced by book identified by book_id
|
||||
|
||||
book_id: the book id in question.
|
||||
|
||||
returns:
|
||||
{field: {field_value, link_value}, ...
|
||||
for all fields that have a non-empty link value for that book
|
||||
|
||||
Example: Assume author A has link X, author B has link Y, tag S has link
|
||||
F, and tag T has link G. IF book 1 has author A and
|
||||
tag T, this method returns {'authors':{'A':'X'}, 'tags':{'T', 'G'}}
|
||||
If book 2's author is neither A nor B and has no tags, this
|
||||
method returns {}
|
||||
'''
|
||||
if book_id in self.link_maps_cache:
|
||||
return self.link_maps_cache[book_id]
|
||||
links = {}
|
||||
def add_links_for_field(f):
|
||||
field_ids = frozenset(self.field_ids_for(f, book_id))
|
||||
table = self.fields[f].table
|
||||
lm = table.link_map
|
||||
vm = table.id_map
|
||||
d = dict({vm.get(fid, None):v for fid,v in lm.items() if v and fid in field_ids})
|
||||
if d:
|
||||
links[f] = d
|
||||
for field in ('authors', 'publisher', 'series', 'tags'):
|
||||
add_links_for_field(field)
|
||||
for field in self.field_metadata.custom_field_keys(include_composites=False):
|
||||
if self.has_link_map(field):
|
||||
add_links_for_field(field)
|
||||
self.link_maps_cache[book_id] = links
|
||||
return links
|
||||
|
||||
@write_api
|
||||
def set_link_map(self, field, value_to_link_map):
|
||||
'''
|
||||
Sets links for item values in field
|
||||
|
||||
field: the lookup name
|
||||
value_to_link_map: dict(field_value:link, ...). Note that these are
|
||||
values, not field ids.
|
||||
|
||||
returns books changed by setting the link
|
||||
|
||||
NB: this method doesn't change values not in the value_to_link_map
|
||||
'''
|
||||
if field not in self.fields:
|
||||
raise ValueError(f'Lookup name {field} is not a valid name')
|
||||
table = self.fields[field].table
|
||||
if not hasattr(table, 'link_map'):
|
||||
raise ValueError(f"Lookup name {field} doesn't have a link map")
|
||||
# Clear the links for book cache as we don't know what will be affected
|
||||
self.link_maps_cache = {}
|
||||
|
||||
fids = {k: self.get_item_id(field, k) for k in value_to_link_map.keys()}
|
||||
id_to_link_map = {fid:value_to_link_map[k] for k, fid in fids.items() if fid is not None}
|
||||
result_map = table.set_links(id_to_link_map, self.backend)
|
||||
changed_books = set()
|
||||
for id_ in result_map:
|
||||
changed_books |= self._books_for_field(field, id_)
|
||||
if changed_books:
|
||||
self._mark_as_dirty(changed_books)
|
||||
self.clear_link_map_cache(changed_books)
|
||||
return changed_books
|
||||
|
||||
@read_api
|
||||
|
@ -642,7 +642,7 @@ class AuthorsField(ManyToManyField):
|
||||
return {
|
||||
'name': self.table.id_map[author_id],
|
||||
'sort': self.table.asort_map[author_id],
|
||||
'link': self.table.alink_map[author_id],
|
||||
'link': self.table.link_map[author_id],
|
||||
}
|
||||
|
||||
def category_sort_value(self, item_id, book_ids, lang_map):
|
||||
|
@ -351,6 +351,10 @@ class ProxyMetadata(Metadata):
|
||||
return custom_getter(field, ga(self, '_db'), ga(self, '_book_id'), ga(self, '_cache'))
|
||||
return composite_getter(self, field, ga(self, '_db'), ga(self, '_book_id'), ga(self, '_cache'), ga(self, 'formatter'), ga(self, 'template_cache'))
|
||||
|
||||
if field == 'link_maps':
|
||||
db = ga(self, '_db')()
|
||||
return db.get_all_link_maps_for_book(ga(self, '_book_id'))
|
||||
|
||||
try:
|
||||
return ga(self, '_cache')[field]
|
||||
except KeyError:
|
||||
|
@ -793,3 +793,31 @@ CREATE TRIGGER fkc_annot_update
|
||||
|
||||
def upgrade_version_24(self):
|
||||
self.db.reindex_annotations()
|
||||
|
||||
def upgrade_version_25(self):
|
||||
alters = []
|
||||
for record in self.db.execute(
|
||||
'SELECT label,name,datatype,editable,display,normalized,id,is_multiple FROM custom_columns'):
|
||||
data = {
|
||||
'label':record[0],
|
||||
'name':record[1],
|
||||
'datatype':record[2],
|
||||
'editable':bool(record[3]),
|
||||
'display':record[4],
|
||||
'normalized':bool(record[5]),
|
||||
'num':record[6],
|
||||
'is_multiple':bool(record[7]),
|
||||
}
|
||||
if data['normalized']:
|
||||
tn = 'custom_column_{}'.format(data['num'])
|
||||
alters.append(f'ALTER TABLE {tn} ADD COLUMN link TEXT NOT NULL DEFAULT "";')
|
||||
|
||||
alters.append('ALTER TABLE publishers ADD COLUMN link TEXT NOT NULL DEFAULT "";')
|
||||
alters.append('ALTER TABLE series ADD COLUMN link TEXT NOT NULL DEFAULT "";')
|
||||
alters.append('ALTER TABLE tags ADD COLUMN link TEXT NOT NULL DEFAULT "";')
|
||||
# These aren't necessary in that there is no UI to set links, but having them
|
||||
# makes the code uniform
|
||||
alters.append('ALTER TABLE languages ADD COLUMN link TEXT NOT NULL DEFAULT "";')
|
||||
alters.append('ALTER TABLE ratings ADD COLUMN link TEXT NOT NULL DEFAULT "";')
|
||||
self.db.execute('\n'.join(alters))
|
||||
|
||||
|
@ -199,19 +199,22 @@ class ManyToOneTable(Table):
|
||||
|
||||
def read(self, db):
|
||||
self.id_map = {}
|
||||
self.link_map = {}
|
||||
self.col_book_map = defaultdict(set)
|
||||
self.book_col_map = {}
|
||||
self.read_id_maps(db)
|
||||
self.read_maps(db)
|
||||
|
||||
def read_id_maps(self, db):
|
||||
query = db.execute('SELECT id, {} FROM {}'.format(
|
||||
query = db.execute('SELECT id, {}, link FROM {}'.format(
|
||||
self.metadata['column'], self.metadata['table']))
|
||||
if self.unserialize is None:
|
||||
self.id_map = dict(query)
|
||||
us = lambda x: x
|
||||
else:
|
||||
us = self.unserialize
|
||||
self.id_map = {book_id:us(val) for book_id, val in query}
|
||||
for id_, val, link in query:
|
||||
self.id_map[id_] = us(val)
|
||||
self.link_map[id_] = link
|
||||
|
||||
def read_maps(self, db):
|
||||
cbm = self.col_book_map
|
||||
@ -343,6 +346,14 @@ class ManyToOneTable(Table):
|
||||
self.link_table, lcol, table), (existing_item, item_id, item_id))
|
||||
return affected_books, new_id
|
||||
|
||||
def set_links(self, link_map, db):
|
||||
link_map = {id_:(l or '').strip() for id_, l in iteritems(link_map)}
|
||||
link_map = {id_:l for id_, l in iteritems(link_map) if l != self.link_map.get(id_)}
|
||||
self.link_map.update(link_map)
|
||||
db.executemany(f'UPDATE {self.metadata["table"]} SET link=? WHERE id=?',
|
||||
[(v, k) for k, v in iteritems(link_map)])
|
||||
return link_map
|
||||
|
||||
|
||||
class RatingTable(ManyToOneTable):
|
||||
|
||||
@ -526,7 +537,7 @@ class ManyToManyTable(ManyToOneTable):
|
||||
class AuthorsTable(ManyToManyTable):
|
||||
|
||||
def read_id_maps(self, db):
|
||||
self.alink_map = lm = {}
|
||||
self.link_map = lm = {}
|
||||
self.asort_map = sm = {}
|
||||
self.id_map = im = {}
|
||||
us = self.unserialize
|
||||
@ -547,8 +558,8 @@ class AuthorsTable(ManyToManyTable):
|
||||
|
||||
def set_links(self, link_map, db):
|
||||
link_map = {aid:(l or '').strip() for aid, l in iteritems(link_map)}
|
||||
link_map = {aid:l for aid, l in iteritems(link_map) if l != self.alink_map.get(aid, None)}
|
||||
self.alink_map.update(link_map)
|
||||
link_map = {aid:l for aid, l in iteritems(link_map) if l != self.link_map.get(aid, None)}
|
||||
self.link_map.update(link_map)
|
||||
db.executemany('UPDATE authors SET link=? WHERE id=?',
|
||||
[(v, k) for k, v in iteritems(link_map)])
|
||||
return link_map
|
||||
@ -556,14 +567,14 @@ class AuthorsTable(ManyToManyTable):
|
||||
def remove_books(self, book_ids, db):
|
||||
clean = ManyToManyTable.remove_books(self, book_ids, db)
|
||||
for item_id in clean:
|
||||
self.alink_map.pop(item_id, None)
|
||||
self.link_map.pop(item_id, None)
|
||||
self.asort_map.pop(item_id, None)
|
||||
return clean
|
||||
|
||||
def rename_item(self, item_id, new_name, db):
|
||||
ret = ManyToManyTable.rename_item(self, item_id, new_name, db)
|
||||
if item_id not in self.id_map:
|
||||
self.alink_map.pop(item_id, None)
|
||||
self.link_map.pop(item_id, None)
|
||||
self.asort_map.pop(item_id, None)
|
||||
else:
|
||||
# Was a simple rename, update the author sort value
|
||||
|
@ -225,7 +225,7 @@ class AddRemoveTest(BaseTest):
|
||||
self.assertNotIn(3, c.all_book_ids())
|
||||
self.assertNotIn('Unknown', set(itervalues(table.id_map)))
|
||||
self.assertNotIn(item_id, table.asort_map)
|
||||
self.assertNotIn(item_id, table.alink_map)
|
||||
self.assertNotIn(item_id, table.link_map)
|
||||
ae(len(table.id_map), olen-1)
|
||||
|
||||
# Check that files are removed
|
||||
|
@ -919,4 +919,62 @@ class WritingTest(BaseTest):
|
||||
ae(cache.field_for('series_index', 1), 2.0)
|
||||
ae(cache.field_for('series_index', 2), 3.5)
|
||||
|
||||
def test_link_maps(self):
|
||||
cache = self.init_cache()
|
||||
|
||||
# Add two tags
|
||||
cache.set_field('tags', {1:'foo'})
|
||||
self.assertEqual(('foo',), cache.field_for('tags', 1), 'Setting tag foo failed')
|
||||
cache.set_field('tags', {1:'foo, bar'})
|
||||
self.assertEqual(('foo', 'bar'), cache.field_for('tags', 1), 'Adding second tag failed')
|
||||
|
||||
# Check adding a link
|
||||
links = cache.get_link_map('tags')
|
||||
self.assertDictEqual(links, {}, 'Initial tags link dict is not empty')
|
||||
links['foo'] = 'url'
|
||||
cache.set_link_map('tags', links)
|
||||
links2 = cache.get_link_map('tags')
|
||||
self.assertDictEqual(links2, links, 'tags link dict mismatch')
|
||||
|
||||
# Check getting links for a book and that links are correct
|
||||
cache.set_field('publisher', {1:'random'})
|
||||
cache.set_link_map('publisher', {'random': 'url2'})
|
||||
links = cache.get_all_link_maps_for_book(1)
|
||||
self.assertSetEqual({v for v in links.keys()}, {'tags', 'publisher'}, 'Wrong link keys')
|
||||
self.assertSetEqual({v for v in links['tags'].keys()}, {'foo', }, 'Should be "foo"')
|
||||
self.assertSetEqual({v for v in links['publisher'].keys()}, {'random', }, 'Should be "random"')
|
||||
self.assertEqual('url', links['tags']['foo'], 'link for tag foo is wrong')
|
||||
self.assertEqual('url2', links['publisher']['random'], 'link for publisher random is wrong')
|
||||
|
||||
# Check that renaming a tag keeps the link and clears the link map cache for the book
|
||||
self.assertTrue(1 in cache.link_maps_cache, "book not in link_map_cache")
|
||||
tag_id = cache.get_item_id('tags', 'foo')
|
||||
cache.rename_items('tags', {tag_id: 'foobar'})
|
||||
self.assertTrue(1 not in cache.link_maps_cache, "book still in link_map_cache")
|
||||
links = cache.get_link_map('tags')
|
||||
self.assertTrue('foobar' in links, "rename foo lost the link")
|
||||
self.assertEqual(links['foobar'], 'url', "The link changed contents")
|
||||
links = cache.get_all_link_maps_for_book(1)
|
||||
self.assertTrue(1 in cache.link_maps_cache, "book not put back into link_map_cache")
|
||||
self.assertDictEqual({'publisher': {'random': 'url2'}, 'tags': {'foobar': 'url'}},
|
||||
links, "book links incorrect after tag rename")
|
||||
|
||||
# Check ProxyMetadata
|
||||
mi = cache.get_proxy_metadata(1)
|
||||
self.assertDictEqual({'publisher': {'random': 'url2'}, 'tags': {'foobar': 'url'}},
|
||||
mi.link_maps, "ProxyMetadata didn't return the right link map")
|
||||
|
||||
|
||||
# Now test deleting the links.
|
||||
links = cache.get_link_map('tags')
|
||||
to_del = {l:'' for l in links.keys()}
|
||||
cache.set_link_map('tags', to_del)
|
||||
self.assertEqual({}, cache.get_link_map('tags'), 'links on tags were not deleted')
|
||||
links = cache.get_link_map('publisher')
|
||||
to_del = {l:'' for l in links.keys()}
|
||||
cache.set_link_map('publisher', to_del)
|
||||
self.assertEqual({}, cache.get_link_map('publisher'), 'links on publisher were not deleted')
|
||||
self.assertEqual({}, cache.get_all_link_maps_for_book(1), 'Not all links for book were deleted')
|
||||
|
||||
|
||||
# }}}
|
||||
|
@ -295,7 +295,8 @@ def get_db_id(val, db, m, table, kmap, rid_map, allow_case_change,
|
||||
table.col_book_map[item_id] = set()
|
||||
if is_authors:
|
||||
table.asort_map[item_id] = aus
|
||||
table.alink_map[item_id] = ''
|
||||
if hasattr(table, 'link_map'):
|
||||
table.link_map[item_id] = ''
|
||||
elif allow_case_change and val != table.id_map[item_id]:
|
||||
case_changes[item_id] = val
|
||||
val_map[val] = item_id
|
||||
@ -491,7 +492,8 @@ def many_many(book_id_val_map, db, field, allow_case_change, *args):
|
||||
table.col_book_map.pop(item_id, None)
|
||||
if is_authors:
|
||||
table.asort_map.pop(item_id, None)
|
||||
table.alink_map.pop(item_id, None)
|
||||
if hasattr(table, 'link_map'):
|
||||
table.link_map.pop(item_id, None)
|
||||
|
||||
return dirtied
|
||||
|
||||
|
@ -91,6 +91,24 @@ def mi_to_html(
|
||||
rating_font='Liberation Serif', rtl=False, comments_heading_pos='hide',
|
||||
for_qt=False, vertical_fields=()
|
||||
):
|
||||
|
||||
show_links = not hasattr(mi, '_bd_dbwref')
|
||||
|
||||
def get_link_map(column):
|
||||
if not show_links:
|
||||
return {}
|
||||
try:
|
||||
return mi.link_maps[column]
|
||||
except (KeyError, ValueError):
|
||||
return {column:{}}
|
||||
|
||||
def add_other_link(field, field_value):
|
||||
link = get_link_map(field).get(field_value, None)
|
||||
if link:
|
||||
return (' <a title="%s" href="%s">%s</a>'%(_('Click to open {}').format(link), link, _('(item link)')))
|
||||
else:
|
||||
return ''
|
||||
|
||||
if field_list is None:
|
||||
field_list = get_field_list(mi)
|
||||
ans = []
|
||||
@ -170,9 +188,12 @@ def mi_to_html(
|
||||
else:
|
||||
all_vals = [v.strip()
|
||||
for v in val.split(metadata['is_multiple']['cache_to_list']) if v.strip()]
|
||||
links = ['<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action(field, x), _('Click to see books with {0}: {1}').format(
|
||||
if show_links:
|
||||
links = ['<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action(field, x), _('Click to see books with {0}: {1}').format(
|
||||
metadata['name'], a(x)), p(x)) for x in all_vals]
|
||||
else:
|
||||
links = all_vals
|
||||
val = value_list(metadata['is_multiple']['list_to_ui'], links)
|
||||
ans.append((field, row % (name, val)))
|
||||
elif field == 'path':
|
||||
@ -188,10 +209,15 @@ def mi_to_html(
|
||||
durl = ':::'.join((durl.split(':::'))[2:])
|
||||
extra = '<br><span style="font-size:smaller">%s</span>'%(
|
||||
prepare_string_for_xml(durl))
|
||||
link = '<a href="{}" title="{}">{}</a>{}'.format(action(scheme, loc=loc),
|
||||
if show_links:
|
||||
link = '<a href="{}" title="{}">{}</a>{}'.format(action(scheme, loc=loc),
|
||||
prepare_string_for_xml(path, True), pathstr, extra)
|
||||
else:
|
||||
link = prepare_string_for_xml(path, True)
|
||||
ans.append((field, row % (name, link)))
|
||||
elif field == 'formats':
|
||||
# Don't need show_links here because formats are removed from mi on
|
||||
# cross library displays.
|
||||
if isdevice:
|
||||
continue
|
||||
path = mi.path or ''
|
||||
@ -209,12 +235,15 @@ def mi_to_html(
|
||||
ans.append((field, row % (name, value_list(', ', fmts))))
|
||||
elif field == 'identifiers':
|
||||
urls = urls_from_identifiers(mi.identifiers, sort_results=True)
|
||||
links = [
|
||||
'<a href="{}" title="{}:{}">{}</a>'.format(
|
||||
action('identifier', url=url, name=namel, id_type=id_typ, value=id_val, field='identifiers', book_id=book_id),
|
||||
a(id_typ), a(id_val), p(namel))
|
||||
for namel, id_typ, id_val, url in urls]
|
||||
links = value_list(', ', links)
|
||||
if show_links:
|
||||
links = [
|
||||
'<a href="{}" title="{}:{}">{}</a>'.format(
|
||||
action('identifier', url=url, name=namel, id_type=id_typ, value=id_val, field='identifiers', book_id=book_id),
|
||||
a(id_typ), a(id_val), p(namel))
|
||||
for namel, id_typ, id_val, url in urls]
|
||||
links = value_list(', ', links)
|
||||
else:
|
||||
links = ', '.join(mi.identifiers)
|
||||
if links:
|
||||
ans.append((field, row % (_('Ids')+':', links)))
|
||||
elif field == 'authors':
|
||||
@ -222,39 +251,46 @@ def mi_to_html(
|
||||
formatter = EvalFormatter()
|
||||
for aut in mi.authors:
|
||||
link = ''
|
||||
if mi.author_link_map.get(aut):
|
||||
link = lt = mi.author_link_map[aut]
|
||||
elif default_author_link:
|
||||
if default_author_link.startswith('search-'):
|
||||
which_src = default_author_link.partition('-')[2]
|
||||
link, lt = author_search_href(which_src, title=mi.title, author=aut)
|
||||
if show_links:
|
||||
if default_author_link:
|
||||
if default_author_link.startswith('search-'):
|
||||
which_src = default_author_link.partition('-')[2]
|
||||
link, lt = author_search_href(which_src, title=mi.title, author=aut)
|
||||
else:
|
||||
vals = {'author': qquote(aut), 'title': qquote(mi.title)}
|
||||
try:
|
||||
vals['author_sort'] = qquote(mi.author_sort_map[aut])
|
||||
except KeyError:
|
||||
vals['author_sort'] = qquote(aut)
|
||||
link = lt = formatter.safe_format(default_author_link, vals, '', vals)
|
||||
else:
|
||||
vals = {'author': qquote(aut), 'title': qquote(mi.title)}
|
||||
try:
|
||||
vals['author_sort'] = qquote(mi.author_sort_map[aut])
|
||||
except KeyError:
|
||||
vals['author_sort'] = qquote(aut)
|
||||
link = lt = formatter.safe_format(default_author_link, vals, '', vals)
|
||||
aut = p(aut)
|
||||
aut = p(aut)
|
||||
if link:
|
||||
authors.append('<a title="%s" href="%s">%s</a>'%(a(lt), action('author', url=link, name=aut, title=lt), aut))
|
||||
val = '<a title="%s" href="%s">%s</a>'%(a(lt), action('author', url=link, name=aut, title=lt), aut)
|
||||
else:
|
||||
authors.append(aut)
|
||||
val = aut
|
||||
val += add_other_link('authors', aut)
|
||||
authors.append(val)
|
||||
ans.append((field, row % (name, value_list(' & ', authors))))
|
||||
elif field == 'languages':
|
||||
if not mi.languages:
|
||||
continue
|
||||
names = filter(None, map(calibre_langcode_to_name, mi.languages))
|
||||
names = ['<a href="{}" title="{}">{}</a>'.format(search_action_with_data('languages', n, book_id), _(
|
||||
'Search calibre for books with the language: {}').format(n), n) for n in names]
|
||||
if show_links:
|
||||
names = ['<a href="{}" title="{}">{}</a>'.format(search_action_with_data('languages', n, book_id), _(
|
||||
'Search calibre for books with the language: {}').format(n), n) for n in names]
|
||||
ans.append((field, row % (name, value_list(', ', names))))
|
||||
elif field == 'publisher':
|
||||
if not mi.publisher:
|
||||
continue
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data('publisher', mi.publisher, book_id),
|
||||
_('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)),
|
||||
p(mi.publisher))
|
||||
if show_links:
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data('publisher', mi.publisher, book_id),
|
||||
_('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)),
|
||||
p(mi.publisher))
|
||||
val += add_other_link('publisher', mi.publisher)
|
||||
else:
|
||||
val = p(mi.publisher)
|
||||
ans.append((field, row % (name, val)))
|
||||
elif field == 'title':
|
||||
# otherwise title gets metadata['datatype'] == 'text'
|
||||
@ -268,79 +304,85 @@ def mi_to_html(
|
||||
if val is None:
|
||||
continue
|
||||
val = p(val)
|
||||
if metadata['datatype'] == 'series':
|
||||
sidx = mi.get(field+'_index')
|
||||
if sidx is None:
|
||||
sidx = 1.0
|
||||
try:
|
||||
st = metadata['search_terms'][0]
|
||||
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')))
|
||||
elif metadata['datatype'] == 'datetime':
|
||||
aval = getattr(mi, field)
|
||||
if is_date_undefined(aval):
|
||||
continue
|
||||
aval = format_date(aval, 'yyyy-MM-dd')
|
||||
key = field if field != 'timestamp' else 'date'
|
||||
if val == aval:
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data(key, str(aval), book_id, None, original_value=val), a(
|
||||
_('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val)
|
||||
else:
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
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 = ['<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))
|
||||
for x in all_vals]
|
||||
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
|
||||
val = '<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)
|
||||
elif metadata['datatype'] == 'bool':
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data(field, val, book_id, None), a(
|
||||
_('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val)
|
||||
else:
|
||||
try:
|
||||
aval = str(getattr(mi, field))
|
||||
if not aval:
|
||||
if show_links:
|
||||
if metadata['datatype'] == 'series':
|
||||
sidx = mi.get(field+'_index')
|
||||
if sidx is None:
|
||||
sidx = 1.0
|
||||
try:
|
||||
st = metadata['search_terms'][0]
|
||||
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')))
|
||||
val += add_other_link('series', mi.series)
|
||||
elif metadata['datatype'] == 'datetime':
|
||||
aval = getattr(mi, field)
|
||||
if is_date_undefined(aval):
|
||||
continue
|
||||
aval = format_date(aval, 'yyyy-MM-dd')
|
||||
key = field if field != 'timestamp' else 'date'
|
||||
if val == aval:
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data(field, str(aval), book_id, None, original_value=val), a(
|
||||
search_action_with_data(key, str(aval), book_id, None, original_value=val), a(
|
||||
_('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val)
|
||||
else:
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data(field, str(aval), book_id, None, original_value=val), a(
|
||||
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)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
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_link(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_link(field, val)
|
||||
elif metadata['datatype'] == 'bool':
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data(field, val, book_id, None), a(
|
||||
_('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val)
|
||||
else:
|
||||
try:
|
||||
aval = str(getattr(mi, field))
|
||||
if not aval:
|
||||
continue
|
||||
if val == aval:
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data(field, str(aval), book_id, None, original_value=val), a(
|
||||
_('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val)
|
||||
else:
|
||||
val = '<a href="{}" title="{}">{}</a>'.format(
|
||||
search_action_with_data(field, 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)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
ans.append((field, row % (name, val)))
|
||||
|
||||
|
@ -595,6 +595,8 @@ class OPF: # {{{
|
||||
renderer=dump_dict)
|
||||
author_link_map = MetadataField('author_link_map', is_dc=False,
|
||||
formatter=json.loads, renderer=dump_dict)
|
||||
link_map = MetadataField('link_maps', is_dc=False,
|
||||
formatter=json.loads, renderer=dump_dict)
|
||||
|
||||
def __init__(self, stream, basedir=os.getcwd(), unquote_urls=True,
|
||||
populate_spine=True, try_to_guess_cover=False, preparsed_opf=None, read_toc=True):
|
||||
@ -1310,7 +1312,7 @@ class OPF: # {{{
|
||||
for attr in ('title', 'authors', 'author_sort', 'title_sort',
|
||||
'publisher', 'series', 'series_index', 'rating',
|
||||
'isbn', 'tags', 'category', 'comments', 'book_producer',
|
||||
'pubdate', 'user_categories', 'author_link_map'):
|
||||
'pubdate', 'user_categories', 'author_link_map', 'link_map'):
|
||||
val = getattr(mi, attr, None)
|
||||
if attr == 'rating' and val:
|
||||
val = float(val)
|
||||
@ -1673,6 +1675,8 @@ def metadata_to_opf(mi, as_string=True, default_lang=None):
|
||||
return factory('meta', name='calibre:' + n, content=c)
|
||||
if getattr(mi, 'author_link_map', None) is not None:
|
||||
meta('author_link_map', dump_dict(mi.author_link_map))
|
||||
if getattr(mi, 'link_maps', None) is not None:
|
||||
meta('link_maps', dump_dict(mi.link_maps))
|
||||
if mi.series:
|
||||
meta('series', mi.series)
|
||||
if mi.series_index is not None:
|
||||
|
@ -23,25 +23,61 @@ class ShowBookDetailsAction(InterfaceAction):
|
||||
def genesis(self):
|
||||
self.qaction.triggered.connect(self.show_book_info)
|
||||
self.memory = []
|
||||
self.dialogs = [None, ]
|
||||
|
||||
def show_book_info(self, *args):
|
||||
def show_book_info(self, *args, **kwargs):
|
||||
if self.gui.current_view() is not self.gui.library_view:
|
||||
error_dialog(self.gui, _('No detailed info available'),
|
||||
_('No detailed information is available for books '
|
||||
'on the device.')).exec()
|
||||
return
|
||||
library_path = kwargs.get('library_path', None)
|
||||
book_id = kwargs.get('book_id', None)
|
||||
library_id = kwargs.get('library_id', None)
|
||||
query = kwargs.get('query', None)
|
||||
index = self.gui.library_view.currentIndex()
|
||||
if index.isValid():
|
||||
d = BookInfo(self.gui, self.gui.library_view, index,
|
||||
self.gui.book_details.handle_click)
|
||||
if library_path or index.isValid():
|
||||
# Window #0 is slaved to changes in the book list. As such
|
||||
# it must not be used for details from other libraries.
|
||||
for dn,v in enumerate(self.dialogs):
|
||||
if dn == 0 and library_path:
|
||||
continue
|
||||
if v is None:
|
||||
break
|
||||
else:
|
||||
self.dialogs.append(None)
|
||||
dn += 1
|
||||
|
||||
try:
|
||||
d = BookInfo(self.gui, self.gui.library_view, index,
|
||||
self.gui.book_details.handle_click, dialog_number=dn,
|
||||
library_id=library_id, library_path=library_path, book_id=book_id, query=query)
|
||||
except ValueError as e:
|
||||
error_dialog(self.gui, _('Book not found'), str(e)).exec()
|
||||
return
|
||||
|
||||
d.open_cover_with.connect(self.gui.bd_open_cover_with, type=Qt.ConnectionType.QueuedConnection)
|
||||
self.dialogs[dn] = d
|
||||
self.memory.append(d)
|
||||
d.closed.connect(self.closed, type=Qt.ConnectionType.QueuedConnection)
|
||||
d.show()
|
||||
|
||||
def shutting_down(self):
|
||||
for d in self.dialogs:
|
||||
if d:
|
||||
d.done(0)
|
||||
|
||||
def library_about_to_change(self, *args):
|
||||
for i,d in enumerate(self.dialogs):
|
||||
if i == 0:
|
||||
continue
|
||||
if d:
|
||||
d.done(0)
|
||||
|
||||
def closed(self, d):
|
||||
try:
|
||||
d.closed.disconnect(self.closed)
|
||||
self.dialogs[d.dialog_number] = None
|
||||
self.memory.remove(d)
|
||||
except ValueError:
|
||||
pass
|
||||
|
@ -42,12 +42,15 @@ InternetSearch = namedtuple('InternetSearch', 'author where')
|
||||
|
||||
|
||||
def set_html(mi, html, text_browser):
|
||||
from calibre.gui2.ui import get_gui
|
||||
gui = get_gui()
|
||||
if hasattr(mi, '_bd_dbwref') and mi._bd_dbwref is not None:
|
||||
db = mi._bd_dbwref
|
||||
else:
|
||||
from calibre.gui2.ui import get_gui
|
||||
db = get_gui().current_db
|
||||
book_id = getattr(mi, 'id', None)
|
||||
search_paths = []
|
||||
if gui and book_id is not None:
|
||||
path = gui.current_db.abspath(book_id, index_is_id=True)
|
||||
if db and book_id is not None:
|
||||
path = db.abspath(book_id, index_is_id=True)
|
||||
if path:
|
||||
search_paths = [path]
|
||||
text_browser.setSearchPaths(search_paths)
|
||||
@ -203,10 +206,16 @@ def comments_pat():
|
||||
return re.compile(r'<!--.*?-->', re.DOTALL)
|
||||
|
||||
|
||||
def render_html(mi, vertical, widget, all_fields=False, render_data_func=None, pref_name='book_display_fields'): # {{{
|
||||
from calibre.gui2.ui import get_gui
|
||||
def render_html(mi, vertical, widget, all_fields=False, render_data_func=None,
|
||||
pref_name='book_display_fields',
|
||||
pref_value=None): # {{{
|
||||
if hasattr(mi, '_bd_dbwref') and mi._bd_dbwref is not None:
|
||||
db = mi._bd_dbwref
|
||||
else:
|
||||
from calibre.gui2.ui import get_gui
|
||||
db = get_gui().current_db
|
||||
func = render_data_func or partial(render_data,
|
||||
vertical_fields=get_gui().current_db.prefs.get('book_details_vertical_categories') or ())
|
||||
vertical_fields=db.prefs.get('book_details_vertical_categories') or ())
|
||||
try:
|
||||
table, comment_fields = func(mi, all_fields=all_fields,
|
||||
use_roman_numbers=config['use_roman_numerals_for_series_number'], pref_name=pref_name)
|
||||
@ -246,9 +255,12 @@ def render_html(mi, vertical, widget, all_fields=False, render_data_func=None, p
|
||||
return ans
|
||||
|
||||
|
||||
def get_field_list(fm, use_defaults=False, pref_name='book_display_fields'):
|
||||
from calibre.gui2.ui import get_gui
|
||||
db = get_gui().current_db
|
||||
def get_field_list(fm, use_defaults=False, pref_name='book_display_fields', mi=None):
|
||||
if mi is not None and hasattr(mi, '_bd_dbwref') and mi._bd_dbwref is not None:
|
||||
db = mi._bd_dbwref
|
||||
else:
|
||||
from calibre.gui2.ui import get_gui
|
||||
db = get_gui().current_db
|
||||
if use_defaults:
|
||||
src = db.prefs.defaults
|
||||
else:
|
||||
@ -267,7 +279,8 @@ def get_field_list(fm, use_defaults=False, pref_name='book_display_fields'):
|
||||
|
||||
def render_data(mi, use_roman_numbers=True, all_fields=False, pref_name='book_display_fields',
|
||||
vertical_fields=()):
|
||||
field_list = get_field_list(getattr(mi, 'field_metadata', field_metadata), pref_name=pref_name)
|
||||
field_list = get_field_list(getattr(mi, 'field_metadata', field_metadata),
|
||||
pref_name=pref_name, mi=mi)
|
||||
field_list = [(x, all_fields or display) for x, display in field_list]
|
||||
return mi_to_html(
|
||||
mi, field_list=field_list, use_roman_numbers=use_roman_numbers, rtl=is_rtl(),
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
|
||||
import textwrap
|
||||
import weakref
|
||||
|
||||
from qt.core import (
|
||||
QAction, QApplication, QBrush, QCheckBox, QDialog, QDialogButtonBox, QGridLayout,
|
||||
QHBoxLayout, QIcon, QKeySequence, QLabel, QListView, QModelIndex, QPalette, QPixmap,
|
||||
@ -114,17 +116,19 @@ class Configure(Dialog):
|
||||
|
||||
class Details(HTMLDisplay):
|
||||
|
||||
def __init__(self, book_info, parent=None):
|
||||
def __init__(self, book_info, parent=None, allow_context_menu=True):
|
||||
HTMLDisplay.__init__(self, parent)
|
||||
self.book_info = book_info
|
||||
self.edit_metadata = getattr(parent, 'edit_metadata', None)
|
||||
self.setDefaultStyleSheet(css())
|
||||
self.allow_context_menu = allow_context_menu
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(350, 350)
|
||||
|
||||
def contextMenuEvent(self, ev):
|
||||
details_context_menu_event(self, ev, self.book_info, edit_metadata=self.edit_metadata)
|
||||
if self.allow_context_menu:
|
||||
details_context_menu_event(self, ev, self.book_info, edit_metadata=self.edit_metadata)
|
||||
|
||||
|
||||
class BookInfo(QDialog):
|
||||
@ -132,8 +136,11 @@ class BookInfo(QDialog):
|
||||
closed = pyqtSignal(object)
|
||||
open_cover_with = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent, view, row, link_delegate):
|
||||
QDialog.__init__(self, parent)
|
||||
def __init__(self, parent, view, row, link_delegate, dialog_number=None,
|
||||
library_id=None, library_path=None, book_id=None, query=None):
|
||||
QDialog.__init__(self, None, flags=Qt.WindowType.Window)
|
||||
self.dialog_number = dialog_number
|
||||
self.library_id = library_id
|
||||
self.marked = None
|
||||
self.gui = parent
|
||||
self.splitter = QSplitter(self)
|
||||
@ -150,7 +157,8 @@ class BookInfo(QDialog):
|
||||
self.cover.sizeHint = self.details_size_hint
|
||||
self.splitter.addWidget(self.cover)
|
||||
|
||||
self.details = Details(parent.book_details.book_info, self)
|
||||
self.details = Details(parent.book_details.book_info, self,
|
||||
allow_context_menu=library_path is None)
|
||||
self.details.anchor_clicked.connect(self.on_link_clicked)
|
||||
self.link_delegate = link_delegate
|
||||
self.details.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, False)
|
||||
@ -172,47 +180,76 @@ class BookInfo(QDialog):
|
||||
hl.setContentsMargins(0, 0, 0, 0)
|
||||
l2.addLayout(hl, l2.rowCount(), 0, 1, -1)
|
||||
hl.addWidget(self.fit_cover), hl.addStretch()
|
||||
self.clabel = QLabel('<div style="text-align: right"><a href="calibre:conf" title="{}" style="text-decoration: none">{}</a>'.format(
|
||||
_('Configure this view'), _('Configure')))
|
||||
self.clabel.linkActivated.connect(self.configure)
|
||||
hl.addWidget(self.clabel)
|
||||
self.previous_button = QPushButton(QIcon.ic('previous.png'), _('&Previous'), self)
|
||||
self.previous_button.clicked.connect(self.previous)
|
||||
l2.addWidget(self.previous_button, l2.rowCount(), 0)
|
||||
self.next_button = QPushButton(QIcon.ic('next.png'), _('&Next'), self)
|
||||
self.next_button.clicked.connect(self.next)
|
||||
l2.addWidget(self.next_button, l2.rowCount() - 1, 1)
|
||||
if self.dialog_number == 0:
|
||||
self.previous_button = QPushButton(QIcon.ic('previous.png'), _('&Previous'), self)
|
||||
self.previous_button.clicked.connect(self.previous)
|
||||
l2.addWidget(self.previous_button, l2.rowCount(), 0)
|
||||
self.next_button = QPushButton(QIcon.ic('next.png'), _('&Next'), self)
|
||||
self.next_button.clicked.connect(self.next)
|
||||
l2.addWidget(self.next_button, l2.rowCount() - 1, 1)
|
||||
self.ns = QShortcut(QKeySequence('Alt+Right'), self)
|
||||
self.ns.activated.connect(self.next)
|
||||
self.ps = QShortcut(QKeySequence('Alt+Left'), self)
|
||||
self.ps.activated.connect(self.previous)
|
||||
self.next_button.setToolTip(_('Next [%s]')%
|
||||
str(self.ns.key().toString(QKeySequence.SequenceFormat.NativeText)))
|
||||
self.previous_button.setToolTip(_('Previous [%s]')%
|
||||
str(self.ps.key().toString(QKeySequence.SequenceFormat.NativeText)))
|
||||
|
||||
self.view = view
|
||||
self.path_to_book = None
|
||||
self.current_row = None
|
||||
self.refresh(row)
|
||||
self.view.model().new_bookdisplay_data.connect(self.slave)
|
||||
self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
|
||||
self.ns = QShortcut(QKeySequence('Alt+Right'), self)
|
||||
self.ns.activated.connect(self.next)
|
||||
self.ps = QShortcut(QKeySequence('Alt+Left'), self)
|
||||
self.ps.activated.connect(self.previous)
|
||||
self.next_button.setToolTip(_('Next [%s]')%
|
||||
str(self.ns.key().toString(QKeySequence.SequenceFormat.NativeText)))
|
||||
self.previous_button.setToolTip(_('Previous [%s]')%
|
||||
str(self.ps.key().toString(QKeySequence.SequenceFormat.NativeText)))
|
||||
self.slave_connected = False
|
||||
if library_path is not None:
|
||||
self.view = None
|
||||
from calibre.db.legacy import LibraryDatabase
|
||||
db = LibraryDatabase(library_path, read_only=True, is_second_db=True)
|
||||
if book_id is None:
|
||||
ids = db.new_api.search(query)
|
||||
if len(ids) == 0:
|
||||
raise ValueError(_('Query "{}" found no books').format(query))
|
||||
book_id = sorted([i for i in ids])[0]
|
||||
if not db.new_api.has_id(book_id):
|
||||
raise ValueError(_("Book {} doesn't exist").format(book_id))
|
||||
mi = db.new_api.get_metadata(book_id, get_cover=False)
|
||||
mi.cover_data = [None, db.new_api.cover(book_id, as_image=True)]
|
||||
mi.path = None
|
||||
mi.format_files = dict()
|
||||
mi.formats = list()
|
||||
mi.marked = ''
|
||||
mi._bd_dbwref = weakref.proxy(db)
|
||||
self.refresh(row, mi)
|
||||
else:
|
||||
self.view = view
|
||||
if dialog_number == 0:
|
||||
self.slave_connected = True
|
||||
self.view.model().new_bookdisplay_data.connect(self.slave)
|
||||
self.refresh(row)
|
||||
|
||||
self.restore_geometry(gprefs, 'book_info_dialog_geometry')
|
||||
ema = get_gui().iactions['Edit Metadata'].menuless_qaction
|
||||
a = self.ema = QAction('edit metadata', self)
|
||||
a.setShortcut(ema.shortcut())
|
||||
self.addAction(a)
|
||||
a.triggered.connect(self.edit_metadata)
|
||||
vb = get_gui().iactions['View'].menuless_qaction
|
||||
a = self.vba = QAction('view book', self)
|
||||
a.setShortcut(vb.shortcut())
|
||||
a.triggered.connect(self.view_book)
|
||||
self.addAction(a)
|
||||
self.clabel = QLabel('<div style="text-align: right"><a href="calibre:conf" title="{}" style="text-decoration: none">{}</a>'.format(
|
||||
_('Configure this view'), _('Configure')))
|
||||
self.clabel.linkActivated.connect(self.configure)
|
||||
hl.addWidget(self.clabel)
|
||||
self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
|
||||
self.restore_geometry(gprefs, self.geometry_string('book_info_dialog_geometry'))
|
||||
try:
|
||||
self.splitter.restoreState(gprefs.get('book_info_dialog_splitter_state'))
|
||||
self.splitter.restoreState(gprefs.get(self.geometry_string('book_info_dialog_splitter_state')))
|
||||
except Exception:
|
||||
pass
|
||||
ema = get_gui().iactions['Edit Metadata'].menuless_qaction
|
||||
a = self.ema = QAction('edit metadata', self)
|
||||
a.setShortcut(ema.shortcut())
|
||||
self.addAction(a)
|
||||
a.triggered.connect(self.edit_metadata)
|
||||
vb = get_gui().iactions['View'].menuless_qaction
|
||||
a = self.vba = QAction('view book', self)
|
||||
a.setShortcut(vb.shortcut())
|
||||
a.triggered.connect(self.view_book)
|
||||
self.addAction(a)
|
||||
|
||||
def geometry_string(self, txt):
|
||||
if self.dialog_number is None or self.dialog_number == 0:
|
||||
return txt
|
||||
return txt + '_' + str(self.dialog_number)
|
||||
|
||||
def sizeHint(self):
|
||||
try:
|
||||
@ -248,10 +285,11 @@ class BookInfo(QDialog):
|
||||
self.link_delegate(link)
|
||||
|
||||
def done(self, r):
|
||||
self.save_geometry(gprefs, 'book_info_dialog_geometry')
|
||||
gprefs['book_info_dialog_splitter_state'] = bytearray(self.splitter.saveState())
|
||||
self.save_geometry(gprefs, self.geometry_string('book_info_dialog_geometry'))
|
||||
gprefs[self.geometry_string('book_info_dialog_splitter_state')] = bytearray(self.splitter.saveState())
|
||||
ret = QDialog.done(self, r)
|
||||
self.view.model().new_bookdisplay_data.disconnect(self.slave)
|
||||
if self.slave_connected:
|
||||
self.view.model().new_bookdisplay_data.disconnect(self.slave)
|
||||
self.view = self.link_delegate = self.gui = None
|
||||
self.closed.emit(self)
|
||||
return ret
|
||||
@ -342,11 +380,15 @@ class BookInfo(QDialog):
|
||||
# Indicates books was deleted from library, or row numbers have
|
||||
# changed
|
||||
return
|
||||
|
||||
self.previous_button.setEnabled(False if row == 0 else True)
|
||||
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
|
||||
if self.dialog_number == 0:
|
||||
self.previous_button.setEnabled(False if row == 0 else True)
|
||||
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
|
||||
self.setWindowTitle(mi.title + ' ' + _('(the current book)'))
|
||||
elif self.library_id is not None:
|
||||
self.setWindowTitle(mi.title + ' ' + _('(from {})').format(self.library_id))
|
||||
else:
|
||||
self.setWindowTitle(mi.title + ' ' + _('(will not change)'))
|
||||
self.current_row = row
|
||||
self.setWindowTitle(mi.title)
|
||||
self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1])
|
||||
self.path_to_book = getattr(mi, 'path', None)
|
||||
try:
|
||||
|
@ -132,7 +132,9 @@ class EditColumnDelegate(QItemDelegate):
|
||||
else:
|
||||
editor = EnLineEdit(parent)
|
||||
return editor
|
||||
return None
|
||||
editor = EnLineEdit(parent)
|
||||
editor.setClearButtonEnabled(True)
|
||||
return editor
|
||||
|
||||
def destroyEditor(self, editor, index):
|
||||
self.editing_finished.emit(index.row())
|
||||
@ -142,12 +144,13 @@ class EditColumnDelegate(QItemDelegate):
|
||||
class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
|
||||
def __init__(self, window, cat_name, tag_to_match, get_book_ids, sorter,
|
||||
ttm_is_first_letter=False, category=None, fm=None):
|
||||
ttm_is_first_letter=False, category=None, fm=None, link_map=None):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_TagListEditor.__init__(self)
|
||||
self.setupUi(self)
|
||||
self.verticalLayout_2.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.search_box.setMinimumContentsLength(25)
|
||||
self.link_map = link_map
|
||||
|
||||
# Put the category name into the title bar
|
||||
t = self.windowTitle()
|
||||
@ -171,6 +174,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
self.to_delete = set()
|
||||
self.all_tags = {}
|
||||
self.original_names = {}
|
||||
self.links = {}
|
||||
|
||||
self.ordered_tags = []
|
||||
self.sorter = sorter
|
||||
@ -413,13 +417,15 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
select_item = None
|
||||
self.table.blockSignals(True)
|
||||
self.table.clear()
|
||||
self.table.setColumnCount(3)
|
||||
self.table.setColumnCount(4)
|
||||
self.name_col = QTableWidgetItem(self.category_name)
|
||||
self.table.setHorizontalHeaderItem(0, self.name_col)
|
||||
self.count_col = QTableWidgetItem(_('Count'))
|
||||
self.table.setHorizontalHeaderItem(1, self.count_col)
|
||||
self.was_col = QTableWidgetItem(_('Was'))
|
||||
self.table.setHorizontalHeaderItem(2, self.was_col)
|
||||
self.link_col = QTableWidgetItem(_('Link'))
|
||||
self.table.setHorizontalHeaderItem(3, self.link_col)
|
||||
|
||||
self.table.setRowCount(len(tags))
|
||||
for row,tag in enumerate(tags):
|
||||
@ -457,6 +463,16 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
item.setData(Qt.ItemDataRole.DisplayRole, tag)
|
||||
self.table.setItem(row, 2, item)
|
||||
|
||||
item = QTableWidgetItem()
|
||||
if self.link_map is None:
|
||||
item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
|
||||
item.setText(_('no links available'))
|
||||
else:
|
||||
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable)
|
||||
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable)
|
||||
item.setText(self.link_map.get(tag, ''))
|
||||
self.table.setItem(row, 3, item)
|
||||
|
||||
if self.last_sorted_by == 'name':
|
||||
self.table.sortByColumn(0, Qt.SortOrder(self.name_order))
|
||||
elif self.last_sorted_by == 'count':
|
||||
@ -530,6 +546,8 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
self.table.blockSignals(False)
|
||||
|
||||
def finish_editing(self, edited_item):
|
||||
if edited_item.column != 0:
|
||||
return
|
||||
if not edited_item.text():
|
||||
error_dialog(self, _('Item is blank'), _(
|
||||
'An item cannot be set to nothing. Delete it instead.'), show=True)
|
||||
@ -662,4 +680,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
self.table.sortByColumn(2, Qt.SortOrder(self.was_order))
|
||||
|
||||
def accepted(self):
|
||||
self.links = {}
|
||||
for r in range(0, self.table.rowCount()):
|
||||
self.links[self.table.item(r, 0).text()] = self.table.item(r, 3).text()
|
||||
self.save_geometry()
|
||||
|
@ -58,6 +58,7 @@ class TagBrowserMixin: # {{{
|
||||
partial(func, *args))
|
||||
fm = db.new_api.field_metadata
|
||||
categories = [x[0] for x in find_categories(fm) if fm.is_custom_field(x[0])]
|
||||
categories = [c for c in categories if fm[c]['datatype'] != 'composite']
|
||||
if categories:
|
||||
if len(categories) > 5:
|
||||
m = m.addMenu(_('Custom columns'))
|
||||
@ -281,7 +282,8 @@ class TagBrowserMixin: # {{{
|
||||
tag_to_match=tag,
|
||||
get_book_ids=partial(self.get_book_ids, db=db, category=category),
|
||||
sorter=key, ttm_is_first_letter=is_first_letter,
|
||||
fm=db.field_metadata[category])
|
||||
fm=db.field_metadata[category],
|
||||
link_map=db.new_api.get_link_map(category))
|
||||
d.exec()
|
||||
if d.result() == QDialog.DialogCode.Accepted:
|
||||
to_rename = d.to_rename # dict of old id to new name
|
||||
@ -300,6 +302,9 @@ class TagBrowserMixin: # {{{
|
||||
db.new_api.remove_items(category, to_delete)
|
||||
db.new_api.rename_items(category, to_rename, change_index=False)
|
||||
|
||||
# Must do this at the end so renames are accounted for
|
||||
db.new_api.set_link_map(category, d.links)
|
||||
|
||||
# Clean up the library view
|
||||
self.do_tag_item_renamed()
|
||||
self.tags_view.recount()
|
||||
|
@ -690,6 +690,22 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
library_path = self.library_broker.path_for_library_id(library_id)
|
||||
if not db_matches(self.current_db, library_id, library_path):
|
||||
self.library_moved(library_path)
|
||||
elif action == 'book-details':
|
||||
parts = tuple(filter(None, path.split('/')))
|
||||
if len(parts) != 2:
|
||||
return
|
||||
library_id, book_id = parts
|
||||
library_id = decode_library_id(library_id)
|
||||
library_path = self.library_broker.path_for_library_id(library_id)
|
||||
if library_path is None:
|
||||
return
|
||||
try:
|
||||
book_id = int(book_id)
|
||||
except Exception:
|
||||
prints('Ignoring invalid book id', book_id, file=sys.stderr)
|
||||
return
|
||||
details = self.iactions['Show Book Details']
|
||||
details.show_book_info(library_id=library_id, library_path=library_path, book_id=book_id)
|
||||
elif action == 'show-book':
|
||||
parts = tuple(filter(None, path.split('/')))
|
||||
if len(parts) != 2:
|
||||
|
Loading…
x
Reference in New Issue
Block a user