mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Change the tags column in the device view to a Collections column that allows the user to directly edit collections on the device. Note that if the user deletes a collection taht corresponds to some data in the calibre library that would be turned intoa coleection, then the deletion has no effect, on device reconnect
This commit is contained in:
parent
ec18604949
commit
b97141b208
@ -396,14 +396,6 @@ class BookList(list):
|
||||
''' Return True if the the device supports tags (collections) for this book list. '''
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
'''
|
||||
Set the tags for C{book} to C{tags}.
|
||||
@param tags: A list of strings. Can be empty.
|
||||
@param book: A book object that is in this BookList.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_book(self, book, replace_metadata):
|
||||
'''
|
||||
Add the book to the booklist. Intent is to maintain any device-internal
|
||||
|
@ -101,16 +101,25 @@ class XMLCache(object):
|
||||
|
||||
# Playlist management {{{
|
||||
def purge_broken_playlist_items(self, root):
|
||||
for item in root.xpath(
|
||||
'//*[local-name()="playlist"]/*[local-name()="item"]'):
|
||||
id_ = item.get('id', None)
|
||||
if id_ is None or not root.xpath(
|
||||
'//*[local-name()!="item" and @id="%s"]'%id_):
|
||||
if DEBUG:
|
||||
prints('Purging broken playlist item:',
|
||||
etree.tostring(item, with_tail=False))
|
||||
item.getparent().remove(item)
|
||||
|
||||
for pl in root.xpath('//*[local-name()="playlist"]'):
|
||||
seen = set([])
|
||||
for item in list(pl):
|
||||
id_ = item.get('id', None)
|
||||
if id_ is None or id_ in seen or not root.xpath(
|
||||
'//*[local-name()!="item" and @id="%s"]'%id_):
|
||||
if DEBUG:
|
||||
if id_ is None:
|
||||
cause = 'invalid id'
|
||||
elif id_ in seen:
|
||||
cause = 'duplicate item'
|
||||
else:
|
||||
cause = 'id not found'
|
||||
prints('Purging broken playlist item:',
|
||||
id_, 'from playlist:', pl.get('title', None),
|
||||
'because:', cause)
|
||||
item.getparent().remove(item)
|
||||
continue
|
||||
seen.add(id_)
|
||||
|
||||
def prune_empty_playlists(self):
|
||||
for i, root in self.record_roots.items():
|
||||
@ -175,6 +184,8 @@ class XMLCache(object):
|
||||
# }}}
|
||||
|
||||
def fix_ids(self): # {{{
|
||||
if DEBUG:
|
||||
prints('Running fix_ids()')
|
||||
|
||||
def ensure_numeric_ids(root):
|
||||
idmap = {}
|
||||
@ -294,13 +305,8 @@ class XMLCache(object):
|
||||
break
|
||||
if book.lpath in playlist_map:
|
||||
tags = playlist_map[book.lpath]
|
||||
if tags:
|
||||
if DEBUG:
|
||||
prints('Adding tags:', tags, 'to', book.title)
|
||||
if not book.tags:
|
||||
book.tags = []
|
||||
book.tags = list(book.tags)
|
||||
book.tags += tags
|
||||
book.device_collections = tags
|
||||
|
||||
# }}}
|
||||
|
||||
# Update XML from JSON {{{
|
||||
@ -359,7 +365,25 @@ class XMLCache(object):
|
||||
nsmap=playlist.nsmap, attrib={'id':id_})
|
||||
playlist.append(item)
|
||||
|
||||
|
||||
# Delete playlist entries not in collections
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
title = playlist.get('title', None)
|
||||
if title not in collections:
|
||||
if DEBUG:
|
||||
prints('Deleting playlist:', playlist.get('title', ''))
|
||||
playlist.getparent().remove(playlist)
|
||||
continue
|
||||
books = collections[title]
|
||||
records = [self.book_by_lpath(b.lpath, root) for b in books]
|
||||
records = [x for x in records if x is not None]
|
||||
ids = [x.get('id', None) for x in records]
|
||||
ids = [x for x in ids if x is not None]
|
||||
for item in list(playlist):
|
||||
if item.get('id', None) not in ids:
|
||||
if DEBUG:
|
||||
prints('Deleting item:', item.get('id', ''),
|
||||
'from playlist:', playlist.get('title', ''))
|
||||
playlist.remove(item)
|
||||
|
||||
def create_text_record(self, root, bl_id, lpath):
|
||||
namespace = self.namespaces[bl_id]
|
||||
@ -414,8 +438,19 @@ class XMLCache(object):
|
||||
child.iterchildren(reversed=True).next().tail = '\n'+'\t'*level
|
||||
root.iterchildren(reversed=True).next().tail = '\n'+'\t'*(level-1)
|
||||
|
||||
def move_playlists_to_bottom(self):
|
||||
for root in self.record_roots.values():
|
||||
seen = []
|
||||
for pl in root.xpath('//*[local-name()="playlist"]'):
|
||||
pl.getparent().remove(pl)
|
||||
seen.append(pl)
|
||||
for pl in seen:
|
||||
root.append(pl)
|
||||
|
||||
|
||||
def write(self):
|
||||
for i, path in self.paths.items():
|
||||
self.move_playlists_to_bottom()
|
||||
self.cleanup_whitespace(i)
|
||||
raw = etree.tostring(self.roots[i], encoding='UTF-8',
|
||||
xml_declaration=True)
|
||||
|
@ -14,14 +14,14 @@ from calibre import isbytestring
|
||||
|
||||
class Book(MetaInformation):
|
||||
|
||||
BOOK_ATTRS = ['lpath', 'size', 'mime']
|
||||
BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections']
|
||||
|
||||
JSON_ATTRS = [
|
||||
'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort',
|
||||
'title_sort', 'comments', 'category', 'publisher', 'series',
|
||||
'series_index', 'rating', 'isbn', 'language', 'application_id',
|
||||
'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type',
|
||||
'uuid'
|
||||
'uuid',
|
||||
]
|
||||
|
||||
def __init__(self, prefix, lpath, size=None, other=None):
|
||||
@ -29,6 +29,7 @@ class Book(MetaInformation):
|
||||
|
||||
MetaInformation.__init__(self, '')
|
||||
|
||||
self.device_collections = []
|
||||
self.path = os.path.join(prefix, lpath)
|
||||
if os.sep == '\\':
|
||||
self.path = self.path.replace('/', '\\')
|
||||
@ -45,27 +46,7 @@ class Book(MetaInformation):
|
||||
self.smart_update(other)
|
||||
|
||||
def __eq__(self, other):
|
||||
spath = self.path
|
||||
opath = other.path
|
||||
|
||||
if not isinstance(self.path, unicode):
|
||||
try:
|
||||
spath = unicode(self.path)
|
||||
except:
|
||||
try:
|
||||
spath = self.path.decode(filesystem_encoding)
|
||||
except:
|
||||
spath = self.path
|
||||
if not isinstance(other.path, unicode):
|
||||
try:
|
||||
opath = unicode(other.path)
|
||||
except:
|
||||
try:
|
||||
opath = other.path.decode(filesystem_encoding)
|
||||
except:
|
||||
opath = other.path
|
||||
|
||||
return spath == opath
|
||||
return self.path == getattr(other, 'path', None)
|
||||
|
||||
@dynamic_property
|
||||
def db_id(self):
|
||||
@ -119,9 +100,6 @@ class BookList(_BookList):
|
||||
def supports_tags(self):
|
||||
return True
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
book.tags = tags
|
||||
|
||||
def add_book(self, book, replace_metadata):
|
||||
if book not in self:
|
||||
self.append(book)
|
||||
@ -134,6 +112,7 @@ class BookList(_BookList):
|
||||
def get_collections(self, collection_attributes):
|
||||
collections = {}
|
||||
series_categories = set([])
|
||||
collection_attributes = list(collection_attributes)+['device_collections']
|
||||
for attr in collection_attributes:
|
||||
for book in self:
|
||||
val = getattr(book, attr, None)
|
||||
@ -147,9 +126,12 @@ class BookList(_BookList):
|
||||
for category in val:
|
||||
if category not in collections:
|
||||
collections[category] = []
|
||||
collections[category].append(book)
|
||||
if attr == 'series':
|
||||
series_categories.add(category)
|
||||
if book not in collections[category]:
|
||||
collections[category].append(book)
|
||||
if attr == 'series':
|
||||
series_categories.add(category)
|
||||
|
||||
# Sort collections
|
||||
for category, books in collections.items():
|
||||
def tgetter(x):
|
||||
return getattr(x, 'title_sort', 'zzzz')
|
||||
|
@ -807,7 +807,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
'authors' : _('Author(s)'),
|
||||
'timestamp' : _('Date'),
|
||||
'size' : _('Size'),
|
||||
'tags' : _('Tags')
|
||||
'tags' : _('Collections')
|
||||
}
|
||||
self.marked_for_deletion = {}
|
||||
self.search_engine = OnDeviceSearch(self)
|
||||
@ -1000,7 +1000,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
dt = dt_factory(dt, assume_utc=True, as_utc=False)
|
||||
return QVariant(strftime(TIME_FMT, dt.timetuple()))
|
||||
elif cname == 'tags':
|
||||
tags = self.db[self.map[row]].tags
|
||||
tags = self.db[self.map[row]].device_collections
|
||||
if tags:
|
||||
return QVariant(', '.join(tags))
|
||||
elif role == Qt.ToolTipRole and index.isValid():
|
||||
@ -1047,7 +1047,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
elif cname == 'tags':
|
||||
tags = [i.strip() for i in val.split(',')]
|
||||
tags = [t for t in tags if t]
|
||||
self.db.set_tags(self.db[idx], tags)
|
||||
self.db[idx].device_collections = tags
|
||||
self.dataChanged.emit(index, index)
|
||||
self.booklist_dirtied.emit()
|
||||
done = True
|
||||
|
Loading…
x
Reference in New Issue
Block a user