mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-03-06 00:43:42 -05:00
The basic workflow using drag and drop is in place. You can now drop files from your file manager into the library and drop files from the library into either the main memory or the card.
I've lowered the version number from 0.3.0 to 0.3.0a1 as the GUI still has many bugs, so you need to do a python setup.py develop --uninstall and then re-issue the develop command.
This commit is contained in:
parent
8e9194a575
commit
2f0de66c96
@ -32,7 +32,7 @@ the following rule in C{/etc/udev/rules.d/90-local.rules} ::
|
||||
BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="plugdev"
|
||||
You may have to adjust the GROUP and the location of the rules file to suit your distribution.
|
||||
"""
|
||||
__version__ = "0.3.0"
|
||||
__version__ = "0.3.0a1"
|
||||
__docformat__ = "epytext"
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
TEMPORARY_FILENAME_TEMPLATE = "libprs500_"+__version__+"_temp"
|
||||
|
||||
@ -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.
|
||||
""" This module contains the logic for dealing with XML book lists found in the reader cache """
|
||||
from xml.dom.ext import PrettyPrint as PrettyPrint
|
||||
from xml.dom.ext import PrettyPrint
|
||||
import xml.dom.minidom as dom
|
||||
from base64 import b64decode as decode
|
||||
from base64 import b64encode as encode
|
||||
@ -25,6 +25,7 @@ class book_metadata_field(object):
|
||||
def __init__(self, attr, formatter=None, setter=None):
|
||||
self.attr = attr
|
||||
self.formatter = formatter
|
||||
self.setter = setter
|
||||
|
||||
def __get__(self, obj, typ=None):
|
||||
""" Return a string. String may be empty if self.attr is absent """
|
||||
@ -40,8 +41,10 @@ class Book(object):
|
||||
mime = book_metadata_field("mime")
|
||||
rpath = book_metadata_field("path")
|
||||
id = book_metadata_field("id", formatter=int)
|
||||
sourceid = book_metadata_field("sourceid", formatter=int)
|
||||
size = book_metadata_field("size", formatter=int)
|
||||
datetime = book_metadata_field("date", formatter=lambda x: time.strptime(x, "%a, %d %b %Y %H:%M:%S %Z"), setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S %Z", x))
|
||||
# When setting this attribute you must use an epoch
|
||||
datetime = book_metadata_field("date", formatter=lambda x: time.strptime(x, "%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 thumbnail():
|
||||
@ -74,6 +77,23 @@ class Book(object):
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
def fix_ids(media, cache):
|
||||
id = 0
|
||||
for child in media.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
child.setAttribute("id", str(id))
|
||||
id += 1
|
||||
mmaxid = id - 1
|
||||
id = mmaxid + 2
|
||||
if len(cache):
|
||||
for child in cache.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("sourceid"):
|
||||
child.setAttribute("sourceid", str(mmaxid+1))
|
||||
child.setAttribute("id", str(id))
|
||||
id += 1
|
||||
media.document.documentElement.setAttribute("nextID", str(id))
|
||||
|
||||
class BookList(list):
|
||||
__getslice__ = None
|
||||
__setslice__ = None
|
||||
@ -82,27 +102,62 @@ class BookList(list):
|
||||
list.__init__(self)
|
||||
if file:
|
||||
self.prefix = prefix
|
||||
self.root = root
|
||||
self.proot = root
|
||||
file.seek(0)
|
||||
self.document = dom.parse(file)
|
||||
self.root = self.document.documentElement #: The root element containing all records
|
||||
if prefix == "xs1:": self.root = self.root.getElementsByTagName("records")[0]
|
||||
for book in self.document.getElementsByTagName(self.prefix + "text"): self.append(Book(book, root=root, prefix=prefix))
|
||||
|
||||
def add_book(self, info, name):
|
||||
root = self.document.documentElement
|
||||
def max_id(self):
|
||||
id = -1
|
||||
for child in self.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
c = int(child.getAttribute("id"))
|
||||
if c > id: id = c
|
||||
return id
|
||||
|
||||
def has_id(self, id):
|
||||
""" Check if a book with id C{ == id} exists already. This *does not* check if id exists in the underlying XML file """
|
||||
ans = False
|
||||
for book in self:
|
||||
if book.id == id:
|
||||
ans = True
|
||||
break
|
||||
return ans
|
||||
|
||||
def delete_book(self, id):
|
||||
node = None
|
||||
for book in self:
|
||||
if book.id == id:
|
||||
node = book
|
||||
self.remove(book)
|
||||
break
|
||||
node.elem.parentNode.removeChild(node.elem)
|
||||
node.elem.unlink()
|
||||
|
||||
def add_book(self, info, name, size, ctime):
|
||||
node = self.document.createElement(self.prefix + "text")
|
||||
mime = MIME_MAP[name[name.rfind(".")+1:]]
|
||||
id = 0
|
||||
for book in self:
|
||||
if book.id > id:
|
||||
id = book.id
|
||||
break
|
||||
attrs = { "title":info["title"], "author":info["authors"], "page":"0", "part":"0", "scale":"0", "sourceid":"1", "id":str(id), "date":"", "mime":mime, "path":name, "size":str(size)}
|
||||
for attr in attrs.keys():
|
||||
id = self.max_id()+1
|
||||
sourceid = str(self[0].sourceid) if len(self) else "1"
|
||||
attrs = { "title":info["title"], "author":info["authors"], "page":"0", "part":"0", "scale":"0", "sourceid":sourceid, "id":str(id), "date":"", "mime":mime, "path":name, "size":str(size)}
|
||||
for attr in attrs.keys():
|
||||
node.setAttributeNode(self.document.createAttribute(attr))
|
||||
node.setAttribute(attr, attrs[attr])
|
||||
book = Book(node, root=self.root, prefix=self.prefix)
|
||||
book.datetime = time.gmtime()
|
||||
w, h, data = info["cover"]
|
||||
if data:
|
||||
th = self.document.createElement(self.prefix + "thumbnail")
|
||||
th.setAttribute("width", str(w))
|
||||
th.setAttribute("height", str(h))
|
||||
jpeg = self.document.createElement(self.prefix + "jpeg")
|
||||
jpeg.appendChild(self.document.createTextNode(encode(data)))
|
||||
th.appendChild(jpeg)
|
||||
node.appendChild(th)
|
||||
self.root.appendChild(node)
|
||||
book = Book(node, root=self.proot, prefix=self.prefix)
|
||||
book.datetime = ctime
|
||||
self.append(book)
|
||||
|
||||
|
||||
|
||||
def write(self, stream):
|
||||
PrettyPrint(self.document, stream)
|
||||
|
||||
@ -651,9 +651,9 @@ class PRS500Device(object):
|
||||
size = infile.tell()
|
||||
infile.seek(0)
|
||||
card = self.card(end_session=False)
|
||||
space = self.available_space(end_session=False)
|
||||
mspace = space[0][1]
|
||||
cspace = space[1][1] if space[1][1] >= space[2][1] else space[2][1]
|
||||
space = self.free_space(end_session=False)
|
||||
mspace = space[0]
|
||||
cspace = space[1] if space[1] >= space[2] else space[2]
|
||||
if oncard and size > cspace - 1024*1024: raise FreeSpaceError("There is insufficient free space on the storage card")
|
||||
if not oncard and size > mspace - 1024*1024: raise FreeSpaceError("There is insufficient free space in main memory")
|
||||
prefix = "/Data/media/"
|
||||
|
||||
@ -17,8 +17,8 @@ __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
APP_TITLE = "libprs500"
|
||||
|
||||
import pkg_resources, sys, os, re, StringIO, traceback
|
||||
from PyQt4 import QtCore, QtGui # Needed for classes imported with import_ui
|
||||
from PyQt4.uic.Compiler import compiler
|
||||
from PyQt4 import QtCore, QtGui # Needed in globals() for import_ui
|
||||
|
||||
error_dialog = None
|
||||
|
||||
@ -45,7 +45,7 @@ def Error(msg, e):
|
||||
error_dialog.showMessage(msg)
|
||||
error_dialog.show()
|
||||
|
||||
def import_ui(name):
|
||||
def import_ui(name):
|
||||
uifile = pkg_resources.resource_stream(__name__, name)
|
||||
code_string = StringIO.StringIO()
|
||||
winfo = compiler.UICompiler().compileUi(uifile, code_string)
|
||||
@ -53,4 +53,4 @@ def import_ui(name):
|
||||
exec code_string.getvalue()
|
||||
return locals()[winfo["uiclass"]]
|
||||
|
||||
from libprs500.gui.widgets import LibraryBooksView, DeviceBooksView, CoverDisplay, DeviceView # Needed for import_ui
|
||||
from libprs500.gui.widgets import LibraryBooksView, DeviceBooksView, CoverDisplay, DeviceView # Needed in globals() for import_ui
|
||||
|
||||
@ -12,12 +12,13 @@
|
||||
## 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 PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QSettings, QVariant, QSize, QEventLoop, QString
|
||||
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QSettings, QVariant, QSize, QEventLoop, QString, QBuffer, QIODevice, QModelIndex
|
||||
from PyQt4.QtGui import QPixmap, QAbstractItemView, QErrorMessage, QMessageBox, QFileDialog, QIcon
|
||||
from PyQt4.Qt import qInstallMsgHandler, qDebug, qFatal, qWarning, qCritical
|
||||
from PyQt4 import uic
|
||||
|
||||
from libprs500.communicate import PRS500Device as device
|
||||
from libprs500.books import fix_ids
|
||||
from libprs500.errors import *
|
||||
from libprs500.lrf.meta import LRFMetaFile, LRFException
|
||||
from libprs500.gui import import_ui, installErrorHandler, Error, Warning, extension, APP_TITLE
|
||||
@ -36,7 +37,7 @@ class MainWindow(QObject, Ui_MainWindow):
|
||||
|
||||
def show_device(self, yes):
|
||||
""" If C{yes} show the items on the device otherwise show the items in the library """
|
||||
self.device_view.clearSelection(), self.library_view.clearSelection()
|
||||
self.device_view.selectionModel().reset(), self.library_view.selectionModel().reset()
|
||||
self.book_cover.hide(), self.book_info.hide()
|
||||
if yes:
|
||||
self.device_view.show(), self.library_view.hide()
|
||||
@ -80,7 +81,7 @@ class MainWindow(QObject, Ui_MainWindow):
|
||||
for c in range(topleft.column(), bottomright.column()+1):
|
||||
view.resizeColumnToContents(c)
|
||||
|
||||
def show_book(self, current, previous):
|
||||
def show_book(self, current, previous):
|
||||
if self.library_view.isVisible():
|
||||
formats, tags, comments, cover = current.model().info(current.row())
|
||||
data = LIBRARY_BOOK_TEMPLATE.arg(formats).arg(tags).arg(comments)
|
||||
@ -101,50 +102,28 @@ class MainWindow(QObject, Ui_MainWindow):
|
||||
self.show_book(index, index)
|
||||
|
||||
def delete(self, action):
|
||||
count = str(len(self.current_view.selectionModel().selectedRows()))
|
||||
rows = self.current_view.selectionModel().selectedRows()
|
||||
if not len(rows): return
|
||||
count = str(len(rows))
|
||||
ret = QMessageBox.question(self.window, self.trUtf8(APP_TITLE + " - confirm"), self.trUtf8("Are you sure you want to <b>permanently delete</b> these ") +count+self.trUtf8(" item(s)?"), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
||||
if ret != QMessageBox.Yes: return
|
||||
self.window.setCursor(Qt.WaitCursor)
|
||||
if self.library_view.isVisible():
|
||||
self.library_model.delete(self.library_view.selectionModel().selectedRows())
|
||||
else:
|
||||
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 ]
|
||||
|
||||
if ret == QMessageBox.YesToAll:
|
||||
|
||||
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)
|
||||
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.status("Deleting files from device")
|
||||
paths = self.device_view.model().delete(rows)
|
||||
for path in paths:
|
||||
self.status("Deleting "+path[path.rfind("/")+1:])
|
||||
self.dev.del_file(path, end_session=False)
|
||||
fix_ids(self.reader_model.booklist, self.card_model.booklist)
|
||||
self.status("Syncing media list to reader")
|
||||
self.dev.upload_book_list(self.reader_model.booklist)
|
||||
if len(self.card_model.booklist):
|
||||
self.status("Syncing media list to card")
|
||||
self.dev.upload_book_list(self.card_model.booklist)
|
||||
self.update_availabe_space()
|
||||
self.show_book(self.current_view.currentIndex(), QModelIndex())
|
||||
self.window.setCursor(Qt.ArrowCursor)
|
||||
|
||||
def read_settings(self):
|
||||
@ -207,6 +186,8 @@ class MainWindow(QObject, Ui_MainWindow):
|
||||
except Exception, e: Error("Unable to change cover", e)
|
||||
|
||||
def upload_books(self, to, files, ids):
|
||||
oncard = False if to == "reader" else True
|
||||
booklists = (self.reader_model.booklist, self.card_model.booklist)
|
||||
def update_models():
|
||||
hv = self.device_view.horizontalHeader()
|
||||
col = hv.sortIndicatorSection()
|
||||
@ -216,52 +197,82 @@ class MainWindow(QObject, Ui_MainWindow):
|
||||
if self.device_view.isVisible() and self.device_view.model() == model: self.search.clear()
|
||||
else: model.search("")
|
||||
|
||||
def sync_lists():
|
||||
self.status("Syncing media list to device main memory")
|
||||
self.dev.upload_book_list(booklists[0])
|
||||
if len(booklists[1]):
|
||||
self.status("Syncing media list to storage card")
|
||||
self.dev.upload_book_list(booklists[1])
|
||||
|
||||
self.window.setCursor(Qt.WaitCursor)
|
||||
oncard = False if to == "reader" else True
|
||||
ename = "file"
|
||||
booklists = (self.reader_model._orig_data, self.card_model._orig_data)
|
||||
ename = "file"
|
||||
try:
|
||||
if ids:
|
||||
for id in ids:
|
||||
formats = []
|
||||
info = self.library_view.model().book_info(id, ["title", "authors", "cover"])
|
||||
info = self.library_view.model().book_info(id)
|
||||
if info["cover"]:
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(str(info["cover"]))
|
||||
if pix.isNull(): pix = DEFAULT_BOOK_COVER
|
||||
pix = pix.scaledToHeight(self.dev.THUMBNAIL_HEIGHT, Qt.SmoothTransformation)
|
||||
buffer = QBuffer()
|
||||
buffer.open(QIODevice.WriteOnly)
|
||||
pix.save(buffer, "JPEG")
|
||||
info["cover"] = (pix.width(), pix.height(), str(buffer.buffer()))
|
||||
ename = info["title"]
|
||||
for f in files:
|
||||
if re.match("......_"+str(id)+"_", f):
|
||||
if re.match("......_"+str(id)+"_", os.path.basename(f)):
|
||||
formats.append(f)
|
||||
file = None
|
||||
try:
|
||||
for format in self.dev.FORMATS:
|
||||
for f in formats:
|
||||
if extension(format) == format:
|
||||
if extension(f) == format:
|
||||
file = f
|
||||
raise StopIteration()
|
||||
except StopIteration: pass
|
||||
if not file:
|
||||
Error("The library does not have any compatible formats for " + ename)
|
||||
Error("The library does not have any formats that can be viewed on the device for " + ename, None)
|
||||
continue
|
||||
f = open(file, "rb")
|
||||
self.status("Sending "+info["title"]+" to device")
|
||||
try:
|
||||
self.dev.add_book(f, "libprs500_"+str(id)+"."+extension(file), info, booklists, oncard=oncard)
|
||||
self.dev.add_book(f, "libprs500_"+str(id)+"."+extension(file), info, booklists, oncard=oncard, end_session=False)
|
||||
update_models()
|
||||
except PathError, e:
|
||||
if "already exists" in str(e):
|
||||
Error(info["title"] + " already exists on the device", None)
|
||||
self.progress(100)
|
||||
continue
|
||||
else: raise
|
||||
finally: f.close()
|
||||
sync_lists()
|
||||
else:
|
||||
for file in files:
|
||||
ename = file
|
||||
if extension(file) not in self.dev.FORMATS:
|
||||
Error(ename + " is not in a supported format")
|
||||
continue
|
||||
info = { "title":file, "authors":"Unknown", cover:None }
|
||||
info = { "title":os.path.basename(file), "authors":"Unknown", "cover":(None, None, None) }
|
||||
f = open(file, "rb")
|
||||
self.status("Sending "+info["title"]+" to device")
|
||||
try:
|
||||
self.dev.add_book(f, os.path.basename(file), info, booklists, oncard=oncard)
|
||||
self.dev.add_book(f, os.path.basename(file), info, booklists, oncard=oncard, end_session=False)
|
||||
update_models()
|
||||
except PathError, e:
|
||||
if "already exists" in str(e):
|
||||
Error(info["title"] + " already exists on the device", None)
|
||||
self.progress(100)
|
||||
continue
|
||||
else: raise
|
||||
finally: f.close()
|
||||
sync_lists()
|
||||
except Exception, e:
|
||||
Error("Unable to send "+ename+" to device", e)
|
||||
finally: self.window.setCursor(Qt.WaitCursor)
|
||||
finally:
|
||||
self.window.setCursor(Qt.ArrowCursor)
|
||||
self.update_availabe_space()
|
||||
|
||||
def __init__(self, window, log_packets):
|
||||
QObject.__init__(self)
|
||||
@ -377,13 +388,9 @@ class MainWindow(QObject, Ui_MainWindow):
|
||||
traceback.print_exc(e)
|
||||
qFatal("Unable to connect to device. Please try unplugging and reconnecting it")
|
||||
self.df.setText(self.df_template.arg("Connected: "+info[0]).arg(info[1]).arg(info[2]))
|
||||
space = self.dev.available_space(end_session=False)
|
||||
sc = space[1][1] if space[1][1] else space[2][1]
|
||||
self.device_tree.model().update_free_space(space[0][1], sc)
|
||||
self.is_connected = True
|
||||
if space[1][2] > 0: self.card = "a:"
|
||||
elif space[2][2] > 0: self.card = "b:"
|
||||
else: self.card = None
|
||||
self.update_availabe_space(end_session=False)
|
||||
self.card = self.dev.card()
|
||||
self.is_connected = True
|
||||
if self.card: self.device_tree.hide_card(False)
|
||||
else: self.device_tree.hide_card(True)
|
||||
self.device_tree.hide_reader(False)
|
||||
@ -394,6 +401,20 @@ class MainWindow(QObject, Ui_MainWindow):
|
||||
self.progress(100)
|
||||
self.window.setCursor(Qt.ArrowCursor)
|
||||
|
||||
def update_availabe_space(self, end_session=True):
|
||||
space = self.dev.free_space(end_session=end_session)
|
||||
sc = space[1] if int(space[1])>0 else space[2]
|
||||
self.device_tree.model().update_free_space(space[0], sc)
|
||||
|
||||
class LockFile(object):
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
f =open(path, "w")
|
||||
f.close()
|
||||
|
||||
def __del__(self):
|
||||
if os.access(self.path, os.F_OK): os.remove(self.path)
|
||||
|
||||
def main():
|
||||
from optparse import OptionParser
|
||||
from libprs500 import __version__ as VERSION
|
||||
@ -418,11 +439,7 @@ def main():
|
||||
QCoreApplication.setOrganizationName("KovidsBrain")
|
||||
QCoreApplication.setApplicationName(APP_TITLE)
|
||||
gui = MainWindow(window, options.log_packets)
|
||||
f = open(lock, "w")
|
||||
f.close()
|
||||
try:
|
||||
ret = app.exec_()
|
||||
finally: os.remove(lock)
|
||||
return ret
|
||||
lock = LockFile(lock)
|
||||
return app.exec_()
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
if __name__ == "__main__": sys.exit(main())
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>728</width>
|
||||
<height>711</height>
|
||||
<height>822</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy" >
|
||||
@ -99,6 +99,64 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" >
|
||||
<property name="margin" >
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="spacing" >
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label" >
|
||||
<property name="text" >
|
||||
<string>&Search:</string>
|
||||
</property>
|
||||
<property name="buddy" >
|
||||
<cstring>search</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search" >
|
||||
<property name="enabled" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="acceptDrops" >
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip" >
|
||||
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
||||
</property>
|
||||
<property name="whatsThis" >
|
||||
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
||||
</property>
|
||||
<property name="autoFillBackground" >
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<string/>
|
||||
</property>
|
||||
<property name="frame" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="clear_button" >
|
||||
<property name="toolTip" >
|
||||
<string>Reset Quick Search</string>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon" >
|
||||
<iconset resource="images.qrc" >:/images/clear.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" >
|
||||
<property name="margin" >
|
||||
@ -169,64 +227,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" >
|
||||
<property name="margin" >
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="spacing" >
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label" >
|
||||
<property name="text" >
|
||||
<string>&Search:</string>
|
||||
</property>
|
||||
<property name="buddy" >
|
||||
<cstring>search</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search" >
|
||||
<property name="enabled" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="acceptDrops" >
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip" >
|
||||
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
||||
</property>
|
||||
<property name="whatsThis" >
|
||||
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
||||
</property>
|
||||
<property name="autoFillBackground" >
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<string/>
|
||||
</property>
|
||||
<property name="frame" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="clear_button" >
|
||||
<property name="toolTip" >
|
||||
<string>Reset Quick Search</string>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon" >
|
||||
<iconset resource="images.qrc" >:/images/clear.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" >
|
||||
<property name="margin" >
|
||||
@ -348,6 +348,11 @@
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>LibraryBooksView</class>
|
||||
<extends>QTableView</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DeviceBooksView</class>
|
||||
<extends>QTableView</extends>
|
||||
@ -363,11 +368,6 @@
|
||||
<extends>QListView</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>LibraryBooksView</class>
|
||||
<extends>QTableView</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="images.qrc" />
|
||||
|
||||
@ -12,6 +12,16 @@
|
||||
## 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.
|
||||
import re, os, string, textwrap, time, traceback, sys
|
||||
from operator import itemgetter, attrgetter
|
||||
from socket import gethostname
|
||||
from urlparse import urlparse, urlunparse
|
||||
from urllib import quote, unquote
|
||||
from math import sin, cos, pi
|
||||
|
||||
from libprs500 import TEMPORARY_FILENAME_TEMPLATE as TFT
|
||||
from libprs500.lrf.meta import LRFMetaFile
|
||||
from libprs500.gui import Error, Warning
|
||||
|
||||
from PyQt4 import QtGui, QtCore
|
||||
from PyQt4.QtCore import Qt, SIGNAL
|
||||
@ -19,22 +29,9 @@ from PyQt4.Qt import QApplication, QString, QFont, QAbstractListModel, QVariant,
|
||||
QAbstractItemView, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox, QPoint, QTemporaryFile, QDir, QFile, QIODevice,\
|
||||
QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle,\
|
||||
QStringList, QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, QDrag, QRect
|
||||
import re, os, string, textwrap, time, traceback, sys
|
||||
|
||||
from operator import itemgetter, attrgetter
|
||||
from socket import gethostname
|
||||
from urlparse import urlparse, urlunparse
|
||||
from urllib import quote, unquote
|
||||
from math import sin, cos, pi
|
||||
from libprs500 import TEMPORARY_FILENAME_TEMPLATE as TFT
|
||||
from libprs500.lrf.meta import LRFMetaFile
|
||||
from libprs500.gui import Error, Warning
|
||||
|
||||
NONE = QVariant()
|
||||
TIME_WRITE_FMT = "%d %b %Y"
|
||||
COVER_HEIGHT = 80
|
||||
|
||||
|
||||
NONE = QVariant() #: Null value to return from the data function of item models
|
||||
TIME_WRITE_FMT = "%d %b %Y" #: The display format used to show dates
|
||||
|
||||
class FileDragAndDrop(object):
|
||||
_drag_start_position = QPoint()
|
||||
@ -99,7 +96,8 @@ class FileDragAndDrop(object):
|
||||
files = self._get_r_ok_files(event)
|
||||
if files:
|
||||
try:
|
||||
if self.files_dropped(files, event): event.acceptProposedAction()
|
||||
event.setDropAction(Qt.CopyAction)
|
||||
if self.files_dropped(files, event): event.accept()
|
||||
except Exception, e:
|
||||
Error("There was an error processing the dropped files.", e)
|
||||
raise e
|
||||
@ -116,7 +114,7 @@ class FileDragAndDrop(object):
|
||||
urls.append(urlunparse(('file', quote(gethostname()), quote(str(file.name)), '','','')))
|
||||
self._dragged_files.append(file)
|
||||
mime_data.setData("text/uri-list", QByteArray("\n".join(urls)))
|
||||
user = os.getenv['USER']
|
||||
user = os.getenv('USER')
|
||||
if user: mime_data.setData("text/x-xdnd-username", QByteArray(user))
|
||||
drag.setMimeData(mime_data)
|
||||
return drag
|
||||
@ -131,7 +129,7 @@ class FileDragAndDrop(object):
|
||||
return self.drag_object_from_files(files), self._dragged_files
|
||||
|
||||
|
||||
class TableView(FileDragAndDrop, QTableView):
|
||||
class TableView(FileDragAndDrop, QTableView):
|
||||
def __init__(self, parent):
|
||||
FileDragAndDrop.__init__(self, QTableView)
|
||||
QTableView.__init__(self, parent)
|
||||
@ -261,11 +259,11 @@ class LibraryBooksView(TableView):
|
||||
def start_drag(self, pos):
|
||||
index = self.indexAt(pos)
|
||||
if index.isValid():
|
||||
indexes = self.selectedIndexes()
|
||||
files = self.model().extract_formats(indexes)
|
||||
rows = frozenset([ index.row() for index in self.selectedIndexes() ])
|
||||
files = self.model().extract_formats(rows)
|
||||
drag = self.drag_object_from_files(files)
|
||||
if drag:
|
||||
ids = [ str(self.model().id_from_index(index)) for index in indexes ]
|
||||
ids = [ str(self.model().id_from_row(row)) for row in rows ]
|
||||
drag.mimeData().setData("application/x-libprs500-id", QByteArray("\n".join(ids)))
|
||||
drag.start()
|
||||
|
||||
@ -376,13 +374,10 @@ class LibraryBooksModel(QAbstractTableModel):
|
||||
self._data = None
|
||||
self._orig_data = None
|
||||
|
||||
def extract_formats(self, indices):
|
||||
files, rows = [], []
|
||||
for index in indices:
|
||||
row = index.row()
|
||||
if row in rows: continue
|
||||
else: rows.append(row)
|
||||
id = self.id_from_index(index)
|
||||
def extract_formats(self, rows):
|
||||
files = []
|
||||
for row in rows:
|
||||
id = self.id_from_row(row)
|
||||
au = self._data[row]["authors"] if self._data[row]["authors"] else "Unknown"
|
||||
basename = re.sub("\n", "", "_"+str(id)+"_"+self._data[row]["title"]+" by "+ au)
|
||||
exts = self.db.get_extensions(id)
|
||||
@ -489,6 +484,7 @@ class LibraryBooksModel(QAbstractTableModel):
|
||||
return exts, tags, comments, cover
|
||||
|
||||
def id_from_index(self, index): return self._data[index.row()]["id"]
|
||||
def id_from_row(self, row): return self._data[row]["id"]
|
||||
|
||||
def refresh_row(self, row):
|
||||
self._data[row] = self.db.get_row_by_id(self._data[row]["id"], self.FIELDS)
|
||||
@ -498,8 +494,12 @@ class LibraryBooksModel(QAbstractTableModel):
|
||||
break
|
||||
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(row, 0), self.index(row, self.columnCount(0)-1))
|
||||
|
||||
def book_info(self, id, cols=["title", "authors", "cover"]):
|
||||
return self.db.get_row_by_id(id, cols)
|
||||
def book_info(self, id):
|
||||
""" Return title, authors and cover in a dict """
|
||||
cover = self.db.get_cover(id)
|
||||
info = self.db.get_row_by_id(id, ["title", "authors"])
|
||||
info["cover"] = cover
|
||||
return info
|
||||
|
||||
def data(self, index, role):
|
||||
if role == Qt.DisplayRole or role == Qt.EditRole:
|
||||
@ -583,6 +583,13 @@ class LibraryBooksModel(QAbstractTableModel):
|
||||
self._orig_data.append(self.db.get_row_by_id(id, self.FIELDS))
|
||||
|
||||
class DeviceBooksModel(QAbstractTableModel):
|
||||
@apply
|
||||
def booklist():
|
||||
doc = """ The booklist this model is based on """
|
||||
def fget(self):
|
||||
return self._orig_data
|
||||
return property(**locals())
|
||||
|
||||
def __init__(self, parent):
|
||||
QAbstractTableModel.__init__(self, parent)
|
||||
self._data = []
|
||||
@ -662,19 +669,20 @@ class DeviceBooksModel(QAbstractTableModel):
|
||||
self.emit(SIGNAL("layoutChanged()"))
|
||||
self.emit(SIGNAL("searched()"))
|
||||
|
||||
def delete_by_path(self, path):
|
||||
def delete(self, indices):
|
||||
paths = []
|
||||
rows = [ index.row() for index in indices ]
|
||||
if not rows: return
|
||||
self.emit(SIGNAL("layoutAboutToBeChanged()"))
|
||||
index = -1
|
||||
for book in self._data:
|
||||
if path in book["path"]:
|
||||
self._data.remove(book)
|
||||
break
|
||||
for book in self._orig_data:
|
||||
if path in book["path"]:
|
||||
self._orig_data.remove(book)
|
||||
break
|
||||
elems = [ self._data[row] for row in rows ]
|
||||
for e in elems:
|
||||
id = e.id
|
||||
paths.append(e.path)
|
||||
self._orig_data.delete_book(id)
|
||||
try: self._data.remove(e)
|
||||
except ValueError: pass
|
||||
self.emit(SIGNAL("layoutChanged()"))
|
||||
self.emit(SIGNAL("deleted()"))
|
||||
return paths
|
||||
|
||||
def path(self, index): return self._data[index.row()].path
|
||||
def title(self, index): return self._data[index.row()].title
|
||||
@ -709,9 +717,9 @@ class DeviceModel(QAbstractListModel):
|
||||
text = None
|
||||
if row == 0: text = "Library"
|
||||
if row == 1 and self.show_reader:
|
||||
text = "Reader\n" + TableView.human_readable(self.memory_free) + " available"
|
||||
text = "Reader\n" + TableView.human_readable(self.memory_free) + " available"
|
||||
elif row == 2 and self.show_card:
|
||||
text = "Card\n" + TableView.human_readable(self.card_free) + " available"
|
||||
text = "Card\n" + TableView.human_readable(self.card_free) + " available"
|
||||
if text: data = QVariant(text)
|
||||
elif role == Qt.DecorationRole:
|
||||
icon = None
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user