Add support for collections. Bug fixes.

This commit is contained in:
Kovid Goyal 2007-08-09 06:11:12 +00:00
parent 55602e7daf
commit 89e77eb685
9 changed files with 228 additions and 120 deletions

View File

@ -13,7 +13,7 @@
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
''' E-book management software''' ''' E-book management software'''
__version__ = "0.3.92" __version__ = "0.3.93"
__docformat__ = "epytext" __docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
__appname__ = 'libprs500' __appname__ = 'libprs500'

View File

@ -102,8 +102,7 @@ class Device(object):
@param oncard: If True return a list of ebooks on the storage card, @param oncard: If True return a list of ebooks on the storage card,
otherwise return list of ebooks in main memory of device. otherwise return list of ebooks in main memory of device.
If True and no books on card return empty list. If True and no books on card return empty list.
@return: A list of Books. Each Book object must have the fields: @return: A BookList.
title, authors, size, datetime (a UTC time tuple), path, thumbnail (can be None).
""" """
raise NotImplementedError() raise NotImplementedError()
@ -129,9 +128,10 @@ class Device(object):
the device. the device.
@param locations: Result of a call to L{upload_books} @param locations: Result of a call to L{upload_books}
@param metadata: List of dictionaries. Each dictionary must have the @param metadata: List of dictionaries. Each dictionary must have the
keys C{title}, C{authors}, C{cover}. The value of the C{cover} element keys C{title}, C{authors}, C{cover}, C{tags}. The value of the C{cover}
can be None or a three element tuple (width, height, data) element can be None or a three element tuple (width, height, data)
where data is the image data in JPEG format as a string. 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 @param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)). (L{books}(oncard=False), L{books}(oncard=True)).
''' '''
@ -161,4 +161,31 @@ class Device(object):
(L{books}(oncard=False), L{books}(oncard=True)). (L{books}(oncard=False), L{books}(oncard=True)).
''' '''
raise NotImplementedError() 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()

View File

@ -22,6 +22,7 @@ from base64 import b64encode as encode
import time, re import time, re
from libprs500.devices.errors import ProtocolError from libprs500.devices.errors import ProtocolError
from libprs500.devices.interface import BookList as _BookList
MIME_MAP = { \ MIME_MAP = { \
"lrf":"application/x-sony-bbeb", \ "lrf":"application/x-sony-bbeb", \
@ -64,7 +65,6 @@ class Book(object):
datetime = book_metadata_field("date", \ datetime = book_metadata_field("date", \
formatter=lambda x: time.strptime(x.strip(), "%a, %d %b %Y %H:%M:%S %Z"), 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))) setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x)))
@apply @apply
def title_sorter(): def title_sorter():
doc = '''String to sort the title. If absent, title is returned''' doc = '''String to sort the title. If absent, title is returned'''
@ -105,10 +105,11 @@ class Book(object):
return self.root + self.rpath return self.root + self.rpath
return property(fget=fget, doc=doc) return property(fget=fget, doc=doc)
def __init__(self, node, prefix="", root="/Data/media/"): def __init__(self, node, tags=[], prefix="", root="/Data/media/"):
self.elem = node self.elem = node
self.prefix = prefix self.prefix = prefix
self.root = root self.root = root
self.tags = tags
def __str__(self): def __str__(self):
""" Return a utf-8 encoded string with title author and path information """ """ Return a utf-8 encoded string with title author and path information """
@ -117,33 +118,22 @@ class Book(object):
def fix_ids(media, cache): def fix_ids(media, cache):
''' '''
Update ids in media, cache to be consistent with their Adjust ids in cache to correspond with media.
current structure
''' '''
media.purge_empty_playlists() media.purge_empty_playlists()
plitems = media.playlist_items() if cache.root:
cid = 0 sourceid = media.max_id()
for child in media.root.childNodes: cid = sourceid + 1
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):
for child in cache.root.childNodes: for child in cache.root.childNodes:
if child.nodeType == child.ELEMENT_NODE and \ if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("sourceid"):
child.hasAttribute("sourceid"): child.setAttribute("sourceid", str(sourceid))
child.setAttribute("sourceid", str(mmaxid+1))
child.setAttribute("id", str(cid)) child.setAttribute("id", str(cid))
cid += 1 cid += 1
media.document.documentElement.setAttribute("nextID", str(cid)) media.set_next_id(str(cid))
class BookList(list):
class BookList(_BookList):
""" """
A list of L{Book}s. Created from an XML file. Can write list A list of L{Book}s. Created from an XML file. Can write list
to an XML file. to an XML file.
@ -152,7 +142,8 @@ class BookList(list):
__setslice__ = None __setslice__ = None
def __init__(self, root="/Data/media/", sfile=None): def __init__(self, root="/Data/media/", sfile=None):
list.__init__(self) _BookList.__init__(self)
self.root = self.document = self.proot = None
if sfile: if sfile:
sfile.seek(0) sfile.seek(0)
self.document = dom.parse(sfile) self.document = dom.parse(sfile)
@ -160,50 +151,30 @@ class BookList(list):
self.prefix = '' self.prefix = ''
records = self.root.getElementsByTagName('records') records = self.root.getElementsByTagName('records')
if records: if records:
self.prefix = 'xs1:'
self.root = records[0] 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 self.proot = root
for book in self.document.getElementsByTagName(self.prefix + "text"): for book in self.document.getElementsByTagName(self.prefix + "text"):
self.append(Book(book, root=root, prefix=self.prefix)) 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): def playlists(self):
""" Highest id in underlying XML file """ return self.root.getElementsByTagName(self.prefix+'playlist')
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 playlist_items(self): def playlist_items(self):
playlists = self.root.getElementsByTagName(self.prefix+'playlist')
plitems = [] plitems = []
for pl in playlists: for pl in self.playlists():
plitems.extend(pl.getElementsByTagName(self.prefix+'item')) plitems.extend(pl.getElementsByTagName(self.prefix+'item'))
return plitems return plitems
def purge_corrupted_files(self): def purge_corrupted_files(self):
if not self.root:
return []
corrupted = self.root.getElementsByTagName(self.prefix+'corrupted') corrupted = self.root.getElementsByTagName(self.prefix+'corrupted')
paths = [] paths = []
proot = self.proot if self.proot.endswith('/') else self.proot + '/' proot = self.proot if self.proot.endswith('/') else self.proot + '/'
@ -215,8 +186,7 @@ class BookList(list):
def purge_empty_playlists(self): def purge_empty_playlists(self):
''' Remove all playlist entries that have no children. ''' ''' Remove all playlist entries that have no children. '''
playlists = self.root.getElementsByTagName(self.prefix+'playlist') for pl in self.playlists():
for pl in playlists:
if not pl.getElementsByTagName(self.prefix + 'item'): if not pl.getElementsByTagName(self.prefix + 'item'):
pl.parentNode.removeChild(pl) pl.parentNode.removeChild(pl)
pl.unlink() pl.unlink()
@ -225,10 +195,8 @@ class BookList(list):
nid = node.getAttribute('id') nid = node.getAttribute('id')
node.parentNode.removeChild(node) node.parentNode.removeChild(node)
node.unlink() node.unlink()
for pli in self.playlist_items(): self.remove_from_playlists(nid)
if pli.getAttribute('id') == nid:
pli.parentNode.removeChild(pli)
pli.unlink()
def delete_book(self, cid): def delete_book(self, cid):
''' '''
@ -247,11 +215,26 @@ class BookList(list):
Also remove book from any collections it is part of. Also remove book from any collections it is part of.
''' '''
for book in self: for book in self:
if book.path == path: if path.endswith(book.path):
self.remove(book) self.remove(book)
self._delete_book(book.elem) self._delete_book(book.elem)
break 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): def add_book(self, info, name, size, ctime):
""" Add a node into DOM tree representing a book """ """ Add a node into DOM tree representing a book """
node = self.document.createElement(self.prefix + "text") node = self.document.createElement(self.prefix + "text")
@ -266,7 +249,6 @@ class BookList(list):
"sourceid":sourceid, "id":str(cid), "date":"", \ "sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size) "mime":mime, "path":name, "size":str(size)
} }
print name
for attr in attrs.keys(): for attr in attrs.keys():
node.setAttributeNode(self.document.createAttribute(attr)) node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr]) node.setAttribute(attr, attrs[attr])
@ -287,6 +269,63 @@ class BookList(list):
book = Book(node, root=self.proot, prefix=self.prefix) book = Book(node, root=self.proot, prefix=self.prefix)
book.datetime = ctime book.datetime = ctime
self.append(book) 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): def write(self, stream):
""" Write XML representation of DOM tree to C{stream} """ """ Write XML representation of DOM tree to C{stream} """

View File

@ -12,6 +12,7 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from StringIO import StringIO
### End point description for PRS-500 procductId=667 ### End point description for PRS-500 procductId=667
### Endpoint Descriptor: ### Endpoint Descriptor:
@ -802,12 +803,12 @@ class PRS500(Device):
else: else:
self.get_file(self.MEDIA_XML, tfile, end_session=False) self.get_file(self.MEDIA_XML, tfile, end_session=False)
bl = BookList(root=root, sfile=tfile) bl = BookList(root=root, sfile=tfile)
paths = bl.purge_corrupted_files() paths = bl.purge_corrupted_files()
for path in paths: for path in paths:
try: try:
self.del_file(path, end_session=False) self.del_file(path, end_session=False)
except PathError: # Incase this is a refetch without a sync in between except PathError: # Incase this is a refetch without a sync in between
continue continue
return bl return bl
@safe @safe
@ -828,8 +829,9 @@ class PRS500(Device):
@param booklists: A tuple containing the result of calls to @param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)). (L{books}(oncard=False), L{books}(oncard=True)).
''' '''
fix_ids(*booklists)
self.upload_book_list(booklists[0], end_session=False) 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) self.upload_book_list(booklists[1], end_session=False)
@safe @safe
@ -940,7 +942,7 @@ class PRS500(Device):
raise ArgumentError("Cannot upload list to card as "+\ raise ArgumentError("Cannot upload list to card as "+\
"card is not present") "card is not present")
path = card + self.CACHE_XML path = card + self.CACHE_XML
f = TemporaryFile() f = StringIO()
booklist.write(f) booklist.write(f)
f.seek(0) f.seek(0)
self.put_file(f, path, replace_file=True, end_session=False) self.put_file(f, path, replace_file=True, end_session=False)

View File

@ -1230,7 +1230,8 @@ class HTMLConverter(object):
self.process_table(tag, tag_css) self.process_table(tag, tag_css)
except Exception, err: except Exception, err:
print 'WARNING: An error occurred while processing a table:', 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.in_table = False
self.process_children(tag, tag_css) self.process_children(tag, tag_css)
else: else:

View File

@ -12,6 +12,7 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 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 import os, textwrap, traceback, time, re, sre_constants
from datetime import timedelta, datetime from datetime import timedelta, datetime
from operator import attrgetter from operator import attrgetter
@ -19,7 +20,7 @@ from math import cos, sin, pi
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \ from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QLineEdit, QApplication, \ QPen, QStyle, QPainter, QLineEdit, QApplication, \
QPalette, QItemSelectionModel QPalette
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
@ -195,6 +196,7 @@ class BooksModel(QAbstractTableModel):
for row in rows: for row in rows:
row = row.row() row = row.row()
au = self.db.authors(row) au = self.db.authors(row)
tags = self.db.tags(row)
if not au: if not au:
au = 'Unknown' au = 'Unknown'
au = au.split(',') au = au.split(',')
@ -204,11 +206,17 @@ class BooksModel(QAbstractTableModel):
au = t au = t
else: else:
au = ' & '.join(au) au = ' & '.join(au)
if not tags:
tags = []
else:
tags = tags.split(',')
mi = { mi = {
'title' : self.db.title(row), 'title' : self.db.title(row),
'authors' : au, 'authors' : au,
'cover' : self.db.cover(row), 'cover' : self.db.cover(row),
'tags' : tags,
} }
metadata.append(mi) metadata.append(mi)
return metadata return metadata
@ -334,7 +342,8 @@ class BooksView(QTableView):
self.setModel(self._model) self.setModel(self._model)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True) 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)'), QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self._model.current_changed) self._model.current_changed)
# Adding and removing rows should resize rows to contents # Adding and removing rows should resize rows to contents
@ -398,6 +407,7 @@ class DeviceBooksModel(BooksModel):
self.unknown = str(self.trUtf8('Unknown')) self.unknown = str(self.trUtf8('Unknown'))
self.marked_for_deletion = {} self.marked_for_deletion = {}
def mark_for_deletion(self, id, rows): def mark_for_deletion(self, id, rows):
self.marked_for_deletion[id] = self.indices(rows) self.marked_for_deletion[id] = self.indices(rows)
for row in rows: for row in rows:
@ -415,16 +425,16 @@ class DeviceBooksModel(BooksModel):
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1]) self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def path_about_to_be_deleted(self, path): 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: if self.db[self.map[row]].path == path:
print row, path #print row, path
#print self.rowCount(None)
self.beginRemoveRows(QModelIndex(), row, row) self.beginRemoveRows(QModelIndex(), row, row)
self.map.pop(row) self.map.pop(row)
self.endRemoveRows()
#print self.rowCount(None)
return return
def path_deleted(self):
self.endRemoveRows()
def indices_to_be_deleted(self): def indices_to_be_deleted(self):
ans = [] ans = []
for v in self.marked_for_deletion.values(): for v in self.marked_for_deletion.values():
@ -433,8 +443,12 @@ class DeviceBooksModel(BooksModel):
def flags(self, index): def flags(self, index):
if self.map[index.row()] in self.indices_to_be_deleted(): 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 Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
return BooksModel.flags(self, index) 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): def search(self, text, refinement, reset=True):
@ -443,7 +457,7 @@ class DeviceBooksModel(BooksModel):
result = [] result = []
for i in base: for i in base:
add = True 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: for token in tokens:
if not token.search(q): if not token.search(q):
add = False add = False
@ -478,8 +492,11 @@ class DeviceBooksModel(BooksModel):
def sizecmp(x, y): def sizecmp(x, y):
x, y = int(self.db[x].size), int(self.db[y].size) x, y = int(self.db[x].size), int(self.db[y].size)
return cmp(x, y) 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 \ 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) self.map.sort(cmp=fcmp, reverse=descending)
if len(self.map) == len(self.db): if len(self.map) == len(self.db):
self.sorted_map = list(self.map) self.sorted_map = list(self.map)
@ -491,7 +508,7 @@ class DeviceBooksModel(BooksModel):
self.reset() self.reset()
def columnCount(self, parent): def columnCount(self, parent):
return 4 return 5
def rowCount(self, parent): def rowCount(self, parent):
return len(self.map) return len(self.map)
@ -516,6 +533,7 @@ class DeviceBooksModel(BooksModel):
dt = datetime(*dt[0:6]) dt = datetime(*dt[0:6])
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
data['Timestamp'] = dt.ctime() data['Timestamp'] = dt.ctime()
data['Tags'] = ', '.join(item.tags)
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data) self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
def paths(self, rows): def paths(self, rows):
@ -556,30 +574,51 @@ class DeviceBooksModel(BooksModel):
dt = datetime(*dt[0:6]) dt = datetime(*dt[0:6])
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QVariant(dt.strftime(BooksView.TIME_FMT)) 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]: elif role == Qt.TextAlignmentRole and index.column() in [2, 3]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
elif role == Qt.ToolTipRole and index.isValid(): elif role == Qt.ToolTipRole and index.isValid():
if self.map[index.row()] in self.indices_to_be_deleted(): if self.map[index.row()] in self.indices_to_be_deleted():
return QVariant('Marked for deletion') 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 QVariant("Double click to <b>edit</b> me<br><br>")
return NONE 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): def setData(self, index, value, role):
done = False done = False
if role == Qt.EditRole: if role == Qt.EditRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
if col in [2, 3]: if col in [2, 3]:
return False return False
val = unicode(value.toString().toUtf8(), 'utf-8').strip() val = qstring_to_unicode(value.toString()).strip()
idx = self.map[row] idx = self.map[row]
if col == 0: if col == 0:
self.db[idx].title = val self.db[idx].title = val
self.db[idx].title_sorter = val self.db[idx].title_sorter = val
elif col == 1: elif col == 1:
self.db[idx].authors = val self.db[idx].authors = val
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ elif col == 4:
index, index) 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()')) self.emit(SIGNAL('booklist_dirtied()'))
if col == self.sorted_on[0]: if col == self.sorted_on[0]:
self.sort(col, self.sorted_on[1]) self.sort(col, self.sorted_on[1])

View File

@ -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 = error_dialog(self.window, 'Error communicating with device', ' ')
self.device_error_dialog.setModal(Qt.NonModal) self.device_error_dialog.setModal(Qt.NonModal)
self.tb_wrapper = textwrap.TextWrapper(width=40) self.tb_wrapper = textwrap.TextWrapper(width=40)
self.device_connected = False
####################### Location View ######################## ####################### Location View ########################
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'), QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
self.location_selected) self.location_selected)
@ -156,7 +157,9 @@ class Main(QObject, Ui_MainWindow):
self.set_default_thumbnail(cls.THUMBNAIL_HEIGHT) self.set_default_thumbnail(cls.THUMBNAIL_HEIGHT)
self.status_bar.showMessage('Device: '+cls.__name__+' detected.', 3000) self.status_bar.showMessage('Device: '+cls.__name__+' detected.', 3000)
self.action_sync.setEnabled(True) self.action_sync.setEnabled(True)
self.device_connected = True
else: else:
self.device_connected = False
self.job_manager.terminate_device_jobs() self.job_manager.terminate_device_jobs()
self.device_manager.device_removed() self.device_manager.device_removed()
self.location_view.model().update_devices() self.location_view.model().update_devices()
@ -326,15 +329,12 @@ class Main(QObject, Ui_MainWindow):
self.device_job_exception(id, description, exception, formatted_traceback) self.device_job_exception(id, description, exception, formatted_traceback)
return return
self.upload_booklists()
if self.delete_memory.has_key(id): if self.delete_memory.has_key(id):
paths, model = self.delete_memory.pop(id) paths, model = self.delete_memory.pop(id)
for path in paths: for path in paths:
model.path_about_to_be_deleted(path) model.path_about_to_be_deleted(path)
self.device_manager.remove_books_from_metadata((path,), self.booklists()) 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 view.resize_on_select = False
self.status_bar.reset_info() self.status_bar.reset_info()
self.current_view().clearSelection() 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): def wrap_traceback(self, tb):

View File

@ -12,8 +12,6 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from libprs500.gui2.dialogs.jobs import JobsDialog
import textwrap import textwrap
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \ from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \
@ -78,29 +76,12 @@ class BookInfoDisplay(QFrame):
self.clear_message() self.clear_message()
self.setVisible(True) 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): class MovieButton(QFrame):
def __init__(self, movie, jobs_dialog): def __init__(self, movie, jobs_dialog):
QFrame.__init__(self) QFrame.__init__(self)
self.setLayout(QVBoxLayout()) self.setLayout(QVBoxLayout())
self.movie_widget = BusyIndicator(movie, jobs_dialog) self.movie_widget = QLabel()
self.movie_widget.setMovie(movie)
self.movie = movie self.movie = movie
self.layout().addWidget(self.movie_widget) self.layout().addWidget(self.movie_widget)
self.jobs = QLabel('<b>Jobs: 0') self.jobs = QLabel('<b>Jobs: 0')
@ -110,6 +91,18 @@ class MovieButton(QFrame):
self.jobs.setMargin(0) self.jobs.setMargin(0)
self.layout().setMargin(0) self.layout().setMargin(0)
self.jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) 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): class StatusBar(QStatusBar):

View File

@ -57,7 +57,7 @@ class LibraryDatabase(object):
Iterator over the books in the old pre 0.4.0 database. Iterator over the books in the old pre 0.4.0 database.
''' '''
conn = sqlite.connect(path) conn = sqlite.connect(path)
cur = conn.execute('select * from books_meta;') cur = conn.execute('select * from books_meta order by id;')
book = cur.fetchone() book = cur.fetchone()
while book: while book:
id = book[0] id = book[0]