mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add support for collections. Bug fixes.
This commit is contained in:
parent
55602e7daf
commit
89e77eb685
@ -13,7 +13,7 @@
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
''' E-book management software'''
|
||||
__version__ = "0.3.92"
|
||||
__version__ = "0.3.93"
|
||||
__docformat__ = "epytext"
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
__appname__ = 'libprs500'
|
||||
|
@ -102,8 +102,7 @@ class Device(object):
|
||||
@param oncard: If True return a list of ebooks on the storage card,
|
||||
otherwise return list of ebooks in main memory of device.
|
||||
If True and no books on card return empty list.
|
||||
@return: A list of Books. Each Book object must have the fields:
|
||||
title, authors, size, datetime (a UTC time tuple), path, thumbnail (can be None).
|
||||
@return: A BookList.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@ -129,9 +128,10 @@ class Device(object):
|
||||
the device.
|
||||
@param locations: Result of a call to L{upload_books}
|
||||
@param metadata: List of dictionaries. Each dictionary must have the
|
||||
keys C{title}, C{authors}, C{cover}. The value of the C{cover} element
|
||||
can be None or a three element tuple (width, height, data)
|
||||
where data is the image data in JPEG format as a string.
|
||||
keys C{title}, C{authors}, C{cover}, C{tags}. The value of the C{cover}
|
||||
element can be None or a three element tuple (width, height, data)
|
||||
where data is the image data in JPEG format as a string. C{tags} must be
|
||||
a possibly empty list of strings.
|
||||
@param booklists: A tuple containing the result of calls to
|
||||
(L{books}(oncard=False), L{books}(oncard=True)).
|
||||
'''
|
||||
@ -161,4 +161,31 @@ class Device(object):
|
||||
(L{books}(oncard=False), L{books}(oncard=True)).
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
class BookList(list):
|
||||
'''
|
||||
A list of books. Each Book object must have the fields:
|
||||
1. title
|
||||
2. authors
|
||||
3. size (file size of the book)
|
||||
4. datetime (a UTC time tuple)
|
||||
5. path (path on the device to the book)
|
||||
6. thumbnail (can be None)
|
||||
7. tags (a list of strings, can be empty).
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
list.__init__(self)
|
||||
|
||||
def supports_tags(self):
|
||||
''' 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()
|
||||
|
||||
|
@ -22,6 +22,7 @@ from base64 import b64encode as encode
|
||||
import time, re
|
||||
|
||||
from libprs500.devices.errors import ProtocolError
|
||||
from libprs500.devices.interface import BookList as _BookList
|
||||
|
||||
MIME_MAP = { \
|
||||
"lrf":"application/x-sony-bbeb", \
|
||||
@ -64,7 +65,6 @@ class Book(object):
|
||||
datetime = book_metadata_field("date", \
|
||||
formatter=lambda x: time.strptime(x.strip(), "%a, %d %b %Y %H:%M:%S %Z"),
|
||||
setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x)))
|
||||
|
||||
@apply
|
||||
def title_sorter():
|
||||
doc = '''String to sort the title. If absent, title is returned'''
|
||||
@ -105,10 +105,11 @@ class Book(object):
|
||||
return self.root + self.rpath
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
def __init__(self, node, prefix="", root="/Data/media/"):
|
||||
self.elem = node
|
||||
def __init__(self, node, tags=[], prefix="", root="/Data/media/"):
|
||||
self.elem = node
|
||||
self.prefix = prefix
|
||||
self.root = root
|
||||
self.root = root
|
||||
self.tags = tags
|
||||
|
||||
def __str__(self):
|
||||
""" Return a utf-8 encoded string with title author and path information """
|
||||
@ -117,33 +118,22 @@ class Book(object):
|
||||
|
||||
|
||||
def fix_ids(media, cache):
|
||||
'''
|
||||
Update ids in media, cache to be consistent with their
|
||||
current structure
|
||||
'''
|
||||
Adjust ids in cache to correspond with media.
|
||||
'''
|
||||
media.purge_empty_playlists()
|
||||
plitems = media.playlist_items()
|
||||
cid = 0
|
||||
for child in media.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
old_id = child.getAttribute('id')
|
||||
for item in plitems:
|
||||
if item.hasAttribute('id') and item.getAttribute('id') == old_id:
|
||||
item.setAttribute('id', str(cid))
|
||||
child.setAttribute("id", str(cid))
|
||||
cid += 1
|
||||
mmaxid = cid - 1
|
||||
cid = mmaxid + 2
|
||||
if len(cache):
|
||||
if cache.root:
|
||||
sourceid = media.max_id()
|
||||
cid = sourceid + 1
|
||||
for child in cache.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and \
|
||||
child.hasAttribute("sourceid"):
|
||||
child.setAttribute("sourceid", str(mmaxid+1))
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("sourceid"):
|
||||
child.setAttribute("sourceid", str(sourceid))
|
||||
child.setAttribute("id", str(cid))
|
||||
cid += 1
|
||||
media.document.documentElement.setAttribute("nextID", str(cid))
|
||||
|
||||
class BookList(list):
|
||||
media.set_next_id(str(cid))
|
||||
|
||||
|
||||
class BookList(_BookList):
|
||||
"""
|
||||
A list of L{Book}s. Created from an XML file. Can write list
|
||||
to an XML file.
|
||||
@ -152,7 +142,8 @@ class BookList(list):
|
||||
__setslice__ = None
|
||||
|
||||
def __init__(self, root="/Data/media/", sfile=None):
|
||||
list.__init__(self)
|
||||
_BookList.__init__(self)
|
||||
self.root = self.document = self.proot = None
|
||||
if sfile:
|
||||
sfile.seek(0)
|
||||
self.document = dom.parse(sfile)
|
||||
@ -160,50 +151,30 @@ class BookList(list):
|
||||
self.prefix = ''
|
||||
records = self.root.getElementsByTagName('records')
|
||||
if records:
|
||||
self.prefix = 'xs1:'
|
||||
self.root = records[0]
|
||||
for child in self.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
self.prefix = child.tagName.partition(':')[0] + ':'
|
||||
break
|
||||
if not self.prefix:
|
||||
raise ProtocolError, 'Could not determine prefix in media.xml'
|
||||
self.proot = root
|
||||
|
||||
for book in self.document.getElementsByTagName(self.prefix + "text"):
|
||||
self.append(Book(book, root=root, prefix=self.prefix))
|
||||
for book in self.document.getElementsByTagName(self.prefix + "text"):
|
||||
id = book.getAttribute('id')
|
||||
pl = [i.getAttribute('title') for i in self.get_playlists(id)]
|
||||
self.append(Book(book, root=root, prefix=self.prefix, tags=pl))
|
||||
|
||||
def supports_tags(self):
|
||||
return bool(self.prefix)
|
||||
|
||||
def max_id(self):
|
||||
""" Highest id in underlying XML file """
|
||||
cid = -1
|
||||
for child in self.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and \
|
||||
child.hasAttribute("id"):
|
||||
c = int(child.getAttribute("id"))
|
||||
if c > cid:
|
||||
cid = c
|
||||
return cid
|
||||
|
||||
def has_id(self, cid):
|
||||
"""
|
||||
Check if a book with id C{ == cid} exists already.
|
||||
This *does not* check if id exists in the underlying XML file
|
||||
"""
|
||||
ans = False
|
||||
for book in self:
|
||||
if book.id == cid:
|
||||
ans = True
|
||||
break
|
||||
return ans
|
||||
def playlists(self):
|
||||
return self.root.getElementsByTagName(self.prefix+'playlist')
|
||||
|
||||
def playlist_items(self):
|
||||
playlists = self.root.getElementsByTagName(self.prefix+'playlist')
|
||||
plitems = []
|
||||
for pl in playlists:
|
||||
for pl in self.playlists():
|
||||
plitems.extend(pl.getElementsByTagName(self.prefix+'item'))
|
||||
return plitems
|
||||
|
||||
def purge_corrupted_files(self):
|
||||
if not self.root:
|
||||
return []
|
||||
corrupted = self.root.getElementsByTagName(self.prefix+'corrupted')
|
||||
paths = []
|
||||
proot = self.proot if self.proot.endswith('/') else self.proot + '/'
|
||||
@ -215,8 +186,7 @@ class BookList(list):
|
||||
|
||||
def purge_empty_playlists(self):
|
||||
''' Remove all playlist entries that have no children. '''
|
||||
playlists = self.root.getElementsByTagName(self.prefix+'playlist')
|
||||
for pl in playlists:
|
||||
for pl in self.playlists():
|
||||
if not pl.getElementsByTagName(self.prefix + 'item'):
|
||||
pl.parentNode.removeChild(pl)
|
||||
pl.unlink()
|
||||
@ -225,10 +195,8 @@ class BookList(list):
|
||||
nid = node.getAttribute('id')
|
||||
node.parentNode.removeChild(node)
|
||||
node.unlink()
|
||||
for pli in self.playlist_items():
|
||||
if pli.getAttribute('id') == nid:
|
||||
pli.parentNode.removeChild(pli)
|
||||
pli.unlink()
|
||||
self.remove_from_playlists(nid)
|
||||
|
||||
|
||||
def delete_book(self, cid):
|
||||
'''
|
||||
@ -247,11 +215,26 @@ class BookList(list):
|
||||
Also remove book from any collections it is part of.
|
||||
'''
|
||||
for book in self:
|
||||
if book.path == path:
|
||||
if path.endswith(book.path):
|
||||
self.remove(book)
|
||||
self._delete_book(book.elem)
|
||||
break
|
||||
|
||||
def next_id(self):
|
||||
return self.document.documentElement.getAttribute('nextID')
|
||||
|
||||
def set_next_id(self, id):
|
||||
self.document.documentElement.setAttribute('nextID', str(id))
|
||||
|
||||
def max_id(self):
|
||||
max = 0
|
||||
for child in self.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
nid = int(child.getAttribute('id'))
|
||||
if nid > max:
|
||||
max = nid
|
||||
return max
|
||||
|
||||
def add_book(self, info, name, size, ctime):
|
||||
""" Add a node into DOM tree representing a book """
|
||||
node = self.document.createElement(self.prefix + "text")
|
||||
@ -266,7 +249,6 @@ class BookList(list):
|
||||
"sourceid":sourceid, "id":str(cid), "date":"", \
|
||||
"mime":mime, "path":name, "size":str(size)
|
||||
}
|
||||
print name
|
||||
for attr in attrs.keys():
|
||||
node.setAttributeNode(self.document.createAttribute(attr))
|
||||
node.setAttribute(attr, attrs[attr])
|
||||
@ -287,6 +269,63 @@ class BookList(list):
|
||||
book = Book(node, root=self.proot, prefix=self.prefix)
|
||||
book.datetime = ctime
|
||||
self.append(book)
|
||||
self.set_next_id(cid+1)
|
||||
if self.prefix: # Playlists only supportted in main memory
|
||||
self.set_playlists(book.id, info['tags'])
|
||||
|
||||
|
||||
def playlist_by_title(self, title):
|
||||
for pl in self.playlists():
|
||||
if pl.getAttribute('title').lower() == title.lower():
|
||||
return pl
|
||||
|
||||
def add_playlist(self, title):
|
||||
cid = self.max_id()+1
|
||||
pl = self.document.createElement(self.prefix+'playlist')
|
||||
pl.setAttribute('sourceid', '0')
|
||||
pl.setAttribute('id', str(cid))
|
||||
pl.setAttribute('title', title)
|
||||
for child in self.root.childNodes:
|
||||
try:
|
||||
if child.getAttribute('id') == '1':
|
||||
self.root.insertBefore(pl, child)
|
||||
self.set_next_id(cid+1)
|
||||
break
|
||||
except AttributeError:
|
||||
continue
|
||||
return pl
|
||||
|
||||
|
||||
def remove_from_playlists(self, id):
|
||||
for pli in self.playlist_items():
|
||||
if pli.getAttribute('id') == str(id):
|
||||
pli.parentNode.removeChild(pli)
|
||||
pli.unlink()
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
book.tags = tags
|
||||
self.set_playlists(book.id, tags)
|
||||
|
||||
def set_playlists(self, id, collections):
|
||||
self.remove_from_playlists(id)
|
||||
for collection in set(collections):
|
||||
coll = self.playlist_by_title(collection)
|
||||
if not coll:
|
||||
coll = self.add_playlist(collection)
|
||||
item = self.document.createElement(self.prefix+'item')
|
||||
item.setAttribute('id', str(id))
|
||||
coll.appendChild(item)
|
||||
|
||||
def get_playlists(self, id):
|
||||
ans = []
|
||||
for pl in self.playlists():
|
||||
for item in pl.getElementsByTagName(self.prefix+'item'):
|
||||
if item.getAttribute('id') == str(id):
|
||||
ans.append(pl)
|
||||
continue
|
||||
return ans
|
||||
|
||||
|
||||
|
||||
def write(self, stream):
|
||||
""" Write XML representation of DOM tree to C{stream} """
|
||||
|
@ -12,6 +12,7 @@
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
from StringIO import StringIO
|
||||
|
||||
### End point description for PRS-500 procductId=667
|
||||
### Endpoint Descriptor:
|
||||
@ -802,12 +803,12 @@ class PRS500(Device):
|
||||
else:
|
||||
self.get_file(self.MEDIA_XML, tfile, end_session=False)
|
||||
bl = BookList(root=root, sfile=tfile)
|
||||
paths = bl.purge_corrupted_files()
|
||||
paths = bl.purge_corrupted_files()
|
||||
for path in paths:
|
||||
try:
|
||||
self.del_file(path, end_session=False)
|
||||
except PathError: # Incase this is a refetch without a sync in between
|
||||
continue
|
||||
continue
|
||||
return bl
|
||||
|
||||
@safe
|
||||
@ -828,8 +829,9 @@ class PRS500(Device):
|
||||
@param booklists: A tuple containing the result of calls to
|
||||
(L{books}(oncard=False), L{books}(oncard=True)).
|
||||
'''
|
||||
fix_ids(*booklists)
|
||||
self.upload_book_list(booklists[0], end_session=False)
|
||||
if len(booklists[1]):
|
||||
if booklists[1].root:
|
||||
self.upload_book_list(booklists[1], end_session=False)
|
||||
|
||||
@safe
|
||||
@ -940,7 +942,7 @@ class PRS500(Device):
|
||||
raise ArgumentError("Cannot upload list to card as "+\
|
||||
"card is not present")
|
||||
path = card + self.CACHE_XML
|
||||
f = TemporaryFile()
|
||||
f = StringIO()
|
||||
booklist.write(f)
|
||||
f.seek(0)
|
||||
self.put_file(f, path, replace_file=True, end_session=False)
|
||||
|
@ -1230,7 +1230,8 @@ class HTMLConverter(object):
|
||||
self.process_table(tag, tag_css)
|
||||
except Exception, err:
|
||||
print 'WARNING: An error occurred while processing a table:', err
|
||||
print 'Ignoring table markup'
|
||||
print 'Ignoring table markup for table:'
|
||||
print str(tag)[:100]
|
||||
self.in_table = False
|
||||
self.process_children(tag, tag_css)
|
||||
else:
|
||||
|
@ -12,6 +12,7 @@
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
from libprs500.gui2 import qstring_to_unicode
|
||||
import os, textwrap, traceback, time, re, sre_constants
|
||||
from datetime import timedelta, datetime
|
||||
from operator import attrgetter
|
||||
@ -19,7 +20,7 @@ from math import cos, sin, pi
|
||||
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
|
||||
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
|
||||
QPen, QStyle, QPainter, QLineEdit, QApplication, \
|
||||
QPalette, QItemSelectionModel
|
||||
QPalette
|
||||
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
|
||||
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
|
||||
|
||||
@ -195,6 +196,7 @@ class BooksModel(QAbstractTableModel):
|
||||
for row in rows:
|
||||
row = row.row()
|
||||
au = self.db.authors(row)
|
||||
tags = self.db.tags(row)
|
||||
if not au:
|
||||
au = 'Unknown'
|
||||
au = au.split(',')
|
||||
@ -204,11 +206,17 @@ class BooksModel(QAbstractTableModel):
|
||||
au = t
|
||||
else:
|
||||
au = ' & '.join(au)
|
||||
if not tags:
|
||||
tags = []
|
||||
else:
|
||||
tags = tags.split(',')
|
||||
mi = {
|
||||
'title' : self.db.title(row),
|
||||
'authors' : au,
|
||||
'cover' : self.db.cover(row),
|
||||
'tags' : tags,
|
||||
}
|
||||
|
||||
metadata.append(mi)
|
||||
return metadata
|
||||
|
||||
@ -334,7 +342,8 @@ class BooksView(QTableView):
|
||||
self.setModel(self._model)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSortingEnabled(True)
|
||||
self.setItemDelegateForColumn(4, LibraryDelegate(self))
|
||||
if self.__class__.__name__ == 'BooksView': # Subclasses may not have rating as col 4
|
||||
self.setItemDelegateForColumn(4, LibraryDelegate(self))
|
||||
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
||||
self._model.current_changed)
|
||||
# Adding and removing rows should resize rows to contents
|
||||
@ -398,6 +407,7 @@ class DeviceBooksModel(BooksModel):
|
||||
self.unknown = str(self.trUtf8('Unknown'))
|
||||
self.marked_for_deletion = {}
|
||||
|
||||
|
||||
def mark_for_deletion(self, id, rows):
|
||||
self.marked_for_deletion[id] = self.indices(rows)
|
||||
for row in rows:
|
||||
@ -415,16 +425,16 @@ class DeviceBooksModel(BooksModel):
|
||||
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
|
||||
|
||||
def path_about_to_be_deleted(self, path):
|
||||
for row in range(len(self.map)):
|
||||
for row in range(len(self.map)):
|
||||
if self.db[self.map[row]].path == path:
|
||||
print row, path
|
||||
#print row, path
|
||||
#print self.rowCount(None)
|
||||
self.beginRemoveRows(QModelIndex(), row, row)
|
||||
self.map.pop(row)
|
||||
self.endRemoveRows()
|
||||
#print self.rowCount(None)
|
||||
return
|
||||
|
||||
def path_deleted(self):
|
||||
self.endRemoveRows()
|
||||
|
||||
def indices_to_be_deleted(self):
|
||||
ans = []
|
||||
for v in self.marked_for_deletion.values():
|
||||
@ -433,8 +443,12 @@ class DeviceBooksModel(BooksModel):
|
||||
|
||||
def flags(self, index):
|
||||
if self.map[index.row()] in self.indices_to_be_deleted():
|
||||
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
|
||||
return BooksModel.flags(self, index)
|
||||
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
|
||||
flags = QAbstractTableModel.flags(self, index)
|
||||
if index.isValid():
|
||||
if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
|
||||
flags |= Qt.ItemIsEditable
|
||||
return flags
|
||||
|
||||
|
||||
def search(self, text, refinement, reset=True):
|
||||
@ -443,7 +457,7 @@ class DeviceBooksModel(BooksModel):
|
||||
result = []
|
||||
for i in base:
|
||||
add = True
|
||||
q = self.db[i].title + ' ' + self.db[i].authors
|
||||
q = self.db[i].title + ' ' + self.db[i].authors + ' ' + ', '.join(self.db[i].tags)
|
||||
for token in tokens:
|
||||
if not token.search(q):
|
||||
add = False
|
||||
@ -478,8 +492,11 @@ class DeviceBooksModel(BooksModel):
|
||||
def sizecmp(x, y):
|
||||
x, y = int(self.db[x].size), int(self.db[y].size)
|
||||
return cmp(x, y)
|
||||
def tagscmp(x, y):
|
||||
x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags)
|
||||
return cmp(x, y)
|
||||
fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \
|
||||
sizecmp if col == 2 else datecmp
|
||||
sizecmp if col == 2 else datecmp if col == 3 else tagscmp
|
||||
self.map.sort(cmp=fcmp, reverse=descending)
|
||||
if len(self.map) == len(self.db):
|
||||
self.sorted_map = list(self.map)
|
||||
@ -491,7 +508,7 @@ class DeviceBooksModel(BooksModel):
|
||||
self.reset()
|
||||
|
||||
def columnCount(self, parent):
|
||||
return 4
|
||||
return 5
|
||||
|
||||
def rowCount(self, parent):
|
||||
return len(self.map)
|
||||
@ -516,6 +533,7 @@ class DeviceBooksModel(BooksModel):
|
||||
dt = datetime(*dt[0:6])
|
||||
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
||||
data['Timestamp'] = dt.ctime()
|
||||
data['Tags'] = ', '.join(item.tags)
|
||||
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
|
||||
|
||||
def paths(self, rows):
|
||||
@ -556,30 +574,51 @@ class DeviceBooksModel(BooksModel):
|
||||
dt = datetime(*dt[0:6])
|
||||
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
||||
return QVariant(dt.strftime(BooksView.TIME_FMT))
|
||||
elif col == 4:
|
||||
tags = self.db[self.map[row]].tags
|
||||
if tags:
|
||||
return QVariant(', '.join(tags))
|
||||
elif role == Qt.TextAlignmentRole and index.column() in [2, 3]:
|
||||
return QVariant(Qt.AlignRight | Qt.AlignVCenter)
|
||||
elif role == Qt.ToolTipRole and index.isValid():
|
||||
if self.map[index.row()] in self.indices_to_be_deleted():
|
||||
return QVariant('Marked for deletion')
|
||||
if index.column() in [0, 1]:
|
||||
col = index.column()
|
||||
if col in [0, 1] or (col == 4 and self.db.supports_tags()):
|
||||
return QVariant("Double click to <b>edit</b> me<br><br>")
|
||||
return NONE
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role != Qt.DisplayRole:
|
||||
return NONE
|
||||
text = ""
|
||||
if orientation == Qt.Horizontal:
|
||||
if section == 0: text = "Title"
|
||||
elif section == 1: text = "Author(s)"
|
||||
elif section == 2: text = "Size (MB)"
|
||||
elif section == 3: text = "Date"
|
||||
elif section == 4: text = "Tags"
|
||||
return QVariant(self.trUtf8(text))
|
||||
else:
|
||||
return QVariant(section+1)
|
||||
|
||||
def setData(self, index, value, role):
|
||||
done = False
|
||||
if role == Qt.EditRole:
|
||||
row, col = index.row(), index.column()
|
||||
if col in [2, 3]:
|
||||
return False
|
||||
val = unicode(value.toString().toUtf8(), 'utf-8').strip()
|
||||
val = qstring_to_unicode(value.toString()).strip()
|
||||
idx = self.map[row]
|
||||
if col == 0:
|
||||
self.db[idx].title = val
|
||||
self.db[idx].title_sorter = val
|
||||
elif col == 1:
|
||||
self.db[idx].authors = val
|
||||
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
|
||||
index, index)
|
||||
elif col == 4:
|
||||
tags = [i.strip() for i in val.split(',')]
|
||||
self.db.set_tags(self.db[idx], tags)
|
||||
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
|
||||
self.emit(SIGNAL('booklist_dirtied()'))
|
||||
if col == self.sorted_on[0]:
|
||||
self.sort(col, self.sorted_on[1])
|
||||
|
@ -61,6 +61,7 @@ class Main(QObject, Ui_MainWindow):
|
||||
self.device_error_dialog = error_dialog(self.window, 'Error communicating with device', ' ')
|
||||
self.device_error_dialog.setModal(Qt.NonModal)
|
||||
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
||||
self.device_connected = False
|
||||
####################### Location View ########################
|
||||
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
|
||||
self.location_selected)
|
||||
@ -156,7 +157,9 @@ class Main(QObject, Ui_MainWindow):
|
||||
self.set_default_thumbnail(cls.THUMBNAIL_HEIGHT)
|
||||
self.status_bar.showMessage('Device: '+cls.__name__+' detected.', 3000)
|
||||
self.action_sync.setEnabled(True)
|
||||
self.device_connected = True
|
||||
else:
|
||||
self.device_connected = False
|
||||
self.job_manager.terminate_device_jobs()
|
||||
self.device_manager.device_removed()
|
||||
self.location_view.model().update_devices()
|
||||
@ -326,15 +329,12 @@ class Main(QObject, Ui_MainWindow):
|
||||
self.device_job_exception(id, description, exception, formatted_traceback)
|
||||
return
|
||||
|
||||
self.upload_booklists()
|
||||
|
||||
if self.delete_memory.has_key(id):
|
||||
paths, model = self.delete_memory.pop(id)
|
||||
for path in paths:
|
||||
model.path_about_to_be_deleted(path)
|
||||
self.device_manager.remove_books_from_metadata((path,), self.booklists())
|
||||
model.path_deleted()
|
||||
|
||||
self.upload_booklists()
|
||||
|
||||
############################################################################
|
||||
|
||||
@ -437,6 +437,13 @@ class Main(QObject, Ui_MainWindow):
|
||||
view.resize_on_select = False
|
||||
self.status_bar.reset_info()
|
||||
self.current_view().clearSelection()
|
||||
if location == 'library':
|
||||
if self.device_connected:
|
||||
self.action_sync.setEnabled(True)
|
||||
self.action_edit.setEnabled(True)
|
||||
else:
|
||||
self.action_sync.setEnabled(False)
|
||||
self.action_edit.setEnabled(False)
|
||||
|
||||
|
||||
def wrap_traceback(self, tb):
|
||||
|
@ -12,8 +12,6 @@
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
from libprs500.gui2.dialogs.jobs import JobsDialog
|
||||
|
||||
import textwrap
|
||||
|
||||
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \
|
||||
@ -78,29 +76,12 @@ class BookInfoDisplay(QFrame):
|
||||
self.clear_message()
|
||||
self.setVisible(True)
|
||||
|
||||
class BusyIndicator(QLabel):
|
||||
def __init__(self, movie, jobs_dialog):
|
||||
QLabel.__init__(self)
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
self.setToolTip('Click to see list of active jobs.')
|
||||
self.setMovie(movie)
|
||||
movie.start()
|
||||
movie.setPaused(True)
|
||||
self.jobs_dialog = jobs_dialog
|
||||
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.jobs_dialog.isVisible():
|
||||
self.jobs_dialog.hide()
|
||||
else:
|
||||
self.jobs_dialog.show()
|
||||
|
||||
|
||||
class MovieButton(QFrame):
|
||||
def __init__(self, movie, jobs_dialog):
|
||||
QFrame.__init__(self)
|
||||
self.setLayout(QVBoxLayout())
|
||||
self.movie_widget = BusyIndicator(movie, jobs_dialog)
|
||||
self.movie_widget = QLabel()
|
||||
self.movie_widget.setMovie(movie)
|
||||
self.movie = movie
|
||||
self.layout().addWidget(self.movie_widget)
|
||||
self.jobs = QLabel('<b>Jobs: 0')
|
||||
@ -110,6 +91,18 @@ class MovieButton(QFrame):
|
||||
self.jobs.setMargin(0)
|
||||
self.layout().setMargin(0)
|
||||
self.jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
self.jobs_dialog = jobs_dialog
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
self.setToolTip('Click to see list of active jobs.')
|
||||
movie.start()
|
||||
movie.setPaused(True)
|
||||
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.jobs_dialog.isVisible():
|
||||
self.jobs_dialog.hide()
|
||||
else:
|
||||
self.jobs_dialog.show()
|
||||
|
||||
|
||||
class StatusBar(QStatusBar):
|
||||
|
@ -57,7 +57,7 @@ class LibraryDatabase(object):
|
||||
Iterator over the books in the old pre 0.4.0 database.
|
||||
'''
|
||||
conn = sqlite.connect(path)
|
||||
cur = conn.execute('select * from books_meta;')
|
||||
cur = conn.execute('select * from books_meta order by id;')
|
||||
book = cur.fetchone()
|
||||
while book:
|
||||
id = book[0]
|
||||
|
Loading…
x
Reference in New Issue
Block a user