gui2 now has info display, add, remove and sync functionality.

This commit is contained in:
Kovid Goyal 2007-07-20 04:29:41 +00:00
parent 24591b5c25
commit 0daf6d3588
35 changed files with 20309 additions and 33006 deletions

View File

@ -32,6 +32,7 @@ class Device(object):
FORMATS = ["lrf", "rtf", "pdf", "txt"]
VENDOR_ID = 0x0000
PRODUCT_ID = 0x0000
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
def __init__(self, key='-1', log_packets=False, report_progress=None) :
"""
@ -106,31 +107,48 @@ class Device(object):
"""
raise NotImplementedError()
def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True):
"""
Add a book to the device. If oncard is True then the book is copied
to the card rather than main memory.
@param infile: The source file, should be opened in "rb" mode
@param name: The name of the book file when uploaded to the
device. The extension of name must be one of
the supported formats for this device.
@param info: A dictionary that must have the keys "title", "authors", "cover".
C{info["cover"]} should be a three element tuple (width, height, data)
where data is the image data in JPEG format as a string
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
"""
def upload_books(self, files, names, on_card=False, end_session=True):
'''
Upload a list of books to the device. If a file already
exists on the device, it should be replaced.
@param files: A list of paths and/or file-like objects.
@param names: A list of file names that the books should have
once uploaded to the device. len(names) == len(files)
@return: A list of 3-element tuples. The list is meant to be passed
to L{add_books_to_metadata}.
'''
raise NotImplementedError()
def remove_book(self, paths, booklists, end_session=True):
"""
Remove the books specified by C{paths} from the device. The metadata
cache on the device must also be updated.
@classmethod
def add_books_to_metadata(cls, locations, metadata, booklists):
'''
Add locations to the booklists. This function must not communicate with
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.
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
"""
'''
raise NotImplementedError
def delete_books(self, paths, end_session=True):
'''
Delete books at paths on device.
'''
raise NotImplementedError()
@classmethod
def remove_books_from_metadata(cls, paths, booklists):
'''
Remove books from the metadata list. This function must not communicate
with the device.
@param paths: paths to books on the device.
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
'''
raise NotImplementedError()
def sync_booklists(self, booklists, end_session=True):

View File

@ -193,6 +193,17 @@ class BookList(list):
node.elem.parentNode.removeChild(node.elem)
node.elem.unlink()
def remove_book(self, path):
node = None
for book in self:
if book.path == path:
node = book
self.remove(book)
break
if node:
node.elem.parentNode.removeChild(node.elem)
node.elem.unlink()
def add_book(self, info, name, size, ctime):
""" Add a node into DOM tree representing a book """
node = self.document.createElement(self.prefix + "text")
@ -214,6 +225,7 @@ class BookList(list):
w, h, data = info["cover"]
except TypeError:
w, h, data = None, None, None
if data:
th = self.document.createElement(self.prefix + "thumbnail")
th.setAttribute("width", str(w))

View File

@ -805,7 +805,7 @@ class PRS500(Device):
return BookList(prefix=prefix, root=root, sfile=tfile)
@safe
def remove_book(self, paths, booklists, end_session=True):
def remove_books(self, paths, booklists, end_session=True):
"""
Remove the books specified by paths from the device. The metadata
cache on the device should also be updated.
@ -826,6 +826,58 @@ class PRS500(Device):
if len(booklists[1]):
self.upload_book_list(booklists[1], end_session=False)
@safe
def upload_books(self, files, names, on_card=False, end_session=True):
card = self.card(end_session=False)
prefix = card + '/' if on_card else '/Data/media/books/'
paths, ctimes, sizes = [], [], []
names = iter(names)
for file in files:
infile = file if hasattr(file, 'read') else open(file, 'rb')
infile.seek(0, 2)
size = infile.tell()
sizes.append(size)
infile.seek(0)
space = self.free_space(end_session=False)
mspace = space[0]
cspace = space[1] if space[1] >= space[2] else space[2]
if on_card and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\
"on the storage card")
if not on_card and size > mspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space " +\
"in main memory")
name = names.next()
paths.append(prefix+name)
self.put_file(infile, paths[-1], replace_file=True, end_session=False)
ctimes.append(self.path_properties(paths[-1], end_session=False).ctime)
return zip(paths, sizes, ctimes)
@classmethod
def add_books_to_metadata(cls, locations, metadata, booklists):
metadata = iter(metadata)
for location in locations:
info = metadata.next()
path = location[0]
on_card = 1 if path[1] == ':' else 0
name = path.rpartition('/')[2]
if not on_card:
name = 'books/' + name
booklists[on_card].add_book(info, name, *location[1:])
fix_ids(*booklists)
@safe
def delete_books(self, paths, end_session=True):
for path in paths:
self.del_file(path, end_session=False)
@classmethod
def remove_books_from_metadata(cls, paths, booklists):
for path in paths:
on_card = 1 if path[1] == ':' else 0
booklists[on_card].remove_book(path)
fix_ids(*booklists)
@safe
def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True):

View File

@ -1,13 +1,15 @@
all : main_ui.py images_rc.py
all : main_ui.py images_rc.pyc
main_ui.py : main.ui
pyuic4 main.ui > main_ui.py
images_rc.py : images.qrc images
images_rc.pyc : images.qrc images
pyrcc4 images.qrc > images_rc.py
python -c "import compiler; compiler.compileFile('images_rc.py')"
rm -f images_rc.py
clean :
rm main_ui.py images_rc.py
rm main_ui.py images_rc.pyc
test : all
python main.py

View File

@ -14,7 +14,8 @@
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
""" The GUI for libprs500. """
import sys, os, re, StringIO, traceback
from PyQt4.QtCore import QVariant
from PyQt4.QtCore import QVariant, QSettings
from PyQt4.QtGui import QFileDialog
from libprs500 import __appname__ as APP_TITLE
from libprs500 import __author__
NONE = QVariant() #: Null value to return from the data function of item models
@ -62,3 +63,31 @@ def human_readable(size):
size = size[:size.find(".")+2]
return size + " " + suffix
def choose_files(window, dialog, title, filetype='',
extensions=[], all_files=True):
'''
Ask user to choose a bunch of files.
@param dialog: Unique gialog name used to store the opened directory
@param title: Title to show in dialogs titlebar
@param filetype: What types of files is this dialog choosing
@params extensions: list of allowable extension
@params all_files: If True show all files
'''
settings = QSettings()
_dir = settings.value(dialog, QVariant(os.path.expanduser("~"))).toString()
books = []
extensions = ['*.'+i for i in extensions]
if extensions:
filter = filetype + ' (' + ' '.join(extensions) + ')'
if all_files:
filter += ';;All files (*)'
else:
filter = 'All files (*)'
files = QFileDialog.getOpenFileNames(window, title, _dir, filter)
for file in files:
file = unicode(file.toUtf8(), 'utf8')
books.append(os.path.abspath(file))
if books:
settings.setValue(dialog, QVariant(os.path.dirname(books[0])))
return books

View File

@ -110,3 +110,21 @@ class DeviceManager(QObject):
self.device.set_progress_reporter(updater)
self.device.sync_booklists(booklists)
return sync_booklists
def upload_books_func(self):
'''Upload books to device'''
def upload_books(updater, files, names, on_card=False):
return self.device.upload_books(files, names, on_card, end_session=True)
return upload_books
def add_books_to_metadata(self, locations, metadata, booklists):
self.device_class.add_books_to_metadata(locations, metadata, booklists)
def delete_books_func(self):
'''Remove books from device'''
def delete_books(updater, paths):
self.device.delete_books(paths, end_session=True)
return delete_books
def remove_books_from_metadata(self, paths, booklists):
self.device_class.remove_books_from_metadata(paths, booklists)

View File

@ -1,18 +1,15 @@
<RCC>
<qresource prefix="/" >
<file>images/addfile.png</file>
<file alias="default_cover" >images/cherubs.jpg</file>
<file>images/clear.png</file>
<file>images/delfile.png</file>
<file>images/edit.png</file>
<file>images/fileopen.png</file>
<file>images/book.svg</file>
<file>images/clear_left.svg</file>
<file>images/edit_input.svg</file>
<file>images/jobs-animated.mng</file>
<file>images/jobs.svg</file>
<file alias="library" >images/library.png</file>
<file alias="card" >images/memory_stick_unmount.png</file>
<file>images/minus.png</file>
<file>images/plus.png</file>
<file alias="reader" >images/reader.png</file>
<file>images/upload.png</file>
<file>images/plus.svg</file>
<file>images/reader.svg</file>
<file>images/sd.svg</file>
<file>images/sync.svg</file>
<file>images/trash.svg</file>
</qresource>
</RCC>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -0,0 +1,352 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.45.1"
version="1.0"
sodipodi:docbase="/home/david"
sodipodi:docname="clear-left.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<defs
id="defs4">
<linearGradient
y2="-131.93112"
x2="-45.096584"
y1="-131.93112"
x1="-88.058083"
gradientTransform="matrix(0,1.022977,-1.022977,0,111.9686,137.8125)"
gradientUnits="userSpaceOnUse"
id="linearGradient1980"
xlink:href="#linearGradient3711"
inkscape:collect="always" />
<radialGradient
r="36"
fy="92"
fx="343.99899"
cy="92"
cx="343.99899"
gradientUnits="userSpaceOnUse"
id="radialGradient1978"
xlink:href="#linearGradient3711"
inkscape:collect="always" />
<linearGradient
y2="-383.9971"
x2="-11.91648"
y1="-383.9971"
x1="-70.002899"
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
gradientUnits="userSpaceOnUse"
id="linearGradient1976"
xlink:href="#linearGradient26907"
inkscape:collect="always" />
<radialGradient
r="63.912209"
fy="116.88514"
fx="63.975182"
cy="115.70919"
cx="63.912209"
gradientTransform="matrix(1,0,0,0.197802,0,92.82166)"
gradientUnits="userSpaceOnUse"
id="radialGradient3743"
xlink:href="#linearGradient3291"
inkscape:collect="always" />
<linearGradient
inkscape:collect="always"
id="linearGradient3291">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3293" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3295" />
</linearGradient>
<linearGradient
id="linearGradient3711"
gradientUnits="userSpaceOnUse"
x1="-84.002403"
y1="-383.9971"
x2="-23.516129"
y2="-383.9975"
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)">
<stop
offset="0"
style="stop-color:white;stop-opacity:1;"
id="stop3713" />
<stop
offset="1"
style="stop-color:white;stop-opacity:0;"
id="stop3715" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
y2="-383.9971"
x2="-12.0029"
y1="-383.9971"
x1="-84.002403"
gradientUnits="userSpaceOnUse"
id="linearGradient26907">
<stop
id="stop26909"
style="stop-color:#888a85;stop-opacity:1;"
offset="0" />
<stop
id="stop26911"
style="stop-color:#2e3436;stop-opacity:1;"
offset="1" />
</linearGradient>
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.197802,0,92.82166)"
r="63.912209"
fy="115.7093"
fx="63.912209"
cy="115.70919"
cx="63.912209"
id="radialGradient3297"
xlink:href="#linearGradient3291"
inkscape:collect="always" />
<linearGradient
id="linearGradient2871"
inkscape:collect="always">
<stop
id="stop2873"
offset="0"
style="stop-color:white;stop-opacity:1;" />
<stop
id="stop2875"
offset="1"
style="stop-color:white;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient2898"
inkscape:collect="always">
<stop
id="stop2900"
offset="0"
style="stop-color:#2c72c7;stop-opacity:1;" />
<stop
id="stop2902"
offset="1"
style="stop-color:#2c72c7;stop-opacity:0;" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2898"
id="radialGradient2505"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.113341,0,0,0.899421,144.15267,184.75423)"
cx="63.249104"
cy="29.640472"
fx="63.249104"
fy="29.640472"
r="58.621283" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2898"
id="radialGradient2508"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.113341,0,0,0.899421,144.15267,184.75423)"
cx="63.249104"
cy="29.640472"
fx="63.249104"
fy="29.640472"
r="58.621283" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2898"
id="radialGradient2511"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.113341,0,0,0.899421,144.15267,184.75423)"
cx="63.249104"
cy="29.640472"
fx="63.249104"
fy="29.640472"
r="58.621283" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2898"
id="radialGradient2514"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.113341,0,0,0.899421,144.15267,184.75423)"
cx="63.249104"
cy="29.640472"
fx="63.249104"
fy="29.640472"
r="58.621283" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2871"
id="linearGradient2518"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.908626,0,0,0.908626,131.38537,118.41482)"
x1="50.389755"
y1="6.6258311"
x2="50.389755"
y2="56.132732" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2871"
id="linearGradient2521"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.908626,0,0,0.908626,131.38537,118.41482)"
x1="50.78854"
y1="-22.328487"
x2="52.001846"
y2="98.047089" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3711"
id="linearGradient2524"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0,1.022977,-1.022977,0,118.90977,143.61542)"
x1="-88.058083"
y1="-131.93112"
x2="-45.096584"
y2="-131.93112" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient26907"
id="linearGradient3498"
x1="105.21252"
y1="92.100929"
x2="84.151566"
y2="28.206367"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3711"
id="linearGradient3502"
gradientUnits="userSpaceOnUse"
x1="58.714989"
y1="23.999331"
x2="58.714989"
y2="69.301285"
gradientTransform="matrix(0.8571429,0,0,0.8571429,9.1428571,9.150686)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3711"
id="linearGradient3505"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0,1.591298,-1.591298,0,-12.882452,195.15999)"
x1="-88.058083"
y1="-131.93112"
x2="-45.096584"
y2="-131.93112" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3711"
id="linearGradient3512"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.8571429,0,0,0.8571429,12.305954,9.150686)"
x1="58.714989"
y1="12.817292"
x2="58.714989"
y2="74.892502" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3711"
id="radialGradient3518"
cx="76.780853"
cy="141.12396"
fx="76.780853"
fy="141.12396"
r="49.162853"
gradientTransform="matrix(1.0602856,0,0,0.1805211,-4.233122,72.750927)"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="64"
inkscape:cy="64"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="128px"
height="128px"
showgrid="false"
gridempspacing="2"
gridspacingx="4px"
gridspacingy="4px"
showborder="false"
inkscape:grid-bbox="false"
inkscape:window-width="746"
inkscape:window-height="712"
inkscape:window-x="18"
inkscape:window-y="32" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:nodetypes="ccc"
id="path2276"
d="M 26.78122,97.797937 L 26.78122,95.003228 L 26.78122,97.797937 z "
style="fill:#ffffff;fill-opacity:0.75688076;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccc"
id="path2481"
d="M 57.833968,116.6694 L 57.833968,113.87469 L 57.833968,116.6694 z "
style="fill:#ffffff;fill-opacity:0.75688076;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<path
style="fill:url(#linearGradient3498);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 117.98502,24 C 119.09959,24 120,24.951655 120,26.129676 L 120,101.97327 C 120,103.15129 119.09959,104.10293 117.98502,104.10293 L 49.810672,104.10293 C 49.753021,104.10293 49.704568,104.10791 49.648173,104.10293 C 49.081965,104.15046 48.49064,103.94409 48.055692,103.48465 L 8.5912566,65.562848 C 7.8029145,64.730097 7.8029145,63.372848 8.5912566,62.540097 L 48.055692,24.652645 C 48.474329,24.210424 49.037322,24.00691 49.583174,24.03435 C 49.661578,24.024618 49.729716,24 49.810672,24 L 117.98502,24 z "
id="rect1969"
sodipodi:nodetypes="cccccsccccscc" />
<path
style="opacity:0.8;fill:url(#linearGradient3512);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 52.687242,29.71875 C 52.617851,29.71875 52.566947,29.74166 52.499742,29.75 C 52.031868,29.72648 51.546074,29.902203 51.187242,30.28125 L 17.343492,62.75 C 16.914019,63.203667 16.758289,63.85147 16.874742,64.4375 C 29.612903,69.972662 48.740097,73.343748 67.163097,73.34375 C 85.598588,73.34375 102.42183,69.979357 115.1631,64.4375 L 115.1631,31.5625 C 115.1631,30.552768 114.3997,29.71875 113.44435,29.71875 L 52.687242,29.71875 z "
id="path3500"
sodipodi:nodetypes="cscccscccc" />
<path
sodipodi:nodetypes="cscccscccc"
id="path3514"
d="M 52.687242,98.226781 C 52.617851,98.226781 52.566947,98.203871 52.499742,98.195531 C 52.031868,98.219051 51.546074,98.043328 51.187242,97.664281 L 17.343492,65.195531 C 16.914019,64.741864 16.758289,64.094061 16.874742,63.508031 C 29.612903,57.972869 48.740097,54.601783 67.163097,54.601781 C 85.598588,54.601781 102.42183,57.966174 115.1631,63.508031 L 115.1631,96.383031 C 115.1631,97.392763 114.3997,98.226781 113.44435,98.226781 L 52.687242,98.226781 z "
style="opacity:0.3;fill:url(#radialGradient3518);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<g
id="g3527"
transform="matrix(0.5450885,-0.5450884,0.5450884,0.5450885,-91.905703,107.44253)">
<path
style="opacity:0.8;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#323232;stroke-width:5.18893194;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1"
d="M 189.16642,77.673811 L 189.16642,110.22823 L 157.16642,109.67381 L 157.16642,125.67381 L 189.16642,125.67381 L 189.16642,157.67381 L 205.16642,157.67381 L 205.16642,125.67381 L 237.16642,125.67381 L 237.16642,110.22823 L 205.16642,110.22823 L 205.16642,77.673811 L 189.16642,77.673811 z "
id="path1990"
sodipodi:nodetypes="ccccccccccccc" />
<path
sodipodi:nodetypes="ccccccccccccc"
id="rect3232"
d="M 189.16642,77.673811 L 189.16642,110.22823 L 157.16642,109.67381 L 157.16642,125.67381 L 189.16642,125.67381 L 189.16642,157.67381 L 205.16642,157.67381 L 205.16642,125.67381 L 237.16642,125.67381 L 237.16642,110.22823 L 205.16642,110.22823 L 205.16642,77.673811 L 189.16642,77.673811 z "
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,700 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg1307"
sodipodi:version="0.32"
inkscape:version="0.43"
version="1.0"
sodipodi:docbase="/home/pinheiro/Documents/pics/new oxygen/svg"
sodipodi:docname="plus.svg">
<defs
id="defs1309">
<linearGradient
id="linearGradient2403">
<stop
style="stop-color:#28691f;stop-opacity:1;"
offset="0"
id="stop2405" />
<stop
style="stop-color:#42ad33;stop-opacity:1;"
offset="1"
id="stop2407" />
</linearGradient>
<linearGradient
id="linearGradient2389">
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="0"
id="stop2391" />
<stop
id="stop2393"
offset="0.4375"
style="stop-color:#000000;stop-opacity:0;" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="0.56588125"
id="stop2395" />
<stop
id="stop2423"
offset="0.76237977"
style="stop-color:#000000;stop-opacity:0.24705882;" />
<stop
id="stop2421"
offset="0.77884614"
style="stop-color:#000000;stop-opacity:0.49803922;" />
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0.875"
id="stop2397" />
<stop
id="stop2411"
offset="0.875"
style="stop-color:#000000;stop-opacity:0.49803922;" />
<stop
id="stop2399"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient2362">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop2364" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop2366" />
</linearGradient>
<linearGradient
id="linearGradient2321">
<stop
style="stop-color:#c3c3c3;stop-opacity:1;"
offset="0"
id="stop2323" />
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="1"
id="stop2325" />
</linearGradient>
<linearGradient
id="linearGradient2287">
<stop
id="stop2299"
offset="0"
style="stop-color:#000000;stop-opacity:0;" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="0.4375"
id="stop2307" />
<stop
id="stop2309"
offset="0.58240438"
style="stop-color:#000000;stop-opacity:0;" />
<stop
style="stop-color:#000000;stop-opacity:0.49803922;"
offset="0.76442307"
id="stop2419" />
<stop
id="stop2303"
offset="0.875"
style="stop-color:#000000;stop-opacity:1;" />
<stop
style="stop-color:#000000;stop-opacity:0.49803922;"
offset="0.91826922"
id="stop2413" />
<stop
id="stop2417"
offset="0.96048182"
style="stop-color:#000000;stop-opacity:0;" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop2291" />
</linearGradient>
<linearGradient
id="linearGradient3325">
<stop
id="stop3327"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop3329"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient3311">
<stop
style="stop-color:#2d2d2d;stop-opacity:1;"
offset="0"
id="stop3313" />
<stop
id="stop3319"
offset="0.5"
style="stop-color:#000000;stop-opacity:1;" />
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="1"
id="stop3315" />
</linearGradient>
<linearGradient
id="linearGradient3303">
<stop
style="stop-color:#ffffff;stop-opacity:0.68345326;"
offset="0"
id="stop3305" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop3307" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3291">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3293" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3295" />
</linearGradient>
<linearGradient
id="linearGradient3273">
<stop
style="stop-color:#ffffff;stop-opacity:0.55035973;"
offset="0"
id="stop3275" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop3277" />
</linearGradient>
<linearGradient
id="linearGradient3259">
<stop
id="stop3261"
offset="0"
style="stop-color:#ffffff;stop-opacity:0.55035973;" />
<stop
id="stop3263"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient3251">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3253" />
<stop
style="stop-color:#131313;stop-opacity:0;"
offset="1"
id="stop3255" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3235">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3237" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop3239" />
</linearGradient>
<linearGradient
id="linearGradient3225">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3227" />
<stop
style="stop-color:#aeaeae;stop-opacity:1;"
offset="1"
id="stop3229" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3217">
<stop
style="stop-color:#252525;stop-opacity:1;"
offset="0"
id="stop3219" />
<stop
style="stop-color:#252525;stop-opacity:0;"
offset="1"
id="stop3221" />
</linearGradient>
<linearGradient
id="linearGradient3207">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3209" />
<stop
style="stop-color:#252525;stop-opacity:0;"
offset="1"
id="stop3211" />
</linearGradient>
<linearGradient
id="linearGradient2257">
<stop
style="stop-color:#b4942a;stop-opacity:1;"
offset="0"
id="stop2259" />
<stop
style="stop-color:#e4dcc9;stop-opacity:1"
offset="1"
id="stop2261" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3291"
id="radialGradient3297"
cx="63.912209"
cy="115.70919"
fx="63.912209"
fy="115.7093"
r="63.912209"
gradientTransform="matrix(1,0,0,0.197802,0,92.82166)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2257"
id="radialGradient1405"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.519831,9.412826e-2,-0.895354,13.78472,115.1882,-1545.166)"
cx="42.617531"
cy="120.64188"
fx="42.617531"
fy="120.64188"
r="3.406888" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3311"
id="radialGradient1407"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(6.22884e-2,-1.47547e-4,1.889714e-3,0.798624,69.12243,5.487066)"
cx="95.505852"
cy="59.591507"
fx="95.505852"
fy="59.591507"
r="47.746404" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3225"
id="radialGradient1409"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.297066,3.012623e-3,-1.134728e-3,0.488669,7.096503,-13.69501)"
cx="49.009884"
cy="8.4953122"
fx="47.370888"
fy="6.7701697"
r="3.9750405" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3217"
id="linearGradient1411"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.29707,-3.693584e-16,3.693584e-16,1.29707,7.064707,-20.57911)"
x1="48.914677"
y1="2.9719031"
x2="48.913002"
y2="2.5548496" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3207"
id="radialGradient1413"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.29707,-4.501275e-16,6.640356e-17,0.1578,7.064707,-17.56653)"
cx="49.011971"
cy="2.6743078"
fx="49.011971"
fy="2.6743078"
r="1.7246193" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3235"
id="linearGradient1415"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.297066,3.012623e-3,-3.012623e-3,1.297066,7.112448,-20.56258)"
x1="48.498562"
y1="0.81150496"
x2="48.732723"
y2="2.3657269" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3251"
id="linearGradient1417"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.28993,-5.022494e-16,5.050298e-16,1.29707,7.402337,-20.57911)"
x1="46.051746"
y1="3.0999987"
x2="46.051746"
y2="2.395859" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3273"
id="radialGradient1419"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.860164,-2.800126e-16,6.473209e-17,0.1578,24.75801,-17.56653)"
cx="49.011971"
cy="2.6743078"
fx="49.011971"
fy="2.6743078"
r="1.7246193" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3251"
id="linearGradient1421"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.279856,4.983275e-16,-5.050298e-16,1.29707,-133.3868,-20.57911)"
x1="46.051746"
y1="3.0999987"
x2="46.051746"
y2="2.395859" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3259"
id="radialGradient1423"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.853446,3.872019e-16,-5.817635e-17,0.1578,-116.1668,-17.56653)"
cx="49.011971"
cy="2.6743078"
fx="49.011971"
fy="2.6743078"
r="1.7246193" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3303"
id="radialGradient1425"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,7.573576e-17,-1.374554e-18,2.608014e-2,-7.697455e-14,7.26766)"
cx="34.677639"
cy="7.4622769"
fx="34.677639"
fy="7.4622769"
r="47.595197" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3325"
id="radialGradient1427"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.511766,-6.865741e-3,4.187271e-5,-9.110636e-3,87.10184,7.76835)"
cx="34.677639"
cy="7.4622769"
fx="34.677639"
fy="7.4622769"
r="47.595196" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3259"
id="radialGradient1433"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.853446,3.879821e-16,-5.832064e-17,0.1578,-115.9141,-7.300115)"
cx="49.011971"
cy="2.6743078"
fx="49.011971"
fy="2.6743078"
r="1.7246193" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3251"
id="linearGradient1436"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.279856,4.994967e-16,-5.062158e-16,1.29707,-133.1341,-10.31269)"
x1="46.051746"
y1="3.0999987"
x2="46.051746"
y2="2.395859" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3273"
id="radialGradient1439"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.860164,-2.80798e-16,6.487638e-17,0.1578,24.50481,-7.300115)"
cx="49.011971"
cy="2.6743078"
fx="49.011971"
fy="2.6743078"
r="1.7246193" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3251"
id="linearGradient1442"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.28993,-5.034291e-16,5.062158e-16,1.29707,7.14915,-10.31269)"
x1="46.051746"
y1="3.0999987"
x2="46.051746"
y2="2.395859" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3235"
id="linearGradient1445"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.297068,-1.880044e-3,1.880044e-3,1.297068,6.796523,-10.3225)"
x1="48.498562"
y1="0.81150496"
x2="48.732723"
y2="2.3657269" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3207"
id="radialGradient1448"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.29707,-4.513135e-16,6.654785e-17,0.1578,6.81152,-7.300115)"
cx="49.011971"
cy="2.6743078"
fx="49.011971"
fy="2.6743078"
r="1.7246193" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3217"
id="linearGradient1451"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.29707,-3.705444e-16,3.705444e-16,1.29707,6.81152,-10.31269)"
x1="48.914677"
y1="2.9719031"
x2="48.913002"
y2="2.5548496" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3225"
id="radialGradient1455"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.297068,-1.880044e-3,7.085819e-4,0.48867,6.806484,-3.45491)"
cx="49.009884"
cy="8.4953122"
fx="47.370888"
fy="6.7701697"
r="3.9750405" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3311"
id="radialGradient1462"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(6.228741e-2,-3.825032e-4,4.90218e-3,0.798611,68.90433,5.49306)"
cx="95.505852"
cy="59.591507"
fx="95.505852"
fy="59.591507"
r="47.746404" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2257"
id="radialGradient1466"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.520175,8.839467e-2,-0.843351,13.788,109.1206,-1545.323)"
cx="42.617531"
cy="120.64188"
fx="42.617531"
fy="120.64188"
r="3.406888" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3325"
id="radialGradient1470"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.511766,-6.865741e-3,4.187271e-5,-9.110636e-3,87.10184,7.76835)"
cx="34.677639"
cy="7.4622769"
fx="34.677639"
fy="7.4622769"
r="47.595196" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2287"
id="radialGradient2297"
cx="95.796135"
cy="56.931728"
fx="95.990845"
fy="39.602753"
r="47.11924"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2321"
id="linearGradient2327"
x1="-42.789177"
y1="82.913582"
x2="229.1772"
y2="81.155327"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2389"
id="radialGradient2340"
cx="59.385818"
cy="52.046673"
fx="59.385818"
fy="52.046673"
r="43.225086"
gradientTransform="matrix(1.165294,-9.905815e-18,-1.210432e-17,1.180294,-9.816118,-9.597466)"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2362"
id="linearGradient2368"
x1="74.332748"
y1="17.912012"
x2="54.983063"
y2="90.126022"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.180422,0,0,1.180422,-10.39088,-10.58642)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2362"
id="linearGradient2401"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.258277,0,0,1.258277,-15.29483,-12.98214)"
x1="74.514832"
y1="17.232468"
x2="52.587749"
y2="99.06546" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2403"
id="linearGradient2409"
x1="97.124756"
y1="99.590462"
x2="33.355057"
y2="22.203432"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.065955,0,0,1.065955,-4.218613,-1.697485)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.5452637"
inkscape:cx="131.61259"
inkscape:cy="22.732118"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:document-units="px"
inkscape:grid-bbox="true"
guidetolerance="0.1px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1106"
inkscape:window-height="958"
inkscape:window-x="468"
inkscape:window-y="25">
<sodipodi:guide
orientation="horizontal"
position="32.487481"
id="guide2204" />
</sodipodi:namedview>
<metadata
id="metadata1312">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
<dc:contributor>
<cc:Agent>
<dc:title>Oxygen team</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/LGPL/2.1/">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Notice" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
<cc:requires
rdf:resource="http://web.resource.org/cc/ShareAlike" />
<cc:requires
rdf:resource="http://web.resource.org/cc/SourceCode" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<path
sodipodi:type="arc"
style="opacity:0.38934422;fill:url(#radialGradient2297);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.71249998;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="path1410"
sodipodi:cx="95.796135"
sodipodi:cy="56.931728"
sodipodi:rx="47.11924"
sodipodi:ry="47.11924"
d="M 142.91537 56.931728 A 47.11924 47.11924 0 1 1 48.676895,56.931728 A 47.11924 47.11924 0 1 1 142.91537 56.931728 z"
transform="matrix(1.354468,0,0,1.354468,-65.79139,-14.906)" />
<path
sodipodi:nodetypes="ccc"
id="path2276"
d="M 50.892799,3.2812959 L 50.892799,0.48658747 L 50.892799,3.2812959 z "
style="fill:#ffffff;fill-opacity:0.75688076;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<path
sodipodi:type="arc"
style="opacity:0.38139535;fill:url(#radialGradient3297);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="path3289"
sodipodi:cx="63.912209"
sodipodi:cy="115.70919"
sodipodi:rx="63.912209"
sodipodi:ry="12.641975"
d="M 127.82442 115.70919 A 63.912209 12.641975 0 1 1 0,115.70919 A 63.912209 12.641975 0 1 1 127.82442 115.70919 z"
transform="matrix(1,0,0,0.416667,0,74.87151)" />
<path
sodipodi:type="arc"
style="opacity:1;fill:url(#linearGradient2327);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.71249998;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="path2311"
sodipodi:cx="59.385818"
sodipodi:cy="53.232281"
sodipodi:rx="43.225086"
sodipodi:ry="43.809208"
d="M 102.6109 53.232281 A 43.225086 43.809208 0 1 1 16.160732,53.232281 A 43.225086 43.809208 0 1 1 102.6109 53.232281 z"
transform="matrix(1.303016,0,0,1.325387,-13.29675,-10.91954)" />
<path
transform="matrix(1.303016,0,0,1.325387,-13.29675,-10.91954)"
d="M 102.6109 53.232281 A 43.225086 43.809208 0 1 1 16.160732,53.232281 A 43.225086 43.809208 0 1 1 102.6109 53.232281 z"
sodipodi:ry="43.809208"
sodipodi:rx="43.225086"
sodipodi:cy="53.232281"
sodipodi:cx="59.385818"
id="path2338"
style="opacity:0.38114754;fill:url(#radialGradient2340);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.71249998;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
sodipodi:type="arc" />
<path
style="opacity:1;fill:url(#linearGradient2409);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.71249998;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 56.583051,25.393632 C 55.968826,25.454025 55.498698,25.964941 55.498698,26.579648 L 55.498698,49.555934 L 32.522409,49.555934 C 31.866718,49.555934 31.336395,50.102652 31.336395,50.775836 L 31.336395,68.004033 C 31.336395,68.677227 31.86673,69.207007 32.522409,69.207007 L 55.498698,69.207007 L 55.498698,92.200234 C 55.498698,92.855924 56.045416,93.386248 56.71861,93.386248 L 73.946797,93.386248 C 74.61999,93.386248 75.14977,92.855913 75.14977,92.200234 L 75.14977,69.207007 L 98.126047,69.207007 C 98.781717,69.207007 99.312062,68.677238 99.312062,68.004033 L 99.312062,50.775836 C 99.312062,50.102652 98.781717,49.555934 98.126047,49.555934 L 75.14977,49.555934 L 75.14977,26.579648 C 75.14977,25.923961 74.61999,25.393632 73.946797,25.393632 L 56.71861,25.393632 C 56.676526,25.393632 56.624005,25.389606 56.583051,25.393632 z "
id="rect2372"
sodipodi:nodetypes="cccccccccccccccccccccc" />
<path
style="opacity:0.84426228;fill:url(#linearGradient2401);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.71249998;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:4;stroke-opacity:1"
d="M 62.718389,2.7856523 C 34.26943,3.4970625 11.404252,26.4659 11.404251,51.425951 C 11.404251,55.288815 11.945809,59.047238 12.977098,62.632486 C 47.301752,55.986018 81.431322,56.068486 115.44808,61.688778 C 116.31046,58.392831 116.7457,54.948687 116.7457,51.425951 C 116.7457,26.06971 93.170775,2.7856523 64.09463,2.7856523 C 63.640317,2.7856523 63.16996,2.7743606 62.718389,2.7856523 z "
id="path2342" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 473 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because it is too large Load Diff

View File

@ -60,6 +60,7 @@ class JobManager(QAbstractTableModel):
QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
slot)
job.start()
return job.id
def job_done(self, id, *args, **kwargs):
'''

View File

@ -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.ptempfile import PersistentTemporaryFile
import os, textwrap, traceback, time, re
from datetime import timedelta, datetime
from operator import attrgetter
@ -19,9 +20,9 @@ from math import cos, sin, pi
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QLineEdit, QApplication, \
QPalette
QPalette, QItemSelectionModel
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
QCoreApplication, SIGNAL, QObject, QSize
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
from libprs500.library.database import LibraryDatabase
from libprs500.gui2 import NONE
@ -47,8 +48,8 @@ class LibraryDelegate(QItemDelegate):
self.factor = self.SIZE/100.
def sizeHint(self, option, index):
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
return QSize(num*(self.SIZE), self.SIZE+4)
#num = index.model().data(index, Qt.DisplayRole).toInt()[0]
return QSize(5*(self.SIZE), self.SIZE+4)
def paint(self, painter, option, index):
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
@ -84,13 +85,37 @@ class BooksModel(QAbstractTableModel):
QAbstractTableModel.__init__(self, parent)
self.db = None
self.cols = ['title', 'authors', 'size', 'date', 'rating', 'publisher']
self.sorted_on = None
self.sorted_on = (3, Qt.AscendingOrder)
self.last_search = '' # The last search performed on this model
def set_database(self, db):
if isinstance(db, (QString, basestring)):
db = LibraryDatabase(os.path.expanduser(str(db)))
self.db = db
def add_books(self, paths, formats, metadata, uris=[]):
self.db.add_books(paths, formats, metadata, uris)
def row_indices(self, index):
''' Return list indices of all cells in index.row()'''
return [ self.index(index.row(), c) for c in range(self.columnCount(None))]
def removeRows(self, row, count, parent=QModelIndex()):
rows = [row + i for i in range(count)]
self.beginRemoveRows(parent, row, row+count-1)
self.db.delete_books(rows)
self.endRemoveRows()
def removeRow(self, row, parent=QModelIndex()):
self.removeRows(row, 1)
def delete_books(self, indices):
rows = [ i.row() for i in indices ]
for row in rows:
self.removeRow(row)
self.emit(SIGNAL('layoutChanged()'))
self.emit(SIGNAL('deleted()'))
def search_tokens(self, text):
tokens = []
quot = re.search('"(.*?)"', text)
@ -104,8 +129,8 @@ class BooksModel(QAbstractTableModel):
def search(self, text, refinement):
tokens = self.search_tokens(text)
self.db.filter(tokens, refinement)
self.last_search = text
self.reset()
self.emit(SIGNAL('searched()'))
def sort(self, col, order):
if not self.db:
@ -113,9 +138,14 @@ class BooksModel(QAbstractTableModel):
ascending = order == Qt.AscendingOrder
self.db.refresh(self.cols[col], ascending)
self.reset()
self.emit(SIGNAL('sorted()'))
self.sorted_on = (col, order)
def resort(self):
self.sort(*self.sorted_on)
def research(self):
self.search(self.last_search, False)
def database_needs_migration(self):
path = os.path.expanduser('~/library.db')
return self.db.is_empty() and \
@ -128,6 +158,72 @@ class BooksModel(QAbstractTableModel):
def rowCount(self, parent):
return self.db.rows() if self.db else 0
def current_changed(self, current, previous):
data = {}
idx = current.row()
cdata = self.db.cover(idx)
if cdata:
data['cover'] = cdata
tags = self.db.tags(idx)
if tags:
tags = tags.replace(',', ', ')
else:
tags = 'None'
data['Tags'] = tags
formats = self.db.formats(idx)
if formats:
formats = formats.replace(',', ', ')
else:
formats = 'None'
data['Formats'] = formats
comments = self.db.comments(idx)
if not comments:
comments = 'None'
data['Comments'] = comments
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
def get_metadata(self, rows):
metadata = []
for row in rows:
row = row.row()
au = self.db.authors(row)
if not au:
au = 'Unknown'
au = au.split(',')
if len(au) > 1:
t = ', '.join(au[:-1])
t += ' & ' + au[-1]
au = t
else:
au = ' & '.join(au)
mi = {
'title' : self.db.title(row),
'authors' : au,
'cover' : self.db.cover(row),
}
metadata.append(mi)
return metadata
def get_preferred_formats(self, rows, formats):
ans = []
for row in (row.row() for row in rows):
format = None
for f in self.db.formats(row).split(','):
if f.lower() in formats:
format = f
break
if format:
pt = PersistentTemporaryFile(suffix='.'+format)
pt.write(self.db.format(row, format))
pt.seek(0)
ans.append(pt)
else:
ans.append(None)
return ans
def id(self, row):
return self.db.id(row.row())
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column()
@ -231,13 +327,30 @@ class BooksView(QTableView):
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
self.setItemDelegateForColumn(4, LibraryDelegate(self))
QObject.connect(self._model, SIGNAL('sorted()'), self.resizeRowsToContents)
QObject.connect(self._model, SIGNAL('searched()'), self.resizeRowsToContents)
QObject.connect(self._model, SIGNAL('modelReset()'), self.resizeRowsToContents)
QObject.connect(self._model, SIGNAL('deleted()'), self.deleted)
QObject.connect(self._model, SIGNAL('update_current()'), self.update_current)
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self._model.current_changed)
#self.verticalHeader().setVisible(False)
def set_database(self, db):
self._model.set_database(db)
def update_current(self):
'''
Clear selection and update durrent index.
'''
cidx = self.currentIndex()
self.selectionModel().clear()
for idx in self.model().row_indices(cidx):
self.selectionModel().select(idx, QItemSelectionModel.Select)
self.selectionModel().setCurrentIndex(cidx, QItemSelectionModel.NoUpdate)
def deleted(self):
self.resizeRowsToContents()
self.update_current()
def migrate_database(self):
if self._model.database_needs_migration():
print 'Migrating database from pre 0.4.0 version'
@ -256,13 +369,20 @@ class BooksView(QTableView):
LibraryDatabase.import_old_database(path, self._model.db.conn, meter)
def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self._model.search)
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
self._model.search)
def connect_to_book_display(self, bd):
QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
bd)
class DeviceBooksView(BooksView):
def __init__(self, parent):
BooksView.__init__(self, parent, DeviceBooksModel)
self.columns_resized = False
self.resize_on_select = False
def resizeColumnsToContents(self):
QTableView.resizeColumnsToContents(self)
@ -274,11 +394,47 @@ class DeviceBooksView(BooksView):
class DeviceBooksModel(BooksModel):
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
BooksModel.__init__(self, parent)
self.db = []
self.map = []
self.sorted_map = []
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:
indices = self.row_indices(row)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def deletion_done(self, id, succeeded=True):
if not self.marked_for_deletion.has_key(id):
return
rows = self.marked_for_deletion.pop(id)
for row in rows:
if not succeeded:
indices = self.row_indices(self.index(row, 0))
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def remap(self):
self.set_database(self.db)
self.resort()
self.research()
self.emit(SIGNAL('update_current()'))
def indices_to_be_deleted(self):
ans = []
for v in self.marked_for_deletion.values():
ans.extend(v)
return ans
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)
def search(self, text, refinement):
tokens = self.search_tokens(text)
@ -293,9 +449,9 @@ class DeviceBooksModel(BooksModel):
break
if add:
result.append(i)
self.map = result
self.reset()
self.emit(SIGNAL('searched()'))
def sort(self, col, order):
if not self.db:
@ -330,7 +486,6 @@ class DeviceBooksModel(BooksModel):
self.sorted_map.sort(cmp=fcmp, reverse=descending)
self.sorted_on = (col, order)
self.reset()
self.emit(SIGNAL('sorted()'))
def columnCount(self, parent):
return 4
@ -342,6 +497,34 @@ class DeviceBooksModel(BooksModel):
self.db = db
self.map = list(range(0, len(db)))
def current_changed(self, current, previous):
data = {}
item = self.db[self.map[current.row()]]
cdata = item.thumbnail
if cdata:
data['cover'] = cdata
type = 'Unknown'
ext = os.path.splitext(item.path)[1]
if ext:
type = ext[1:].lower()
data['Format'] = type
data['Path'] = item.path
dt = item.datetime
dt = datetime(*dt[0:6])
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
data['Timestamp'] = dt.ctime()
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
def paths(self, rows):
return [self.db[self.map[r.row()]].path for r in rows ]
def indices(self, rows):
'''
Return indices into underlying database from rows
'''
return [ self.map[r.row()] for r in rows]
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column()
@ -373,6 +556,8 @@ class DeviceBooksModel(BooksModel):
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]:
return QVariant("Double click to <b>edit</b> me<br><br>")
return NONE
@ -399,25 +584,32 @@ class DeviceBooksModel(BooksModel):
return done
class SearchBox(QLineEdit):
INTERVAL = 1000 #: Time to wait before emitting search signal
def __init__(self, parent):
QLineEdit.__init__(self, parent)
self.help_text = 'Search by title, author, publisher, tags and comments'
self.setText(self.help_text)
self.home(False)
QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot)
self.default_palette = QApplication.palette(self)
gray = QPalette(self.default_palette)
gray.setBrush(QPalette.Text, QBrush(QColor('lightgray')))
self.setPalette(gray)
self.initial_state = True
self.default_palette = QApplication.palette(self)
self.gray = QPalette(self.default_palette)
self.gray.setBrush(QPalette.Text, QBrush(QColor('lightgray')))
self.prev_search = ''
self.timer = None
self.interval = 1000 #: Time to wait before emitting search signal
self.clear_to_help()
QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot)
def normalize_state(self):
self.setText('')
self.setPalette(self.default_palette)
def clear_to_help(self):
self.setText(self.help_text)
self.home(False)
self.setPalette(self.gray)
self.initial_state = True
def keyPressEvent(self, event):
if self.initial_state:
self.normalize_state()
@ -433,7 +625,7 @@ class SearchBox(QLineEdit):
def text_edited_slot(self, text):
text = str(text)
self.prev_text = text
self.timer = self.startTimer(self.interval)
self.timer = self.startTimer(self.__class__.INTERVAL)
def timerEvent(self, event):
self.killTimer(event.timerId())

View File

@ -12,14 +12,18 @@
## 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.Warning
from libprs500.devices.interface import Device
from libprs500 import __appname__
import os, tempfile, sys
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QSettings, QVariant, QSize, QThread
from PyQt4.QtGui import QErrorMessage
QSettings, QVariant, QSize, QThread, QBuffer, QByteArray
from PyQt4.QtGui import QErrorMessage, QPixmap, QColor, QPainter, QMenu, QIcon
from PyQt4.QtSvg import QSvgRenderer
from libprs500 import __version__ as VERSION
from libprs500.gui2 import APP_TITLE, installErrorHandler
from libprs500.ebooks.metadata.meta import get_metadata
from libprs500.gui2 import APP_TITLE, installErrorHandler, choose_files
from libprs500.gui2.main_ui import Ui_MainWindow
from libprs500.gui2.device import DeviceDetector, DeviceManager
from libprs500.gui2.status import StatusBar
@ -27,6 +31,19 @@ from libprs500.gui2.jobs import JobManager, JobException
class Main(QObject, Ui_MainWindow):
def set_default_thumbnail(self, height):
r = QSvgRenderer(':/images/book.svg')
pixmap = QPixmap(height, height)
pixmap.fill(QColor(255,255,255))
p = QPainter(pixmap)
r.render(p)
p.end()
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly)
pixmap.save(buf, 'JPEG')
self.default_thumbnail = (pixmap.width(), pixmap.height(), ba.data())
def __init__(self, window):
QObject.__init__(self)
Ui_MainWindow.__init__(self)
@ -35,8 +52,9 @@ class Main(QObject, Ui_MainWindow):
self.read_settings()
self.job_manager = JobManager()
self.device_manager = None
self.temporary_slots = {}
self.upload_memory = {}
self.delete_memory = {}
self.default_thumbnail = None
####################### Location View ########################
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
self.location_selected)
@ -51,17 +69,36 @@ class Main(QObject, Ui_MainWindow):
QObject.connect(self.job_manager, SIGNAL('job_added(int)'), self.status_bar.job_added)
QObject.connect(self.job_manager, SIGNAL('no_more_jobs()'), self.status_bar.no_more_jobs)
####################### Setup Toolbar #####################
sm = QMenu()
sm.addAction(QIcon(':/images/reader.svg'), 'Send to main memory')
sm.addAction(QIcon(':/images/sd.svg'), 'Send to storage card')
self.sync_menu = sm # Needed
QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add_books)
QObject.connect(self.action_del, SIGNAL("triggered(bool)"), self.delete_books)
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"), self.edit_metadata)
QObject.connect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_main_memory)
QObject.connect(sm.actions()[0], SIGNAL('triggered(bool)'), self.sync_to_main_memory)
QObject.connect(sm.actions()[1], SIGNAL('triggered(bool)'), self.sync_to_card)
self.action_sync.setMenu(sm)
self.tool_bar.insertAction(self.action_edit, self.action_sync)
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
####################### Library view ########################
self.library_view.set_database(self.database_path)
self.library_view.connect_to_search_box(self.search)
self.memory_view.connect_to_search_box(self.search)
self.card_view.connect_to_search_box(self.search)
for func, target in [
('connect_to_search_box', self.search),
('connect_to_book_display', self.status_bar.book_info.show_data),
]:
for view in (self.library_view, self.memory_view, self.card_view):
getattr(view, func)(target)
self.memory_view.connect_dirtied_signal(self.upload_booklists)
self.card_view.connect_dirtied_signal(self.upload_booklists)
window.closeEvent = self.close_event
window.show()
self.stack.setCurrentIndex(0)
self.library_view.migrate_database()
self.library_view.sortByColumn(3, Qt.DescendingOrder)
self.library_view.resizeColumnsToContents()
@ -75,31 +112,36 @@ class Main(QObject, Ui_MainWindow):
self.detector.start(QThread.InheritPriority)
def upload_booklists(self):
booklists = self.memory_view.model().db, self.card_view.model().db
self.job_manager.run_device_job(None, self.device_manager.sync_booklists_func(),
booklists)
def current_view(self):
'''Convenience method that returns the currently visible view '''
idx = self.stack.currentIndex()
if idx == 0:
return self.library_view
if idx == 1:
return self.memory_view
if idx == 2:
return self.card_view
def location_selected(self, location):
page = 0 if location == 'library' else 1 if location == 'main' else 2
self.stack.setCurrentIndex(page)
if page == 1:
self.memory_view.resizeRowsToContents()
self.memory_view.resizeColumnsToContents()
if page == 2:
self.card_view.resizeRowsToContents()
self.card_view.resizeColumnsToContents()
def booklists(self):
return self.memory_view.model().db, self.card_view.model().db
def job_exception(self, id, exception, formatted_traceback):
raise JobException, str(exception) + '\n\r' + formatted_traceback
########################## Connect to device ##############################
def device_detected(self, cls, connected):
'''
Called when a device is connected to the computer.
'''
if connected:
self.device_manager = DeviceManager(cls)
func = self.device_manager.info_func()
self.job_manager.run_device_job(self.info_read, func)
self.set_default_thumbnail(cls.THUMBNAIL_HEIGHT)
def info_read(self, id, result, exception, formatted_traceback):
'''
Called once device information has been read.
'''
if exception:
self.job_exception(id, exception, formatted_traceback)
return
@ -107,20 +149,232 @@ class Main(QObject, Ui_MainWindow):
self.location_view.model().update_devices(cp, fs)
self.vanity.setText(self.vanity_template.arg('Connected '+' '.join(info[:-1])))
func = self.device_manager.books_func()
self.job_manager.run_device_job(self.books_read, func)
self.job_manager.run_device_job(self.metadata_downloaded, func)
def books_read(self, id, result, exception, formatted_traceback):
def metadata_downloaded(self, id, result, exception, formatted_traceback):
'''
Called once metadata has been read for all books on the device.
'''
if exception:
self.job_exception(id, exception, formatted_traceback)
return
mainlist, cardlist = result
self.memory_view.set_database(mainlist)
self.card_view.set_database(cardlist)
self.memory_view.sortByColumn(3, Qt.DescendingOrder)
self.card_view.sortByColumn(3, Qt.DescendingOrder)
self.location_selected('main')
for view in (self.memory_view, self.card_view):
view.sortByColumn(3, Qt.DescendingOrder)
view.resizeColumnsToContents()
view.resizeRowsToContents()
view.resize_on_select = not view.isVisible()
#self.location_selected('main')
############################################################################
############################# Upload booklists #############################
def upload_booklists(self):
'''
Upload metadata to device.
'''
self.job_manager.run_device_job(self.metadata_synced,
self.device_manager.sync_booklists_func(),
self.booklists())
def metadata_synced(self, id, result, exception, formatted_traceback):
'''
Called once metadata has been uploaded.
'''
if exception:
self.job_exception(id, exception, formatted_traceback)
return
############################################################################
################################# Add books ################################
def add_books(self, checked):
'''
Add books from the local filesystem to either the library or the device.
'''
books = choose_files(self.window, 'add books dialog dir', 'Select books', 'Books',
extensions=['lrf', 'lrx', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'html', 'xhtml', 'epub'])
if not books:
return
on_card = False if self.stack.currentIndex() != 2 else True
# Get format and metadata information
formats, metadata, names, infos = [], [], [], []
for book in books:
format = os.path.splitext(book)[1]
format = format[1:] if format else None
stream = open(book, 'rb')
mi = get_metadata(stream, stream_type=format)
if not mi.title:
mi.title = os.path.splitext(os.path.basename(book))[0]
formats.append(format)
metadata.append(mi)
names.append(os.path.basename(book))
infos.append({'title':mi.title, 'authors':mi.author, 'cover':self.default_thumbnail})
if self.stack.currentIndex() == 0:
model = self.current_view().model()
model.add_books(books, formats, metadata)
model.resort()
model.research()
else:
self.upload_books(books, names, infos, on_card=on_card)
def upload_books(self, files, names, metadata, on_card=False):
'''
Upload books to device.
@param files: List of either paths to files or file like objects
'''
id = self.job_manager.run_device_job(self.books_uploaded,
self.device_manager.upload_books_func(),
files, names, on_card=on_card
)
self.upload_memory[id] = metadata
def books_uploaded(self, id, result, exception, formatted_traceback):
'''
Called once books have been uploaded.
'''
metadata = self.upload_memory.pop(id)
if exception:
self.job_exception(id, exception, formatted_traceback)
return
self.device_manager.add_books_to_metadata(result, metadata, self.booklists())
self.upload_booklists()
for view in (self.memory_view, self.card_view):
view.model().resort()
view.model().research()
############################################################################
############################### Delete books ###############################
def delete_books(self, checked):
'''
Delete selected books from device or library.
'''
view = self.current_view()
rows = view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
return
if self.stack.currentIndex() == 0:
view.model().delete_books(rows)
else:
view = self.memory_view if self.stack.currentIndex() == 1 else self.card_view
paths = view.model().paths(rows)
id = self.remove_paths(paths)
self.delete_memory[id] = paths
view.model().mark_for_deletion(id, rows)
def remove_paths(self, paths):
return self.job_manager.run_device_job(self.books_deleted,
self.device_manager.delete_books_func(), paths)
def books_deleted(self, id, result, exception, formatted_traceback):
'''
Called once deletion is done on the device
'''
for view in (self.memory_view, self.card_view):
view.model().deletion_done(id, bool(exception))
if exception:
self.job_exception(id, exception, formatted_traceback)
return
self.upload_booklists()
if self.delete_memory.has_key(id):
paths = self.delete_memory.pop(id)
self.device_manager.remove_books_from_metadata(paths, self.booklists())
for view in (self.memory_view, self.card_view):
view.model().remap()
############################################################################
############################### Edit metadata ##############################
def edit_metadata(self, checked):
'''
Edit metadata of selected books in library or on device.
'''
pass
############################################################################
############################# Syncing to device#############################
def sync_to_main_memory(self, checked):
self.sync_to_device(False)
def sync_to_card(self, checked):
self.sync_to_device(True)
def cover_to_thumbnail(self, data):
p = QPixmap()
p.loadFromData(data)
if not p.isNull():
ht = self.device_manager.device_class.THUMBNAIL_HEIGHT if self.device_manager else \
Device.THUMBNAIL_HEIGHT
p = p.scaledToHeight(ht)
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly)
p.save(buf, 'JPEG')
return (p.width(), p.height(), ba.data())
def sync_to_device(self, on_card):
rows = self.library_view.selectionModel().selectedRows()
if not self.device_manager or not rows or len(rows) == 0:
return
ids = iter(self.library_view.model().id(r) for r in rows)
metadata = self.library_view.model().get_metadata(rows)
for mi in metadata:
cdata = mi['cover']
if cdata:
mi['cover'] = self.cover_to_thumbnail(cdata)
metadata = iter(metadata)
files = self.library_view.model().get_preferred_formats(rows,
self.device_manager.device_class.FORMATS)
bad, good, gf, names = [], [], [], []
for f in files:
mi = metadata.next()
id = ids.next()
if f is None:
bad.append(mi)
else:
good.append(mi)
gf.append(f)
names.append('%s_%d%s'%(__appname__, id, os.path.splitext(f.name)[1]))
self.upload_books(gf, names, good, on_card)
raise Exception, str(bad)
############################################################################
def location_selected(self, location):
'''
Called when a location icon is clicked (e.g. Library)
'''
page = 0 if location == 'library' else 1 if location == 'main' else 2
self.stack.setCurrentIndex(page)
view = self.memory_view if page == 1 else self.card_view if page == 2 else None
if view:
if view.resize_on_select:
view.resizeRowsToContents()
view.resizeColumnsToContents()
view.resize_on_select = False
def job_exception(self, id, exception, formatted_traceback):
'''
Handle exceptions in threaded jobs.
'''
raise JobException, str(exception) + '\n\r' + formatted_traceback
def read_settings(self):
settings = QSettings()
@ -149,9 +403,14 @@ def main():
sys.exit(1)
from PyQt4.Qt import QApplication, QMainWindow
app = QApplication(sys.argv)
#from IPython.Shell import IPShellEmbed
#ipshell = IPShellEmbed([],
# banner = 'Dropping into IPython',
# exit_msg = 'Leaving Interpreter, back to program.')
#ipshell()
#return 0
window = QMainWindow()
window.setWindowTitle(APP_TITLE)
#window.setWindowIcon(QIcon(":/icon"))
installErrorHandler(QErrorMessage(window))
QCoreApplication.setOrganizationName("KovidsBrain")
QCoreApplication.setApplicationName(APP_TITLE)
@ -167,4 +426,4 @@ def main():
if __name__ == '__main__':
main()
sys.exit(main())

View File

@ -163,7 +163,7 @@
<string>...</string>
</property>
<property name="icon" >
<iconset resource="images.qrc" >:/images/clear.png</iconset>
<iconset resource="images.qrc" >:/images/clear_left.svg</iconset>
</property>
</widget>
</item>
@ -328,10 +328,10 @@
</widget>
<action name="action_add" >
<property name="icon" >
<iconset resource="images.qrc" >:/images/addfile.png</iconset>
<iconset resource="images.qrc" >:/images/plus.svg</iconset>
</property>
<property name="text" >
<string>Add books to Library</string>
<string>Add books</string>
</property>
<property name="shortcut" >
<string>A</string>
@ -342,7 +342,7 @@
</action>
<action name="action_del" >
<property name="icon" >
<iconset resource="images.qrc" >:/images/delfile.png</iconset>
<iconset resource="images.qrc" >:/images/trash.svg</iconset>
</property>
<property name="text" >
<string>Delete books</string>
@ -353,10 +353,10 @@
</action>
<action name="action_edit" >
<property name="icon" >
<iconset resource="images.qrc" >:/images/edit.png</iconset>
<iconset resource="images.qrc" >:/images/edit_input.svg</iconset>
</property>
<property name="text" >
<string>Edit meta-information</string>
<string>Edit meta information</string>
</property>
<property name="shortcut" >
<string>E</string>
@ -365,6 +365,14 @@
<bool>false</bool>
</property>
</action>
<action name="action_sync" >
<property name="icon" >
<iconset resource="images.qrc" >:/images/sync.svg</iconset>
</property>
<property name="text" >
<string>Send to device</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'main.ui'
#
# Created: Wed Jun 27 16:19:53 2007
# Created: Thu Jul 19 21:07:39 2007
# by: PyQt4 UI code generator 4-snapshot-20070606
#
# WARNING! All changes made in this file will be lost!
@ -81,7 +81,7 @@ class Ui_MainWindow(object):
self.hboxlayout1.addWidget(self.search)
self.clear_button = QtGui.QToolButton(self.centralwidget)
self.clear_button.setIcon(QtGui.QIcon(":/images/clear.png"))
self.clear_button.setIcon(QtGui.QIcon(":/images/clear_left.svg"))
self.clear_button.setObjectName("clear_button")
self.hboxlayout1.addWidget(self.clear_button)
self.gridlayout.addLayout(self.hboxlayout1,1,0,1,1)
@ -184,18 +184,22 @@ class Ui_MainWindow(object):
MainWindow.setStatusBar(self.statusBar)
self.action_add = QtGui.QAction(MainWindow)
self.action_add.setIcon(QtGui.QIcon(":/images/addfile.png"))
self.action_add.setIcon(QtGui.QIcon(":/images/plus.svg"))
self.action_add.setAutoRepeat(False)
self.action_add.setObjectName("action_add")
self.action_del = QtGui.QAction(MainWindow)
self.action_del.setIcon(QtGui.QIcon(":/images/delfile.png"))
self.action_del.setIcon(QtGui.QIcon(":/images/trash.svg"))
self.action_del.setObjectName("action_del")
self.action_edit = QtGui.QAction(MainWindow)
self.action_edit.setIcon(QtGui.QIcon(":/images/edit.png"))
self.action_edit.setIcon(QtGui.QIcon(":/images/edit_input.svg"))
self.action_edit.setAutoRepeat(False)
self.action_edit.setObjectName("action_edit")
self.action_sync = QtGui.QAction(MainWindow)
self.action_sync.setIcon(QtGui.QIcon(":/images/sync.svg"))
self.action_sync.setObjectName("action_sync")
self.tool_bar.addAction(self.action_add)
self.tool_bar.addAction(self.action_del)
self.tool_bar.addAction(self.action_edit)
@ -214,12 +218,13 @@ class Ui_MainWindow(object):
self.search.setWhatsThis(QtGui.QApplication.translate("MainWindow", "Search the list of books by title, author, publisher, tags and comments<br><br>Words separated by spaces are ANDed", None, QtGui.QApplication.UnicodeUTF8))
self.clear_button.setToolTip(QtGui.QApplication.translate("MainWindow", "Reset Quick Search", None, QtGui.QApplication.UnicodeUTF8))
self.clear_button.setText(QtGui.QApplication.translate("MainWindow", "...", None, QtGui.QApplication.UnicodeUTF8))
self.action_add.setText(QtGui.QApplication.translate("MainWindow", "Add books to Library", None, QtGui.QApplication.UnicodeUTF8))
self.action_add.setText(QtGui.QApplication.translate("MainWindow", "Add books", None, QtGui.QApplication.UnicodeUTF8))
self.action_add.setShortcut(QtGui.QApplication.translate("MainWindow", "A", None, QtGui.QApplication.UnicodeUTF8))
self.action_del.setText(QtGui.QApplication.translate("MainWindow", "Delete books", None, QtGui.QApplication.UnicodeUTF8))
self.action_del.setShortcut(QtGui.QApplication.translate("MainWindow", "Del", None, QtGui.QApplication.UnicodeUTF8))
self.action_edit.setText(QtGui.QApplication.translate("MainWindow", "Edit meta-information", None, QtGui.QApplication.UnicodeUTF8))
self.action_edit.setText(QtGui.QApplication.translate("MainWindow", "Edit meta information", None, QtGui.QApplication.UnicodeUTF8))
self.action_edit.setShortcut(QtGui.QApplication.translate("MainWindow", "E", None, QtGui.QApplication.UnicodeUTF8))
self.action_sync.setText(QtGui.QApplication.translate("MainWindow", "Send to device", None, QtGui.QApplication.UnicodeUTF8))
from widgets import LocationView
from library import BooksView, DeviceBooksView, SearchBox

View File

@ -12,23 +12,24 @@
## 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 textwrap
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \
QSizePolicy
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap
from PyQt4.QtCore import Qt, QSize
class BookInfoDisplay(QFrame):
class BookCoverDisplay(QLabel):
WIDTH = 60
HEIGHT = 80
def __init__(self, coverpath=':default_cover'):
WIDTH = 80
HEIGHT = 100
def __init__(self, coverpath=':/images/book.svg'):
QLabel.__init__(self)
self.default_pixmap = QPixmap(coverpath).scaled(self.__class__.WIDTH,
self.__class__.HEIGHT,
Qt.IgnoreAspectRatio,
Qt.SmoothTransformation)
self.setPixmap(self.default_pixmap)
self.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
self.setMaximumSize(QSize(self.WIDTH, self.HEIGHT))
self.setScaledContents(True)
def sizeHint(self):
return QSize(self.__class__.WIDTH, self.__class__.HEIGHT)
@ -38,7 +39,7 @@ class BookInfoDisplay(QFrame):
def __init__(self):
QLabel.__init__(self)
self.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.setText('TODO')
self.setText('')#<table><tr><td>row 1</td><td>row 2</td></tr><tr><td>fsfdsfsfsfsfsfsdfsffsfsd</td></tr></table>')
def __init__(self):
QFrame.__init__(self)
@ -48,7 +49,28 @@ class BookInfoDisplay(QFrame):
self.layout.addWidget(self.cover_display)
self.book_data = BookInfoDisplay.BookDataDisplay()
self.layout.addWidget(self.book_data)
self.setVisible(False)
def show_data(self, data):
if data.has_key('cover'):
cover_data = data.pop('cover')
pixmap = QPixmap()
pixmap.loadFromData(cover_data)
if pixmap.isNull():
self.cover_display.setPixmap(self.cover_display.default_pixmap)
else:
self.cover_display.setPixmap(pixmap)
else:
self.cover_display.setPixmap(self.cover_display.default_pixmap)
rows = u''
self.book_data.setText('')
for key in data.keys():
txt = '<br />\n'.join(textwrap.wrap(data[key], 120))
rows += '<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
self.book_data.setText('<table>'+rows+'</table>')
self.setVisible(True)
class MovieButton(QLabel):
def __init__(self, movie):

View File

@ -24,8 +24,8 @@ class LocationModel(QAbstractListModel):
def __init__(self, parent):
QAbstractListModel.__init__(self, parent)
self.icons = [QVariant(QIcon(':/library')),
QVariant(QIcon(':/reader')),
QVariant(QIcon(':/card'))]
QVariant(QIcon(':/images/reader.svg')),
QVariant(QIcon(':/images/sd.svg'))]
self.text = ['Library',
'Reader\n%s available',
'Card\n%s available']

View File

@ -16,9 +16,8 @@
Backend that implements storage of ebooks in an sqlite database.
"""
import sqlite3 as sqlite
import os, datetime, re
import datetime, re
from zlib import compressobj, decompress
from stat import ST_SIZE
class Concatenate(object):
'''String concatenation aggregator for sqlite'''
@ -506,7 +505,7 @@ class LibraryDatabase(object):
text TEXT NON NULL COLLATE NOCASE,
UNIQUE(book)
);
CREATE INDEX comments_idx ON covers (book);
CREATE INDEX comments_idx ON comments (book);
CREATE TRIGGER fkc_comments_insert
BEFORE INSERT ON comments
BEGIN
@ -572,6 +571,9 @@ class LibraryDatabase(object):
return not self.conn.execute('SELECT id FROM books LIMIT 1').fetchone()
def refresh(self, sort_field, ascending):
'''
Rebuild self.data and self.cache. Filter results are lost.
'''
FIELDS = {'title' : 'sort',
'authors': 'authors_sort',
'publisher': 'publisher_sort',
@ -638,6 +640,43 @@ class LibraryDatabase(object):
def max_size(self, index):
return self.data[index][6]
def cover(self, index):
id = self.id(index)
matches = self.conn.execute('SELECT data from covers where id=?', (id,)).fetchall()
if not matches:
return None
raw = matches[0][0]
if raw:
return decompress(raw)
return None
def tags(self, index):
id = self.id(index)
matches = self.conn.execute('SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=?)', (id,)).fetchall()
if not matches:
return None
return matches[0][0]
def comments(self, index):
id = self.id(index)
matches = self.conn.execute('SELECT text FROM comments WHERE book=?', (id,)).fetchall()
if not matches:
return None
return matches[0][0]
def formats(self, index):
''' Return available formats as a comma separated list '''
id = self.id(index)
matches = self.conn.execute('SELECT concat(format) FROM data WHERE data.book=?', (id,)).fetchall()
if not matches:
return None
return matches[0][0]
def format(self, index, format):
id = self.id(index)
return decompress(self.conn.execute('SELECT data FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0])
def set(self, row, column, val):
''' Convenience method for setting the title, authors, publisher or rating '''
id = self.data[row][0]
@ -709,13 +748,23 @@ class LibraryDatabase(object):
self.conn.execute('INSERT INTO books_ratings_link(book, rating) VALUES (?,?)', (id, rat))
self.conn.commit()
def add_book(self, stream, format, mi, uri=None):
if not mi.author:
mi.author = 'Unknown'
def add_books(self, paths, formats, metadata, uris=[]):
'''
Add a book to the database. self.data and self.cache are not updated.
'''
formats, metadata, uris = iter(formats), iter(metadata), iter(uris)
for path in paths:
mi = metadata.next()
try:
uri = uris.next()
except StopIteration:
uri = None
obj = self.conn.execute('INSERT INTO books(title, uri, series_index) VALUES (?, ?, ?)',
(mi.title, uri, mi.series_index))
id = obj.lastrowid
self.conn.commit()
if not mi.author:
mi.author = 'Unknown'
temp = mi.author.split(',')
authors = []
for a in temp:
@ -727,10 +776,27 @@ class LibraryDatabase(object):
self.set_rating(id, mi.rating)
if mi.series:
self.set_series(id, mi.series)
stream = open(path, 'rb')
stream.seek(0, 2)
usize = stream.tell()
stream.seek(0)
format = formats.next()
self.conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?,?,?,?)',
(id, format, usize, compressobj().compress(stream)))
(id, format, usize, compressobj().compress(stream.read())))
stream.close()
self.conn.commit()
def delete_books(self, indices):
'''
Removes books from self.cache, self.data and underlying database.
'''
ids = [ self.id(i) for i in indices ]
cache_indices = [ idx for idx in range(len(self.cache)-1, -1, -1) if self.cache[idx][0] in ids ]
for idx in cache_indices:
self.cache[idx:idx+1] = []
for idx in indices:
self.data[idx:idx+1] = []
for id in ids:
self.conn.execute('DELETE FROM books WHERE id=?', (id,))
self.conn.commit()