diff --git a/libprs500/gui/database.py b/libprs500/gui/database.py
index 17e591f845..5376bac995 100644
--- a/libprs500/gui/database.py
+++ b/libprs500/gui/database.py
@@ -17,12 +17,11 @@ import os, os.path, zlib
from stat import ST_SIZE
from libprs500.lrf.meta import LRFMetaFile
-
class LibraryDatabase(object):
BOOKS_SQL = """
create table if not exists books_meta(id INTEGER PRIMARY KEY, title TEXT, authors TEXT, publisher TEXT, size INTEGER, tags TEXT,
- cover BLOB, date DATE DEFAULT CURRENT_TIMESTAMP, comments TEXT );
+ cover BLOB, date DATE DEFAULT CURRENT_TIMESTAMP, comments TEXT, rating INTEGER);
create table if not exists books_data(id INTEGER, extension TEXT, data BLOB);
"""
@@ -53,7 +52,7 @@ class LibraryDatabase(object):
if "unknown" in author.lower(): author = None
file = zlib.compress(open(file).read())
if cover: cover = sqlite.Binary(zlib.compress(cover))
- self.con.execute("insert into books_meta (title, authors, publisher, size, tags, cover, comments) values (?,?,?,?,?,?)", (title, author, publisher, size, None, cover, None))
+ self.con.execute("insert into books_meta (title, authors, publisher, size, tags, cover, comments, rating) values (?,?,?,?,?,?,?,?)", (title, author, publisher, size, None, cover, None, None))
id = self.con.execute("select max(id) from books_meta").next()[0]
self.con.execute("insert into books_data values (?,?,?)", (id, ext, sqlite.Binary(file)))
self.con.commit()
@@ -66,6 +65,12 @@ class LibraryDatabase(object):
for c in columns: r[c] = row[c]
return r
+ def commit(self): self.con.commit()
+
+ def delete_by_id(self, id):
+ self.con.execute("delete from books_meta where id=?", (id,))
+ self.con.execute("delete from books_data where id=?", (id,))
+
def get_table(self, columns):
cols = ",".join([ c for c in columns])
cur = self.con.execute("select " + cols + " from books_meta")
@@ -102,13 +107,17 @@ class LibraryDatabase(object):
data[field] = row[field]
return data
- def set_metadata(self, id, title=None, authors=None, publisher=None, tags=None, cover=None, comments=None):
+ def set_metadata(self, id, title=None, authors=None, rating=None, publisher=None, tags=None, cover=None, comments=None):
if authors and not len(authors): authors = None
if publisher and not len(publisher): publisher = None
if tags and not len(tags): tags = None
if comments and not len(comments): comments = None
if cover: cover = sqlite.Binary(zlib.compress(cover))
- self.con.execute('update books_meta set title=?, authors=?, publisher=?, tags=?, cover=?, comments=? where id=?', (title, authors, publisher, tags, cover, comments, id))
+ self.con.execute('update books_meta set title=?, authors=?, publisher=?, tags=?, cover=?, comments=?, rating=? where id=?', (title, authors, publisher, tags, cover, comments, rating, id))
+ self.con.commit()
+
+ def set_metadata_item(self, id, col, val):
+ self.con.execute('update books_meta set '+col+'=? where id=?',(val, id))
self.con.commit()
def search(self, query): pass
diff --git a/libprs500/gui/editbook.py b/libprs500/gui/editbook.py
index 6d526086b8..6faa828485 100644
--- a/libprs500/gui/editbook.py
+++ b/libprs500/gui/editbook.py
@@ -56,10 +56,11 @@ class EditBookDialog(Ui_BookEditDialog):
def write_data(self):
title = str(self.title.text()).strip()
authors = str(self.authors.text()).strip()
+ rating = self.rating.value()
tags = str(self.tags.text()).strip()
publisher = str(self.publisher.text()).strip()
comments = str(self.comments.toPlainText()).strip()
- self.db.set_metadata(self.id, title=title, authors=authors, tags=tags, publisher=publisher, comments=comments, cover=self.cover_data)
+ self.db.set_metadata(self.id, title=title, authors=authors, rating=rating, tags=tags, publisher=publisher, comments=comments, cover=self.cover_data)
if self.formats_changed:
for r in range(self.formats.count()):
format = self.formats.item(r)
@@ -116,11 +117,12 @@ class EditBookDialog(Ui_BookEditDialog):
QObject.connect(self.button_box, SIGNAL("accepted()"), self.write_data)
QObject.connect(self.add_format_button, SIGNAL("clicked(bool)"), self.add_format)
QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), self.remove_format)
- data = self.db.get_row_by_id(self.id, ["title","authors","publisher","tags","comments"])
+ data = self.db.get_row_by_id(self.id, ["title","authors","rating","publisher","tags","comments"])
self.title.setText(data["title"])
self.authors.setText(data["authors"] if data["authors"] else "")
self.publisher.setText(data["publisher"] if data["publisher"] else "")
self.tags.setText(data["tags"] if data["tags"] else "")
+ if data["rating"] > 0: self.rating.setValue(data["rating"])
self.comments.setPlainText(data["comments"] if data["comments"] else "")
cover = self.db.get_cover(self.id)
if cover:
diff --git a/libprs500/gui/editbook.ui b/libprs500/gui/editbook.ui
index bdb4f871ce..bbd421d4aa 100644
--- a/libprs500/gui/editbook.ui
+++ b/libprs500/gui/editbook.ui
@@ -6,11 +6,11 @@
0
0
865
- 642
+ 776
- Edit Meta Information
+ SONY Reader - Edit Meta Information
@@ -44,20 +44,75 @@
6
- -
-
+
-
+
- Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
+ Rating of this book. 0-5 stars
+
+
+ Rating of this book. 0-5 stars
+
+
+ QAbstractSpinBox::PlusMinus
+
+
+ stars
+
+
+ 5
- -
+
-
+
+
+ &Rating:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
Change the publisher of this book
+ -
+
+
+ &Publisher:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ publisher
+
+
+
+ -
+
+
+ Ta&gs:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ tags
+
+
+
+ -
+
+
+ Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
+
+
+
-
@@ -72,32 +127,6 @@
- -
-
-
- Ta&gs:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- tags
-
-
-
- -
-
-
- &Publisher:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- publisher
-
-
-
-
@@ -124,7 +153,7 @@
- -
+
-
0
@@ -174,33 +203,7 @@
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 21
-
-
-
-
- -
+
-
@@ -227,6 +230,32 @@
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 21
+
+
+
+
diff --git a/libprs500/gui/main.py b/libprs500/gui/main.py
index 86c4efa84f..f31160c01f 100644
--- a/libprs500/gui/main.py
+++ b/libprs500/gui/main.py
@@ -21,7 +21,8 @@ from editbook import EditBookDialog
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.Qt import QObject, QThread, QCoreApplication, QEventLoop, QString, QStandardItem, QStandardItemModel, QStatusBar, QVariant, QAbstractTableModel, \
- QAbstractItemView, QImage, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog
+ QAbstractItemView, QImage, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox,\
+ QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle
from PyQt4 import uic
import sys, re, string, time, os, os.path, traceback, textwrap, zlib
from stat import ST_SIZE
@@ -30,6 +31,7 @@ from exceptions import Exception as Exception
import xml.dom.minidom as dom
from xml.dom.ext import PrettyPrint as PrettyPrint
from operator import itemgetter
+from math import sin, cos, pi
DEFAULT_BOOK_COVER = None
NONE = QVariant()
@@ -50,8 +52,95 @@ def human_readable(size):
def wrap(s, width=20):
return textwrap.fill(str(s), width)
+class LibraryDelegate(QItemDelegate):
+ COLOR = QColor("blue")
+ SIZE = 16
+ PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
+
+ def __init__(self, parent):
+ QItemDelegate.__init__(self, parent)
+ self.star_path = QPainterPath()
+ self.star_path.moveTo(90, 50)
+ for i in range(1, 5):
+ self.star_path.lineTo(50 + 40 * cos(0.8 * i * pi), 50 + 40 * sin(0.8 * i * pi))
+ self.star_path.closeSubpath()
+ self.star_path.setFillRule(Qt.WindingFill)
+ gradient = QLinearGradient(0, 0, 0, 100)
+ gradient.setColorAt(0.0, self.COLOR)
+ gradient.setColorAt(1.0, self.COLOR)
+ self. brush = QBrush(gradient)
+ self.factor = self.SIZE/100.
+
+
+ def sizeHint(self, option, index):
+ if index.column() != 4:
+ return QItemDelegate.sizeHint(self, option, index)
+ num = index.model().data(index, Qt.DisplayRole).toInt()[0]
+ return QSize(num*(self.SIZE), self.SIZE+4)
+
+ def paint(self, painter, option, index):
+ if index.column() != 4:
+ return QItemDelegate.paint(self, painter, option, index)
+ num = index.model().data(index, Qt.DisplayRole).toInt()[0]
+ def draw_star():
+ painter.save()
+ painter.scale(self.factor, self.factor)
+ painter.translate(50.0, 50.0)
+ painter.rotate(-20)
+ painter.translate(-50.0, -50.0)
+ painter.drawPath(self.star_path)
+ painter.restore()
+
+ painter.save()
+ try:
+ if option.state & QStyle.State_Selected:
+ painter.fillRect(option.rect, option.palette.highlight())
+ painter.setRenderHint(QPainter.Antialiasing)
+ y = option.rect.center().y()-self.SIZE/2.
+ x = option.rect.right() - self.SIZE
+ painter.setPen(self.PEN)
+ painter.setBrush(self.brush)
+ painter.translate(x, y)
+ for i in range(num):
+ draw_star()
+ painter.translate(-self.SIZE, 0)
+ except Exception, e:
+ traceback.print_exc(e)
+ painter.restore()
+
+ def createEditor(self, parent, option, index):
+ if index.column() != 4:
+ return QItemDelegate.createEditor(self, parent, option, index)
+ print "hello"
+ editor = QSpinBox(parent)
+ editor.setSuffix(" stars")
+ editor.setMinimum(0)
+ editor.setMaximum(5)
+ editor.installEventFilter(self)
+ return editor
+
+ def setEditorData(self, editor, index):
+ if index.column() != 4:
+ return QItemDelegate.setEditorData(self, editor, index)
+ val = index.model()._data[index.row()]["rating"]
+ if not val: val = 0
+ editor.setValue(val)
+
+ def setModelData(self, editor, model, index):
+ if index.column() != 4:
+ return QItemDelegate.setModelData(self, editor, model, index)
+ editor.interpretText()
+ index.model().setData(index, QVariant(editor.value()), Qt.EditRole)
+
+ def updateEditorGeometry(self, editor, option, index):
+ if index.column() != 4:
+ return QItemDelegate.updateEditorGeometry(self, editor, option, index)
+ editor.setGeometry(option.rect)
+
+
+
class LibraryBooksModel(QAbstractTableModel):
- FIELDS = ["id", "title", "authors", "size", "date", "publisher", "tags"]
+ FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", "tags"]
TIME_READ_FMT = "%Y-%m-%d %H:%M:%S"
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
@@ -63,6 +152,40 @@ class LibraryBooksModel(QAbstractTableModel):
def rowCount(self, parent): return len(self._data)
def columnCount(self, parent): return len(self.FIELDS)-2
+ def setData(self, index, value, role):
+ done = False
+ if role == Qt.EditRole:
+ row = index.row()
+ id = self._data[row]["id"]
+ col = index.column()
+ val = str(value.toString())
+ if col == 0: col = "title"
+ elif col == 1: col = "authors"
+ elif col == 2: return False
+ elif col == 3: return False
+ elif col == 4:
+ col, val = "rating", int(value.toInt()[0])
+ if val < 0: val =0
+ if val > 5: val = 5
+ elif col == 5: col = "publisher"
+ else: return False
+ self.db.set_metadata_item(id, col, val)
+ self._data[row][col] = val
+ self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
+ for i in range(len(self._orig_data)):
+ if self._orig_data[i]["id"] == self._data[row]["id"]:
+ self._orig_data[i][col] = self._data[row][col]
+ break
+ done = True
+ return done
+
+ def flags(self, index):
+ flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
+ col = index.column()
+ if col not in [2,3]:
+ flags |= Qt.ItemIsEditable
+ return flags
+
def set_data(self, db):
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.db = db
@@ -79,7 +202,8 @@ class LibraryBooksModel(QAbstractTableModel):
elif section == 1: text = "Author(s)"
elif section == 2: text = "Size"
elif section == 3: text = "Date"
- elif section == 4: text = "Publisher"
+ elif section == 4: text = "Rating"
+ elif section == 5: text = "Publisher"
return QVariant(self.trUtf8(text))
else: return QVariant(str(1+section))
@@ -103,23 +227,32 @@ class LibraryBooksModel(QAbstractTableModel):
def refresh_row(self, row):
self._data[row] = self.db.get_row_by_id(self._data[row]["id"], self.FIELDS)
+ for i in range(len(self._orig_data)):
+ if self._orig_data[i]["id"] == self._data[row]["id"]:
+ self._orig_data[i:i+1] = self._data[row]
+ break
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(row, 0), self.index(row, self.columnCount(0)-1))
def data(self, index, role):
- if role == Qt.DisplayRole:
+ if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column()
text = None
row = self._data[row]
+ if col == 4:
+ r = row["rating"] if row["rating"] else 0
+ if r < 0: r= 0
+ if r > 5: r=5
+ return QVariant(r)
if col == 0: text = wrap(row["title"], width=25)
elif col == 1:
au = row["authors"]
if au : text = wrap(re.sub("&", "\n", au), width=25)
elif col == 2: text = human_readable(row["size"])
elif col == 3: text = time.strftime(TIME_WRITE_FMT, time.strptime(row["date"], self.TIME_READ_FMT))
- elif col == 4:
+ elif col == 5:
pub = row["publisher"]
if pub: text = wrap(pub, 20)
- if not text: text = "Unknown"
+ if text == None: text = "Unknown"
return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]:
return QVariant(Qt.AlignRight)
@@ -132,7 +265,8 @@ class LibraryBooksModel(QAbstractTableModel):
if col == 1: key, func = "authors", lambda x : x.split()[-1:][0].lower() if x else ""
if col == 2: key, func = "size", int
if col == 3: key, func = "date", lambda x: time.mktime(time.strptime(x, self.TIME_READ_FMT))
- if col == 4: key, func = "publisher", lambda x : x.lower() if x else ""
+ if col == 4: key, func = "rating", lambda x: x if x else 0
+ if col == 5: key, func = "publisher", lambda x : x.lower() if x else ""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data.sort(key=getter(key, func))
if descending: self._data.reverse()
@@ -159,6 +293,20 @@ class LibraryBooksModel(QAbstractTableModel):
if match: self._data.append(book)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("searched()"))
+
+ def delete(self, indices):
+ if len(indices): self.emit(SIGNAL("layoutAboutToBeChanged()"))
+ for index in indices:
+ id = self.id_from_index(index)
+ self.db.delete_by_id(id)
+ row = index.row()
+ self._data[row:row+1] = []
+ for i in range(len(self._orig_data)):
+ if self._orig_data[i]["id"] == id:
+ self._orig_data[i:i+1] = []
+ i -=1
+ self.emit(SIGNAL("layoutChanged()"))
+ self.db.commit()
class DeviceBooksModel(QAbstractTableModel):
TIME_READ_FMT = "%a, %d %b %Y %H:%M:%S %Z"
@@ -311,12 +459,20 @@ class MainWindow(QObject, Ui_MainWindow):
self.device_view.resizeColumnsToContents()
def model_modified(self):
- self.device_view.clearSelection()
- self.library_view.clearSelection()
+ if self.library_view.isVisible(): view = self.library_view
+ else: view = self.device_view
+ view.clearSelection()
+ view.resizeColumnsToContents()
self.book_cover.hide()
self.book_info.hide()
QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
+ def resize_columns(self, topleft, bottomright):
+ if self.library_view.isVisible(): view = self.library_view
+ else: view = self.device_view
+ for c in range(topleft.column(), bottomright.column()+1):
+ view.resizeColumnToContents(c)
+
def show_book(self, current, previous):
title, author, size, mime, thumbnail = current.model().info(current.row())
self.book_info.setText(self.BOOK_TEMPLATE.arg(title).arg(size).arg(author).arg(mime))
@@ -332,48 +488,49 @@ class MainWindow(QObject, Ui_MainWindow):
def list_context_event(self, event):
print "TODO:"
- def do_delete(self, rows):
- if self.device_model.__class__.__name__ == "DeviceBooksdevice_model":
- paths, mc, cc = [], False, False
- for book in rows:
- path = book.model().path(book)
- if path[0] == "/": file, prefix, mc = self.main_xml, "xs1:", True
- else: file, prefix, cc = self.cache_xml, "", True
- file.seek(0)
- document = dom.parse(file)
- books = document.getElementsByTagName(prefix + "text")
- for candidate in books:
- if candidate.attributes["path"].value in path:
- paths.append(path)
- candidate.parentNode.removeChild(candidate)
- break
- file.close()
- file = TemporaryFile()
- PrettyPrint(document, file)
- if len(prefix) > 0: self.main_xml = file
- else: self.cache_xml = file
- for path in paths:
- self.dev.del_file(path)
- self.device_model.delete_by_path(path)
- self.cache_xml.seek(0)
- self.main_xml.seek(0)
- self.status("Files deleted. Updating media list on device")
- if mc:
- self.dev.del_file(self.dev.MEDIA_XML)
- self.dev.put_file(self.main_xml, self.dev.MEDIA_XML)
- if cc:
- self.dev.del_file(self.card+self.dev.CACHE_XML)
- self.dev.put_file(self.cache_xml, self.card+self.dev.CACHE_XML)
+
def delete(self, action):
- self.window.setCursor(Qt.WaitCursor)
- rows = self.device_view.selectionModel().selectedRows()
- items = [ row.model().title(row) + ": " + row.model().path(row)[row.model().path(row).rfind("/")+1:] for row in rows ]
- ret = QMessageBox.question(self.window, self.trUtf8("SONY Reader - confirm"), self.trUtf8("Are you sure you want to delete these items from the device?\n\n") + "\n".join(items),
- QMessageBox.YesToAll | QMessageBox.No, QMessageBox.YesToAll)
- if ret == QMessageBox.YesToAll:
- self.do_delete(rows)
- self.window.setCursor(Qt.ArrowCursor)
+ if self.device_view.isVisible():
+ rows = self.device_view.selectionModel().selectedRows()
+ items = [ row.model().title(row) + ": " + row.model().path(row)[row.model().path(row).rfind("/")+1:] for row in rows ]
+ ret = QMessageBox.question(self.window, self.trUtf8("SONY Reader - confirm"), self.trUtf8("Are you sure you want to delete these items from the device?\n\n") + "\n".join(items),
+ QMessageBox.YesToAll | QMessageBox.No, QMessageBox.YesToAll)
+ if ret == QMessageBox.YesToAll:
+ self.window.setCursor(Qt.WaitCursor)
+ paths, mc, cc = [], False, False
+ for book in rows:
+ path = book.model().path(book)
+ if path[0] == "/": file, prefix, mc = self.main_xml, "xs1:", True
+ else: file, prefix, cc = self.cache_xml, "", True
+ file.seek(0)
+ document = dom.parse(file)
+ books = document.getElementsByTagName(prefix + "text")
+ for candidate in books:
+ if candidate.attributes["path"].value in path:
+ paths.append(path)
+ candidate.parentNode.removeChild(candidate)
+ break
+ file.close()
+ file = TemporaryFile()
+ PrettyPrint(document, file)
+ if len(prefix) > 0: self.main_xml = file
+ else: self.cache_xml = file
+ for path in paths:
+ self.dev.del_file(path)
+ self.device_model.delete_by_path(path)
+ self.cache_xml.seek(0)
+ self.main_xml.seek(0)
+ self.status("Files deleted. Updating media list on device")
+ if mc:
+ self.dev.del_file(self.dev.MEDIA_XML)
+ self.dev.put_file(self.main_xml, self.dev.MEDIA_XML)
+ if cc:
+ self.dev.del_file(self.card+self.dev.CACHE_XML)
+ self.dev.put_file(self.cache_xml, self.card+self.dev.CACHE_XML)
+ self.window.setCursor(Qt.ArrowCursor)
+ else:
+ self.library_model.delete(self.library_view.selectionModel().selectedRows())
def read_settings(self):
settings = QSettings()
@@ -451,15 +608,16 @@ class MainWindow(QObject, Ui_MainWindow):
self.library_view.setSelectionBehavior(QAbstractItemView.SelectRows)
self.library_view.setSortingEnabled(True)
self.library_view.contextMenuEvent = self.list_context_event
+ self.library_view.setItemDelegate(LibraryDelegate(self.library_view))
QObject.connect(self.library_model, SIGNAL("layoutChanged()"), self.library_view.resizeRowsToContents)
QObject.connect(self.library_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
QObject.connect(self.search, SIGNAL("textChanged(QString)"), self.library_model.search)
QObject.connect(self.library_model, SIGNAL("sorted()"), self.model_modified)
QObject.connect(self.library_model, SIGNAL("searched()"), self.model_modified)
QObject.connect(self.library_model, SIGNAL("deleted()"), self.model_modified)
+ QObject.connect(self.library_model, SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns)
self.library_view.resizeColumnsToContents()
-
# Create Device list
self.tree = QStandardItemModel()
library = QStandardItem(QString("Library"))
@@ -504,6 +662,7 @@ class MainWindow(QObject, Ui_MainWindow):
QObject.connect(self.device_model, SIGNAL("sorted()"), self.model_modified)
QObject.connect(self.device_model, SIGNAL("searched()"), self.model_modified)
QObject.connect(self.device_model, SIGNAL("deleted()"), self.model_modified)
+ QObject.connect(self.device_model, SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns)
self.device_view.hide()
# Setup book display
@@ -612,3 +771,4 @@ def main():
ret = app.exec_()
return ret
+if __name__ == "__main__": main()