mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Support for email ebooks from calibre to your device. To use setup Email delivery in the preferences and then click the arrow next to the Send to Device button. Still experimental, so report bugs.
This commit is contained in:
parent
dbf89cb85e
commit
63ebe19a5f
@ -17,22 +17,25 @@ def option_parser():
|
|||||||
|
|
||||||
Run an embedded python interpreter.
|
Run an embedded python interpreter.
|
||||||
''')
|
''')
|
||||||
parser.add_option('--update-module', help='Update the specified module in the frozen library. '+
|
parser.add_option('--update-module',
|
||||||
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None
|
help='Update the specified module in the frozen library. '+
|
||||||
|
'Module specifications are of the form full.name.of.module,path_to_module.py',
|
||||||
|
default=None
|
||||||
)
|
)
|
||||||
parser.add_option('-c', '--command', help='Run python code.', default=None)
|
parser.add_option('-c', '--command', help='Run python code.', default=None)
|
||||||
parser.add_option('-e', '--exec-file', default=None, help='Run the python code in file.')
|
parser.add_option('-e', '--exec-file', default=None, help='Run the python code in file.')
|
||||||
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
|
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
|
||||||
help='Debug the specified device driver.')
|
help='Debug the specified device driver.')
|
||||||
parser.add_option('-g', '--gui', default=False, action='store_true',
|
parser.add_option('-g', '--gui', default=False, action='store_true',
|
||||||
help='Run the GUI',)
|
help='Run the GUI',)
|
||||||
parser.add_option('--migrate', action='store_true', default=False,
|
parser.add_option('--migrate', action='store_true', default=False,
|
||||||
help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.')
|
help='Migrate old database. Needs two arguments. Path '
|
||||||
|
'to library1.db and path to new library folder.')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def update_zipfile(zipfile, mod, path):
|
def update_zipfile(zipfile, mod, path):
|
||||||
if 'win32' in sys.platform:
|
if 'win32' in sys.platform:
|
||||||
print 'WARNING: On Windows Vista using this option may cause windows to put library.zip into the Virtual Store (typically located in c:\Users\username\AppData\Local\VirtualStore). If it does this you must delete it from there after you\'re done debugging).'
|
print 'WARNING: On Windows Vista using this option may cause windows to put library.zip into the Virtual Store (typically located in c:\Users\username\AppData\Local\VirtualStore). If it does this you must delete it from there after you\'re done debugging).'
|
||||||
pat = re.compile(mod.replace('.', '/')+r'\.py[co]*')
|
pat = re.compile(mod.replace('.', '/')+r'\.py[co]*')
|
||||||
name = mod.replace('.', '/') + os.path.splitext(path)[-1]
|
name = mod.replace('.', '/') + os.path.splitext(path)[-1]
|
||||||
update(zipfile, [pat], [path], [name])
|
update(zipfile, [pat], [path], [name])
|
||||||
@ -46,8 +49,8 @@ def update_module(mod, path):
|
|||||||
zp = os.path.join(os.path.dirname(sys.executable), 'library.zip')
|
zp = os.path.join(os.path.dirname(sys.executable), 'library.zip')
|
||||||
elif isosx:
|
elif isosx:
|
||||||
zp = os.path.join(os.path.dirname(getattr(sys, 'frameworks_dir')),
|
zp = os.path.join(os.path.dirname(getattr(sys, 'frameworks_dir')),
|
||||||
'Resources', 'lib',
|
'Resources', 'lib',
|
||||||
'python'+'.'.join(map(str, sys.version_info[:2])),
|
'python'+'.'.join(map(str, sys.version_info[:2])),
|
||||||
'site-packages.zip')
|
'site-packages.zip')
|
||||||
else:
|
else:
|
||||||
zp = os.path.join(getattr(sys, 'frozen_path'), 'loader.zip')
|
zp = os.path.join(getattr(sys, 'frozen_path'), 'loader.zip')
|
||||||
@ -71,23 +74,23 @@ def migrate(old, new):
|
|||||||
self.max = max
|
self.max = max
|
||||||
def setValue(self, val):
|
def setValue(self, val):
|
||||||
self.update(float(val)/getattr(self, 'max', 1))
|
self.update(float(val)/getattr(self, 'max', 1))
|
||||||
|
|
||||||
db = LibraryDatabase(old)
|
db = LibraryDatabase(old)
|
||||||
db2 = LibraryDatabase2(new)
|
db2 = LibraryDatabase2(new)
|
||||||
db2.migrate_old(db, Dummy(terminal_controller, 'Migrating database...'))
|
db2.migrate_old(db, Dummy(terminal_controller, 'Migrating database...'))
|
||||||
prefs['library_path'] = os.path.abspath(new)
|
prefs['library_path'] = os.path.abspath(new)
|
||||||
print 'Database migrated to', os.path.abspath(new)
|
print 'Database migrated to', os.path.abspath(new)
|
||||||
|
|
||||||
def debug_device_driver():
|
def debug_device_driver():
|
||||||
from calibre.devices.scanner import DeviceScanner
|
from calibre.devices.scanner import DeviceScanner
|
||||||
s = DeviceScanner()
|
s = DeviceScanner()
|
||||||
s.scan()
|
s.scan()
|
||||||
print 'USB devices on system:', repr(s.devices)
|
print 'USB devices on system:', repr(s.devices)
|
||||||
if iswindows:
|
if iswindows:
|
||||||
wmi = __import__('wmi', globals(), locals(), [], -1)
|
wmi = __import__('wmi', globals(), locals(), [], -1)
|
||||||
drives = []
|
drives = []
|
||||||
print 'Drives detected:'
|
print 'Drives detected:'
|
||||||
print '\t', '(ID, Partitions, Drive letter)'
|
print '\t', '(ID, Partitions, Drive letter)'
|
||||||
for drive in wmi.WMI().Win32_DiskDrive():
|
for drive in wmi.WMI().Win32_DiskDrive():
|
||||||
if drive.Partitions == 0:
|
if drive.Partitions == 0:
|
||||||
continue
|
continue
|
||||||
@ -111,7 +114,7 @@ def debug_device_driver():
|
|||||||
d.open()
|
d.open()
|
||||||
print 'Total space:', d.total_space()
|
print 'Total space:', d.total_space()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
opts, args = option_parser().parse_args(args)
|
opts, args = option_parser().parse_args(args)
|
||||||
|
@ -4,32 +4,32 @@ __copyright__ = '2009, John Schember <john at nachtimwald.com>'
|
|||||||
Device driver for Amazon's Kindle
|
Device driver for Amazon's Kindle
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import os, re
|
import os, re, sys
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS, metadata_from_formats
|
from calibre.devices.usbms.driver import USBMS, metadata_from_formats
|
||||||
|
|
||||||
class KINDLE(USBMS):
|
class KINDLE(USBMS):
|
||||||
# Ordered list of supported formats
|
# Ordered list of supported formats
|
||||||
FORMATS = ['azw', 'mobi', 'prc', 'azw1', 'tpz', 'txt']
|
FORMATS = ['azw', 'mobi', 'prc', 'azw1', 'tpz', 'txt']
|
||||||
|
|
||||||
VENDOR_ID = [0x1949]
|
VENDOR_ID = [0x1949]
|
||||||
PRODUCT_ID = [0x0001]
|
PRODUCT_ID = [0x0001]
|
||||||
BCD = [0x399]
|
BCD = [0x399]
|
||||||
|
|
||||||
VENDOR_NAME = 'KINDLE'
|
VENDOR_NAME = 'KINDLE'
|
||||||
WINDOWS_MAIN_MEM = 'INTERNAL_STORAGE'
|
WINDOWS_MAIN_MEM = 'INTERNAL_STORAGE'
|
||||||
WINDOWS_CARD_MEM = 'CARD_STORAGE'
|
WINDOWS_CARD_MEM = 'CARD_STORAGE'
|
||||||
|
|
||||||
OSX_MAIN_MEM = 'Kindle Internal Storage Media'
|
OSX_MAIN_MEM = 'Kindle Internal Storage Media'
|
||||||
OSX_CARD_MEM = 'Kindle Card Storage Media'
|
OSX_CARD_MEM = 'Kindle Card Storage Media'
|
||||||
|
|
||||||
MAIN_MEMORY_VOLUME_LABEL = 'Kindle Main Memory'
|
MAIN_MEMORY_VOLUME_LABEL = 'Kindle Main Memory'
|
||||||
STORAGE_CARD_VOLUME_LABEL = 'Kindle Storage Card'
|
STORAGE_CARD_VOLUME_LABEL = 'Kindle Storage Card'
|
||||||
|
|
||||||
EBOOK_DIR_MAIN = "documents"
|
EBOOK_DIR_MAIN = "documents"
|
||||||
EBOOK_DIR_CARD = "documents"
|
EBOOK_DIR_CARD = "documents"
|
||||||
SUPPORTS_SUB_DIRS = True
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
WIRELESS_FILE_NAME_PATTERN = re.compile(
|
WIRELESS_FILE_NAME_PATTERN = re.compile(
|
||||||
r'(?P<title>[^-]+)-asin_(?P<asin>[a-zA-Z\d]{10,})-type_(?P<type>\w{4})-v_(?P<index>\d+).*')
|
r'(?P<title>[^-]+)-asin_(?P<asin>[a-zA-Z\d]{10,})-type_(?P<type>\w{4})-v_(?P<index>\d+).*')
|
||||||
|
|
||||||
@ -37,13 +37,13 @@ class KINDLE(USBMS):
|
|||||||
for path in paths:
|
for path in paths:
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
filepath = os.path.splitext(path)[0]
|
filepath = os.path.splitext(path)[0]
|
||||||
|
|
||||||
# Delete the ebook auxiliary file
|
# Delete the ebook auxiliary file
|
||||||
if os.path.exists(filepath + '.mbp'):
|
if os.path.exists(filepath + '.mbp'):
|
||||||
os.unlink(filepath + '.mbp')
|
os.unlink(filepath + '.mbp')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def metadata_from_path(cls, path):
|
def metadata_from_path(cls, path):
|
||||||
mi = metadata_from_formats([path])
|
mi = metadata_from_formats([path])
|
||||||
@ -51,10 +51,13 @@ class KINDLE(USBMS):
|
|||||||
match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path))
|
match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path))
|
||||||
if match is not None:
|
if match is not None:
|
||||||
mi.title = match.group('title')
|
mi.title = match.group('title')
|
||||||
|
if not isinstance(mi.title, unicode):
|
||||||
|
mi.title = mi.title.decode(sys.getfilesystemencoding(),
|
||||||
|
'replace')
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
|
|
||||||
class KINDLE2(KINDLE):
|
class KINDLE2(KINDLE):
|
||||||
|
|
||||||
PRODUCT_ID = [0x0002]
|
PRODUCT_ID = [0x0002]
|
||||||
BCD = [0x0100]
|
BCD = [0x0100]
|
||||||
|
@ -38,8 +38,10 @@ class UnsupportedFormatError(Exception):
|
|||||||
class SpineItem(unicode):
|
class SpineItem(unicode):
|
||||||
|
|
||||||
def __new__(cls, *args):
|
def __new__(cls, *args):
|
||||||
|
args = list(args)
|
||||||
|
args[0] = args[0].partition('#')[0]
|
||||||
obj = super(SpineItem, cls).__new__(cls, *args)
|
obj = super(SpineItem, cls).__new__(cls, *args)
|
||||||
path = args[0].partition('#')[0]
|
path = args[0]
|
||||||
raw = open(path, 'rb').read()
|
raw = open(path, 'rb').read()
|
||||||
raw, obj.encoding = xml_to_unicode(raw)
|
raw, obj.encoding = xml_to_unicode(raw)
|
||||||
obj.character_count = character_count(raw)
|
obj.character_count = character_count(raw)
|
||||||
@ -67,6 +69,7 @@ class EbookIterator(object):
|
|||||||
CHARACTERS_PER_PAGE = 1000
|
CHARACTERS_PER_PAGE = 1000
|
||||||
|
|
||||||
def __init__(self, pathtoebook):
|
def __init__(self, pathtoebook):
|
||||||
|
pathtoebook = pathtoebook.strip()
|
||||||
self.pathtoebook = os.path.abspath(pathtoebook)
|
self.pathtoebook = os.path.abspath(pathtoebook)
|
||||||
self.config = DynamicConfig(name='iterator')
|
self.config = DynamicConfig(name='iterator')
|
||||||
ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower()
|
ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower()
|
||||||
|
@ -8,7 +8,7 @@ import sys, re, socket
|
|||||||
from urllib import urlopen, quote
|
from urllib import urlopen, quote
|
||||||
|
|
||||||
from calibre.utils.config import OptionParser
|
from calibre.utils.config import OptionParser
|
||||||
from calibre.ebooks.metadata import MetaInformation, authors_to_sort_string
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
|
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
|
||||||
|
|
||||||
BASE_URL = 'http://isbndb.com/api/books.xml?access_key=%(key)s&page_number=1&results=subjects,authors,texts&'
|
BASE_URL = 'http://isbndb.com/api/books.xml?access_key=%(key)s&page_number=1&results=subjects,authors,texts&'
|
||||||
|
@ -9,7 +9,7 @@ lxml based OPF parser.
|
|||||||
|
|
||||||
import sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO
|
import sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse, urldefrag
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
@ -444,7 +444,7 @@ class OPF(object):
|
|||||||
if not hasattr(stream, 'read'):
|
if not hasattr(stream, 'read'):
|
||||||
stream = open(stream, 'rb')
|
stream = open(stream, 'rb')
|
||||||
self.basedir = self.base_dir = basedir
|
self.basedir = self.base_dir = basedir
|
||||||
self.path_to_html_toc = None
|
self.path_to_html_toc = self.html_toc_fragment = None
|
||||||
raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True)
|
raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True)
|
||||||
raw = raw[raw.find('<'):]
|
raw = raw[raw.find('<'):]
|
||||||
self.root = etree.fromstring(raw, self.PARSER)
|
self.root = etree.fromstring(raw, self.PARSER)
|
||||||
@ -496,7 +496,8 @@ class OPF(object):
|
|||||||
if f:
|
if f:
|
||||||
self.toc.read_ncx_toc(f[0])
|
self.toc.read_ncx_toc(f[0])
|
||||||
else:
|
else:
|
||||||
self.path_to_html_toc = toc
|
self.path_to_html_toc, self.html_toc_fragment = \
|
||||||
|
toc.partition('#')[0], toc.partition('#')[-1]
|
||||||
self.toc.read_html_toc(toc)
|
self.toc.read_html_toc(toc)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@ -627,7 +628,7 @@ class OPF(object):
|
|||||||
attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'}
|
attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'}
|
||||||
elem = self.create_metadata_element('creator', attrib=attrib)
|
elem = self.create_metadata_element('creator', attrib=attrib)
|
||||||
self.set_text(elem, author.strip())
|
self.set_text(elem, author.strip())
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
@apply
|
@apply
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,15 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
""" The GUI """
|
""" The GUI """
|
||||||
import sys, os, re, StringIO, traceback, time
|
import os
|
||||||
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
||||||
QByteArray, QLocale, QUrl, QTranslator, QCoreApplication, \
|
QByteArray, QUrl, QTranslator, QCoreApplication
|
||||||
QModelIndex
|
|
||||||
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||||
QIcon, QTableView, QDialogButtonBox, QApplication, QDialog
|
QIcon, QTableView, QApplication, QDialog
|
||||||
|
|
||||||
ORG_NAME = 'KovidsBrain'
|
ORG_NAME = 'KovidsBrain'
|
||||||
APP_UID = 'libprs500'
|
APP_UID = 'libprs500'
|
||||||
from calibre import __author__, islinux, iswindows, isosx
|
from calibre import islinux, iswindows
|
||||||
from calibre.startup import get_lang
|
from calibre.startup import get_lang
|
||||||
from calibre.utils.config import Config, ConfigProxy, dynamic
|
from calibre.utils.config import Config, ConfigProxy, dynamic
|
||||||
import calibre.resources as resources
|
import calibre.resources as resources
|
||||||
@ -32,7 +31,7 @@ def _config():
|
|||||||
help=_('The format to use when saving single files to disk'))
|
help=_('The format to use when saving single files to disk'))
|
||||||
c.add_opt('confirm_delete', default=False,
|
c.add_opt('confirm_delete', default=False,
|
||||||
help=_('Confirm before deleting'))
|
help=_('Confirm before deleting'))
|
||||||
c.add_opt('toolbar_icon_size', default=QSize(48, 48),
|
c.add_opt('toolbar_icon_size', default=QSize(48, 48),
|
||||||
help=_('Toolbar icon size')) # value QVariant.toSize
|
help=_('Toolbar icon size')) # value QVariant.toSize
|
||||||
c.add_opt('show_text_in_toolbar', default=True,
|
c.add_opt('show_text_in_toolbar', default=True,
|
||||||
help=_('Show button labels in the toolbar'))
|
help=_('Show button labels in the toolbar'))
|
||||||
@ -57,16 +56,19 @@ def _config():
|
|||||||
c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup'))
|
c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup'))
|
||||||
c.add_opt('oldest_news', default=60, help=_('Oldest news kept in database'))
|
c.add_opt('oldest_news', default=60, help=_('Oldest news kept in database'))
|
||||||
c.add_opt('systray_icon', default=True, help=_('Show system tray icon'))
|
c.add_opt('systray_icon', default=True, help=_('Show system tray icon'))
|
||||||
c.add_opt('upload_news_to_device', default=True,
|
c.add_opt('upload_news_to_device', default=True,
|
||||||
help=_('Upload downloaded news to device'))
|
help=_('Upload downloaded news to device'))
|
||||||
c.add_opt('delete_news_from_library_on_upload', default=False,
|
c.add_opt('delete_news_from_library_on_upload', default=False,
|
||||||
help=_('Delete books from library after uploading to device'))
|
help=_('Delete books from library after uploading to device'))
|
||||||
c.add_opt('separate_cover_flow', default=False,
|
c.add_opt('separate_cover_flow', default=False,
|
||||||
help=_('Show the cover flow in a separate window instead of in the main calibre window'))
|
help=_('Show the cover flow in a separate window instead of in the main calibre window'))
|
||||||
c.add_opt('disable_tray_notification', default=False,
|
c.add_opt('disable_tray_notification', default=False,
|
||||||
help=_('Disable notifications from the system tray icon'))
|
help=_('Disable notifications from the system tray icon'))
|
||||||
|
c.add_opt('default_send_to_device_action', default=None,
|
||||||
|
help=_('Default action to perform when send to device button is '
|
||||||
|
'clicked'))
|
||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
config = _config()
|
config = _config()
|
||||||
# Turn off DeprecationWarnings in windows GUI
|
# Turn off DeprecationWarnings in windows GUI
|
||||||
if iswindows:
|
if iswindows:
|
||||||
@ -139,16 +141,16 @@ def human_readable(size):
|
|||||||
|
|
||||||
class Dispatcher(QObject):
|
class Dispatcher(QObject):
|
||||||
'''Convenience class to ensure that a function call always happens in the GUI thread'''
|
'''Convenience class to ensure that a function call always happens in the GUI thread'''
|
||||||
|
SIGNAL = SIGNAL('dispatcher(PyQt_PyObject,PyQt_PyObject)')
|
||||||
|
|
||||||
def __init__(self, func):
|
def __init__(self, func):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
self.func = func
|
self.func = func
|
||||||
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'),
|
self.connect(self, self.SIGNAL, self.dispatch, Qt.QueuedConnection)
|
||||||
self.dispatch, Qt.QueuedConnection)
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'), args, kwargs)
|
self.emit(self.SIGNAL, args, kwargs)
|
||||||
|
|
||||||
def dispatch(self, args, kwargs):
|
def dispatch(self, args, kwargs):
|
||||||
self.func(*args, **kwargs)
|
self.func(*args, **kwargs)
|
||||||
|
|
||||||
@ -157,29 +159,29 @@ class GetMetadata(QObject):
|
|||||||
Convenience class to ensure that metadata readers are used only in the
|
Convenience class to ensure that metadata readers are used only in the
|
||||||
GUI thread. Must be instantiated in the GUI thread.
|
GUI thread. Must be instantiated in the GUI thread.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self._get_metadata, Qt.QueuedConnection)
|
self._get_metadata, Qt.QueuedConnection)
|
||||||
self.connect(self, SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
self.connect(self, SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self._from_formats, Qt.QueuedConnection)
|
self._from_formats, Qt.QueuedConnection)
|
||||||
|
|
||||||
def __call__(self, id, *args, **kwargs):
|
def __call__(self, id, *args, **kwargs):
|
||||||
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||||
id, args, kwargs)
|
id, args, kwargs)
|
||||||
|
|
||||||
def from_formats(self, id, *args, **kwargs):
|
def from_formats(self, id, *args, **kwargs):
|
||||||
self.emit(SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
self.emit(SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||||
id, args, kwargs)
|
id, args, kwargs)
|
||||||
|
|
||||||
def _from_formats(self, id, args, kwargs):
|
def _from_formats(self, id, args, kwargs):
|
||||||
try:
|
try:
|
||||||
mi = metadata_from_formats(*args, **kwargs)
|
mi = metadata_from_formats(*args, **kwargs)
|
||||||
except:
|
except:
|
||||||
mi = MetaInformation('', [_('Unknown')])
|
mi = MetaInformation('', [_('Unknown')])
|
||||||
self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi)
|
self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi)
|
||||||
|
|
||||||
def _get_metadata(self, id, args, kwargs):
|
def _get_metadata(self, id, args, kwargs):
|
||||||
try:
|
try:
|
||||||
mi = get_metadata(*args, **kwargs)
|
mi = get_metadata(*args, **kwargs)
|
||||||
@ -191,27 +193,27 @@ class TableView(QTableView):
|
|||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
QTableView.__init__(self, parent)
|
QTableView.__init__(self, parent)
|
||||||
self.read_settings()
|
self.read_settings()
|
||||||
|
|
||||||
def read_settings(self):
|
def read_settings(self):
|
||||||
self.cw = dynamic[self.__class__.__name__+'column widths']
|
self.cw = dynamic[self.__class__.__name__+'column widths']
|
||||||
|
|
||||||
def write_settings(self):
|
def write_settings(self):
|
||||||
dynamic[self.__class__.__name__+'column widths'] = \
|
dynamic[self.__class__.__name__+'column widths'] = \
|
||||||
tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])
|
tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])
|
||||||
|
|
||||||
def restore_column_widths(self):
|
def restore_column_widths(self):
|
||||||
if self.cw and len(self.cw):
|
if self.cw and len(self.cw):
|
||||||
for i in range(len(self.cw)):
|
for i in range(len(self.cw)):
|
||||||
self.setColumnWidth(i, self.cw[i])
|
self.setColumnWidth(i, self.cw[i])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class FileIconProvider(QFileIconProvider):
|
class FileIconProvider(QFileIconProvider):
|
||||||
|
|
||||||
ICONS = {
|
ICONS = {
|
||||||
'default' : 'unknown',
|
'default' : 'unknown',
|
||||||
'dir' : 'dir',
|
'dir' : 'dir',
|
||||||
'zero' : 'zero',
|
'zero' : 'zero',
|
||||||
|
|
||||||
'jpeg' : 'jpeg',
|
'jpeg' : 'jpeg',
|
||||||
'jpg' : 'jpeg',
|
'jpg' : 'jpeg',
|
||||||
'gif' : 'gif',
|
'gif' : 'gif',
|
||||||
@ -234,7 +236,7 @@ class FileIconProvider(QFileIconProvider):
|
|||||||
'mobi' : 'mobi',
|
'mobi' : 'mobi',
|
||||||
'epub' : 'epub',
|
'epub' : 'epub',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QFileIconProvider.__init__(self)
|
QFileIconProvider.__init__(self)
|
||||||
self.icons = {}
|
self.icons = {}
|
||||||
@ -242,14 +244,14 @@ class FileIconProvider(QFileIconProvider):
|
|||||||
self.icons[key] = ':/images/mimetypes/'+self.__class__.ICONS[key]+'.svg'
|
self.icons[key] = ':/images/mimetypes/'+self.__class__.ICONS[key]+'.svg'
|
||||||
for i in ('dir', 'default', 'zero'):
|
for i in ('dir', 'default', 'zero'):
|
||||||
self.icons[i] = QIcon(self.icons[i])
|
self.icons[i] = QIcon(self.icons[i])
|
||||||
|
|
||||||
def key_from_ext(self, ext):
|
def key_from_ext(self, ext):
|
||||||
key = ext if ext in self.icons.keys() else 'default'
|
key = ext if ext in self.icons.keys() else 'default'
|
||||||
if key == 'default' and ext.count('.') > 0:
|
if key == 'default' and ext.count('.') > 0:
|
||||||
ext = ext.rpartition('.')[2]
|
ext = ext.rpartition('.')[2]
|
||||||
key = ext if ext in self.icons.keys() else 'default'
|
key = ext if ext in self.icons.keys() else 'default'
|
||||||
return key
|
return key
|
||||||
|
|
||||||
def cached_icon(self, key):
|
def cached_icon(self, key):
|
||||||
candidate = self.icons[key]
|
candidate = self.icons[key]
|
||||||
if isinstance(candidate, QIcon):
|
if isinstance(candidate, QIcon):
|
||||||
@ -257,11 +259,11 @@ class FileIconProvider(QFileIconProvider):
|
|||||||
icon = QIcon(candidate)
|
icon = QIcon(candidate)
|
||||||
self.icons[key] = icon
|
self.icons[key] = icon
|
||||||
return icon
|
return icon
|
||||||
|
|
||||||
def icon_from_ext(self, ext):
|
def icon_from_ext(self, ext):
|
||||||
key = self.key_from_ext(ext.lower() if ext else '')
|
key = self.key_from_ext(ext.lower() if ext else '')
|
||||||
return self.cached_icon(key)
|
return self.cached_icon(key)
|
||||||
|
|
||||||
def load_icon(self, fileinfo):
|
def load_icon(self, fileinfo):
|
||||||
key = 'default'
|
key = 'default'
|
||||||
icons = self.icons
|
icons = self.icons
|
||||||
@ -275,7 +277,7 @@ class FileIconProvider(QFileIconProvider):
|
|||||||
ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
|
ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
|
||||||
key = self.key_from_ext(ext)
|
key = self.key_from_ext(ext)
|
||||||
return self.cached_icon(key)
|
return self.cached_icon(key)
|
||||||
|
|
||||||
def icon(self, arg):
|
def icon(self, arg):
|
||||||
if isinstance(arg, QFileInfo):
|
if isinstance(arg, QFileInfo):
|
||||||
return self.load_icon(arg)
|
return self.load_icon(arg)
|
||||||
@ -284,13 +286,13 @@ class FileIconProvider(QFileIconProvider):
|
|||||||
if arg == QFileIconProvider.File:
|
if arg == QFileIconProvider.File:
|
||||||
return self.icons['default']
|
return self.icons['default']
|
||||||
return QFileIconProvider.icon(self, arg)
|
return QFileIconProvider.icon(self, arg)
|
||||||
|
|
||||||
_file_icon_provider = None
|
_file_icon_provider = None
|
||||||
def initialize_file_icon_provider():
|
def initialize_file_icon_provider():
|
||||||
global _file_icon_provider
|
global _file_icon_provider
|
||||||
if _file_icon_provider is None:
|
if _file_icon_provider is None:
|
||||||
_file_icon_provider = FileIconProvider()
|
_file_icon_provider = FileIconProvider()
|
||||||
|
|
||||||
def file_icon_provider():
|
def file_icon_provider():
|
||||||
global _file_icon_provider
|
global _file_icon_provider
|
||||||
return _file_icon_provider
|
return _file_icon_provider
|
||||||
@ -299,13 +301,13 @@ _sidebar_directories = []
|
|||||||
def set_sidebar_directories(dirs):
|
def set_sidebar_directories(dirs):
|
||||||
global _sidebar_directories
|
global _sidebar_directories
|
||||||
if dirs is None:
|
if dirs is None:
|
||||||
dirs = config['frequently_used_directories']
|
dirs = config['frequently_used_directories']
|
||||||
_sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
|
_sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
|
||||||
|
|
||||||
class FileDialog(QObject):
|
class FileDialog(QObject):
|
||||||
def __init__(self, title='Choose Files',
|
def __init__(self, title='Choose Files',
|
||||||
filters=[],
|
filters=[],
|
||||||
add_all_files_filter=True,
|
add_all_files_filter=True,
|
||||||
parent=None,
|
parent=None,
|
||||||
modal = True,
|
modal = True,
|
||||||
name = '',
|
name = '',
|
||||||
@ -321,16 +323,16 @@ class FileDialog(QObject):
|
|||||||
ftext += '%s (%s);;'%(text, ' '.join(extensions))
|
ftext += '%s (%s);;'%(text, ' '.join(extensions))
|
||||||
if add_all_files_filter or not ftext:
|
if add_all_files_filter or not ftext:
|
||||||
ftext += 'All files (*)'
|
ftext += 'All files (*)'
|
||||||
|
|
||||||
self.dialog_name = name if name else 'dialog_' + title
|
self.dialog_name = name if name else 'dialog_' + title
|
||||||
self.selected_files = None
|
self.selected_files = None
|
||||||
self.fd = None
|
self.fd = None
|
||||||
|
|
||||||
if islinux:
|
if islinux:
|
||||||
self.fd = QFileDialog(parent)
|
self.fd = QFileDialog(parent)
|
||||||
self.fd.setFileMode(mode)
|
self.fd.setFileMode(mode)
|
||||||
self.fd.setIconProvider(_file_icon_provider)
|
self.fd.setIconProvider(_file_icon_provider)
|
||||||
self.fd.setModal(modal)
|
self.fd.setModal(modal)
|
||||||
self.fd.setNameFilter(ftext)
|
self.fd.setNameFilter(ftext)
|
||||||
self.fd.setWindowTitle(title)
|
self.fd.setWindowTitle(title)
|
||||||
state = dynamic[self.dialog_name]
|
state = dynamic[self.dialog_name]
|
||||||
@ -347,7 +349,7 @@ class FileDialog(QObject):
|
|||||||
f = qstring_to_unicode(
|
f = qstring_to_unicode(
|
||||||
QFileDialog.getSaveFileName(parent, title, dir, ftext, ""))
|
QFileDialog.getSaveFileName(parent, title, dir, ftext, ""))
|
||||||
if os.path.exists(f):
|
if os.path.exists(f):
|
||||||
self.selected_files.append(f)
|
self.selected_files.append(f)
|
||||||
elif mode == QFileDialog.ExistingFile:
|
elif mode == QFileDialog.ExistingFile:
|
||||||
f = qstring_to_unicode(
|
f = qstring_to_unicode(
|
||||||
QFileDialog.getOpenFileName(parent, title, dir, ftext, ""))
|
QFileDialog.getOpenFileName(parent, title, dir, ftext, ""))
|
||||||
@ -367,44 +369,44 @@ class FileDialog(QObject):
|
|||||||
if self.selected_files:
|
if self.selected_files:
|
||||||
self.selected_files = [qstring_to_unicode(q) for q in self.selected_files]
|
self.selected_files = [qstring_to_unicode(q) for q in self.selected_files]
|
||||||
dynamic[self.dialog_name] = os.path.dirname(self.selected_files[0])
|
dynamic[self.dialog_name] = os.path.dirname(self.selected_files[0])
|
||||||
self.accepted = bool(self.selected_files)
|
self.accepted = bool(self.selected_files)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_files(self):
|
def get_files(self):
|
||||||
if islinux and self.fd.result() != self.fd.Accepted:
|
if islinux and self.fd.result() != self.fd.Accepted:
|
||||||
return tuple()
|
return tuple()
|
||||||
if self.selected_files is None:
|
if self.selected_files is None:
|
||||||
return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles())
|
return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles())
|
||||||
return tuple(self.selected_files)
|
return tuple(self.selected_files)
|
||||||
|
|
||||||
def save_dir(self):
|
def save_dir(self):
|
||||||
if self.fd:
|
if self.fd:
|
||||||
dynamic[self.dialog_name] = self.fd.saveState()
|
dynamic[self.dialog_name] = self.fd.saveState()
|
||||||
|
|
||||||
|
|
||||||
def choose_dir(window, name, title):
|
def choose_dir(window, name, title):
|
||||||
fd = FileDialog(title, [], False, window, name=name,
|
fd = FileDialog(title, [], False, window, name=name,
|
||||||
mode=QFileDialog.DirectoryOnly)
|
mode=QFileDialog.DirectoryOnly)
|
||||||
dir = fd.get_files()
|
dir = fd.get_files()
|
||||||
if dir:
|
if dir:
|
||||||
return dir[0]
|
return dir[0]
|
||||||
|
|
||||||
def choose_files(window, name, title,
|
def choose_files(window, name, title,
|
||||||
filters=[], all_files=True, select_only_single_file=False):
|
filters=[], all_files=True, select_only_single_file=False):
|
||||||
'''
|
'''
|
||||||
Ask user to choose a bunch of files.
|
Ask user to choose a bunch of files.
|
||||||
@param name: Unique dialog name used to store the opened directory
|
@param name: Unique dialog name used to store the opened directory
|
||||||
@param title: Title to show in dialogs titlebar
|
@param title: Title to show in dialogs titlebar
|
||||||
@param filters: list of allowable extensions. Each element of the list
|
@param filters: list of allowable extensions. Each element of the list
|
||||||
must be a 2-tuple with first element a string describing
|
must be a 2-tuple with first element a string describing
|
||||||
the type of files to be filtered and second element a list
|
the type of files to be filtered and second element a list
|
||||||
of extensions.
|
of extensions.
|
||||||
@param all_files: If True add All files to filters.
|
@param all_files: If True add All files to filters.
|
||||||
@param select_only_single_file: If True only one file can be selected
|
@param select_only_single_file: If True only one file can be selected
|
||||||
'''
|
'''
|
||||||
mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
|
mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
|
||||||
fd = FileDialog(title=title, name=name, filters=filters,
|
fd = FileDialog(title=title, name=name, filters=filters,
|
||||||
parent=window, add_all_files_filter=all_files, mode=mode,
|
parent=window, add_all_files_filter=all_files, mode=mode,
|
||||||
)
|
)
|
||||||
if fd.accepted:
|
if fd.accepted:
|
||||||
@ -413,8 +415,8 @@ def choose_files(window, name, title,
|
|||||||
|
|
||||||
def choose_images(window, name, title, select_only_single_file=True):
|
def choose_images(window, name, title, select_only_single_file=True):
|
||||||
mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
|
mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
|
||||||
fd = FileDialog(title=title, name=name,
|
fd = FileDialog(title=title, name=name,
|
||||||
filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])],
|
filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])],
|
||||||
parent=window, add_all_files_filter=False, mode=mode,
|
parent=window, add_all_files_filter=False, mode=mode,
|
||||||
)
|
)
|
||||||
if fd.accepted:
|
if fd.accepted:
|
||||||
@ -432,7 +434,7 @@ def pixmap_to_data(pixmap, format='JPEG'):
|
|||||||
return str(ba.data())
|
return str(ba.data())
|
||||||
|
|
||||||
class ResizableDialog(QDialog):
|
class ResizableDialog(QDialog):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
QDialog.__init__(self, *args)
|
QDialog.__init__(self, *args)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
@ -444,14 +446,15 @@ class ResizableDialog(QDialog):
|
|||||||
nh = min(self.height(), nh)
|
nh = min(self.height(), nh)
|
||||||
nw = min(self.width(), nw)
|
nw = min(self.width(), nw)
|
||||||
self.resize(nw, nh)
|
self.resize(nw, nh)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.utils.single_qt_application import SingleApplication
|
from calibre.utils.single_qt_application import SingleApplication
|
||||||
|
SingleApplication
|
||||||
except:
|
except:
|
||||||
SingleApplication = None
|
SingleApplication = None
|
||||||
|
|
||||||
class Application(QApplication):
|
class Application(QApplication):
|
||||||
|
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
|
qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
|
||||||
QApplication.__init__(self, qargs)
|
QApplication.__init__(self, qargs)
|
||||||
@ -462,6 +465,6 @@ class Application(QApplication):
|
|||||||
if data:
|
if data:
|
||||||
self.translator.loadFromData(data)
|
self.translator.loadFromData(data)
|
||||||
self.installTranslator(self.translator)
|
self.installTranslator(self.translator)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,19 +1,35 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
import os, traceback, Queue, time
|
import os, traceback, Queue, time, socket
|
||||||
from threading import Thread
|
from threading import Thread, RLock
|
||||||
|
from itertools import repeat
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
|
||||||
|
Qt
|
||||||
|
|
||||||
from calibre.devices import devices
|
from calibre.devices import devices
|
||||||
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.parallel import Job
|
from calibre.parallel import Job
|
||||||
from calibre.devices.scanner import DeviceScanner
|
from calibre.devices.scanner import DeviceScanner
|
||||||
|
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
||||||
|
pixmap_to_data, warning_dialog
|
||||||
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
|
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
|
||||||
|
from calibre.devices.interface import Device
|
||||||
|
from calibre import sanitize_file_name, preferred_encoding
|
||||||
|
from calibre.utils.filenames import ascii_filename
|
||||||
|
from calibre.devices.errors import FreeSpaceError
|
||||||
|
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
||||||
|
config as email_config
|
||||||
|
|
||||||
|
|
||||||
class DeviceJob(Job):
|
class DeviceJob(Job):
|
||||||
|
|
||||||
def __init__(self, func, *args, **kwargs):
|
def __init__(self, func, *args, **kwargs):
|
||||||
Job.__init__(self, *args, **kwargs)
|
Job.__init__(self, *args, **kwargs)
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.start_work()
|
self.start_work()
|
||||||
try:
|
try:
|
||||||
@ -23,16 +39,12 @@ class DeviceJob(Job):
|
|||||||
self.traceback = traceback.format_exc()
|
self.traceback = traceback.format_exc()
|
||||||
finally:
|
finally:
|
||||||
self.job_done()
|
self.job_done()
|
||||||
|
|
||||||
|
|
||||||
class DeviceManager(Thread):
|
class DeviceManager(Thread):
|
||||||
'''
|
|
||||||
Worker thread that polls the USB ports for devices. Emits the
|
|
||||||
signal connected(PyQt_PyObject, PyQt_PyObject) on connection and
|
|
||||||
disconnection events.
|
|
||||||
'''
|
|
||||||
def __init__(self, connected_slot, job_manager, sleep_time=2):
|
def __init__(self, connected_slot, job_manager, sleep_time=2):
|
||||||
'''
|
'''
|
||||||
@param sleep_time: Time to sleep between device probes in millisecs
|
@param sleep_time: Time to sleep between device probes in millisecs
|
||||||
@type sleep_time: integer
|
@type sleep_time: integer
|
||||||
'''
|
'''
|
||||||
@ -48,7 +60,7 @@ class DeviceManager(Thread):
|
|||||||
self.job_manager = job_manager
|
self.job_manager = job_manager
|
||||||
self.current_job = None
|
self.current_job = None
|
||||||
self.scanner = DeviceScanner()
|
self.scanner = DeviceScanner()
|
||||||
|
|
||||||
def detect_device(self):
|
def detect_device(self):
|
||||||
self.scanner.scan()
|
self.scanner.scan()
|
||||||
for device in self.devices:
|
for device in self.devices:
|
||||||
@ -63,8 +75,8 @@ class DeviceManager(Thread):
|
|||||||
except:
|
except:
|
||||||
print 'Unable to open device'
|
print 'Unable to open device'
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
finally:
|
finally:
|
||||||
device[1] = True
|
device[1] = True
|
||||||
elif not connected and device[1]:
|
elif not connected and device[1]:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@ -75,14 +87,14 @@ class DeviceManager(Thread):
|
|||||||
self.device = None
|
self.device = None
|
||||||
self.connected_slot(False)
|
self.connected_slot(False)
|
||||||
device[1] ^= True
|
device[1] ^= True
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
if not self.jobs.empty():
|
if not self.jobs.empty():
|
||||||
try:
|
try:
|
||||||
return self.jobs.get_nowait()
|
return self.jobs.get_nowait()
|
||||||
except Queue.Empty:
|
except Queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self.keep_going:
|
while self.keep_going:
|
||||||
self.detect_device()
|
self.detect_device()
|
||||||
@ -94,75 +106,80 @@ class DeviceManager(Thread):
|
|||||||
self.current_job.run()
|
self.current_job.run()
|
||||||
self.current_job = None
|
self.current_job = None
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
time.sleep(self.sleep_time)
|
time.sleep(self.sleep_time)
|
||||||
|
|
||||||
def create_job(self, func, done, description, args=[], kwargs={}):
|
def create_job(self, func, done, description, args=[], kwargs={}):
|
||||||
job = DeviceJob(func, done, self.job_manager,
|
job = DeviceJob(func, done, self.job_manager,
|
||||||
args=args, kwargs=kwargs, description=description)
|
args=args, kwargs=kwargs, description=description)
|
||||||
self.job_manager.add_job(job)
|
self.job_manager.add_job(job)
|
||||||
self.jobs.put(job)
|
self.jobs.put(job)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
def has_card(self):
|
||||||
|
try:
|
||||||
|
return bool(self.device.card_prefix())
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def _get_device_information(self):
|
def _get_device_information(self):
|
||||||
info = self.device.get_device_information(end_session=False)
|
info = self.device.get_device_information(end_session=False)
|
||||||
info = [i.replace('\x00', '').replace('\x01', '') for i in info]
|
info = [i.replace('\x00', '').replace('\x01', '') for i in info]
|
||||||
cp = self.device.card_prefix(end_session=False)
|
cp = self.device.card_prefix(end_session=False)
|
||||||
fs = self.device.free_space()
|
fs = self.device.free_space()
|
||||||
return info, cp, fs
|
return info, cp, fs
|
||||||
|
|
||||||
def get_device_information(self, done):
|
def get_device_information(self, done):
|
||||||
'''Get device information and free space on device'''
|
'''Get device information and free space on device'''
|
||||||
return self.create_job(self._get_device_information, done,
|
return self.create_job(self._get_device_information, done,
|
||||||
description=_('Get device information'))
|
description=_('Get device information'))
|
||||||
|
|
||||||
|
|
||||||
def _books(self):
|
def _books(self):
|
||||||
'''Get metadata from device'''
|
'''Get metadata from device'''
|
||||||
mainlist = self.device.books(oncard=False, end_session=False)
|
mainlist = self.device.books(oncard=False, end_session=False)
|
||||||
cardlist = self.device.books(oncard=True)
|
cardlist = self.device.books(oncard=True)
|
||||||
return (mainlist, cardlist)
|
return (mainlist, cardlist)
|
||||||
|
|
||||||
def books(self, done):
|
def books(self, done):
|
||||||
'''Return callable that returns the list of books on device as two booklists'''
|
'''Return callable that returns the list of books on device as two booklists'''
|
||||||
return self.create_job(self._books, done, description=_('Get list of books on device'))
|
return self.create_job(self._books, done, description=_('Get list of books on device'))
|
||||||
|
|
||||||
def _sync_booklists(self, booklists):
|
def _sync_booklists(self, booklists):
|
||||||
'''Sync metadata to device'''
|
'''Sync metadata to device'''
|
||||||
self.device.sync_booklists(booklists, end_session=False)
|
self.device.sync_booklists(booklists, end_session=False)
|
||||||
return self.device.card_prefix(end_session=False), self.device.free_space()
|
return self.device.card_prefix(end_session=False), self.device.free_space()
|
||||||
|
|
||||||
def sync_booklists(self, done, booklists):
|
def sync_booklists(self, done, booklists):
|
||||||
return self.create_job(self._sync_booklists, done, args=[booklists],
|
return self.create_job(self._sync_booklists, done, args=[booklists],
|
||||||
description=_('Send metadata to device'))
|
description=_('Send metadata to device'))
|
||||||
|
|
||||||
def _upload_books(self, files, names, on_card=False, metadata=None):
|
def _upload_books(self, files, names, on_card=False, metadata=None):
|
||||||
'''Upload books to device: '''
|
'''Upload books to device: '''
|
||||||
return self.device.upload_books(files, names, on_card,
|
return self.device.upload_books(files, names, on_card,
|
||||||
metadata=metadata, end_session=False)
|
metadata=metadata, end_session=False)
|
||||||
|
|
||||||
def upload_books(self, done, files, names, on_card=False, titles=None,
|
def upload_books(self, done, files, names, on_card=False, titles=None,
|
||||||
metadata=None):
|
metadata=None):
|
||||||
desc = _('Upload %d books to device')%len(names)
|
desc = _('Upload %d books to device')%len(names)
|
||||||
if titles:
|
if titles:
|
||||||
desc += u':' + u', '.join(titles)
|
desc += u':' + u', '.join(titles)
|
||||||
return self.create_job(self._upload_books, done, args=[files, names],
|
return self.create_job(self._upload_books, done, args=[files, names],
|
||||||
kwargs={'on_card':on_card,'metadata':metadata}, description=desc)
|
kwargs={'on_card':on_card,'metadata':metadata}, description=desc)
|
||||||
|
|
||||||
def add_books_to_metadata(self, locations, metadata, booklists):
|
def add_books_to_metadata(self, locations, metadata, booklists):
|
||||||
self.device.add_books_to_metadata(locations, metadata, booklists)
|
self.device.add_books_to_metadata(locations, metadata, booklists)
|
||||||
|
|
||||||
def _delete_books(self, paths):
|
def _delete_books(self, paths):
|
||||||
'''Remove books from device'''
|
'''Remove books from device'''
|
||||||
self.device.delete_books(paths, end_session=True)
|
self.device.delete_books(paths, end_session=True)
|
||||||
|
|
||||||
def delete_books(self, done, paths):
|
def delete_books(self, done, paths):
|
||||||
return self.create_job(self._delete_books, done, args=[paths],
|
return self.create_job(self._delete_books, done, args=[paths],
|
||||||
description=_('Delete books from device'))
|
description=_('Delete books from device'))
|
||||||
|
|
||||||
def remove_books_from_metadata(self, paths, booklists):
|
def remove_books_from_metadata(self, paths, booklists):
|
||||||
self.device.remove_books_from_metadata(paths, booklists)
|
self.device.remove_books_from_metadata(paths, booklists)
|
||||||
|
|
||||||
def _save_books(self, paths, target):
|
def _save_books(self, paths, target):
|
||||||
'''Copy books from device to disk'''
|
'''Copy books from device to disk'''
|
||||||
for path in paths:
|
for path in paths:
|
||||||
@ -170,18 +187,469 @@ class DeviceManager(Thread):
|
|||||||
f = open(os.path.join(target, name), 'wb')
|
f = open(os.path.join(target, name), 'wb')
|
||||||
self.device.get_file(path, f)
|
self.device.get_file(path, f)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
def save_books(self, done, paths, target):
|
def save_books(self, done, paths, target):
|
||||||
return self.create_job(self._save_books, done, args=[paths, target],
|
return self.create_job(self._save_books, done, args=[paths, target],
|
||||||
description=_('Download books from device'))
|
description=_('Download books from device'))
|
||||||
|
|
||||||
def _view_book(self, path, target):
|
def _view_book(self, path, target):
|
||||||
f = open(target, 'wb')
|
f = open(target, 'wb')
|
||||||
self.device.get_file(path, f)
|
self.device.get_file(path, f)
|
||||||
f.close()
|
f.close()
|
||||||
return target
|
return target
|
||||||
|
|
||||||
def view_book(self, done, path, target):
|
def view_book(self, done, path, target):
|
||||||
return self.create_job(self._view_book, done, args=[path, target],
|
return self.create_job(self._view_book, done, args=[path, target],
|
||||||
description=_('View book on device'))
|
description=_('View book on device'))
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceAction(QAction):
|
||||||
|
|
||||||
|
def __init__(self, dest, delete, specific, icon_path, text, parent=None):
|
||||||
|
if delete:
|
||||||
|
text += ' ' + _('and delete from library')
|
||||||
|
QAction.__init__(self, QIcon(icon_path), text, parent)
|
||||||
|
self.dest = dest
|
||||||
|
self.delete = delete
|
||||||
|
self.specific = specific
|
||||||
|
self.connect(self, SIGNAL('triggered(bool)'),
|
||||||
|
lambda x : self.emit(SIGNAL('a_s(QAction)'), self))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
|
||||||
|
self.specific)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceMenu(QMenu):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QMenu.__init__(self, parent)
|
||||||
|
self.group = QActionGroup(self)
|
||||||
|
self.actions = []
|
||||||
|
self._memory = []
|
||||||
|
|
||||||
|
self.set_default_menu = self.addMenu(_('Set default send to device'
|
||||||
|
' action'))
|
||||||
|
opts = email_config().parse()
|
||||||
|
default_account = None
|
||||||
|
if opts.accounts:
|
||||||
|
self.email_to_menu = self.addMenu(_('Email to')+'...')
|
||||||
|
keys = sorted(opts.accounts.keys())
|
||||||
|
for account in keys:
|
||||||
|
formats, auto, default = opts.accounts[account]
|
||||||
|
dest = 'mail:'+account+';'+formats
|
||||||
|
if default:
|
||||||
|
default_account = (dest, False, False, ':/images/mail.svg',
|
||||||
|
_('Email to')+' '+account)
|
||||||
|
action1 = DeviceAction(dest, False, False, ':/images/mail.svg',
|
||||||
|
_('Email to')+' '+account, self)
|
||||||
|
action2 = DeviceAction(dest, True, False, ':/images/mail.svg',
|
||||||
|
_('Email to')+' '+account, self)
|
||||||
|
map(self.email_to_menu.addAction, (action1, action2))
|
||||||
|
map(self._memory.append, (action1, action2))
|
||||||
|
self.email_to_menu.addSeparator()
|
||||||
|
self.connect(action1, SIGNAL('a_s(QAction)'),
|
||||||
|
self.action_triggered)
|
||||||
|
self.connect(action2, SIGNAL('a_s(QAction)'),
|
||||||
|
self.action_triggered)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_actions = [
|
||||||
|
('main:', False, False, ':/images/reader.svg',
|
||||||
|
_('Send to main memory')),
|
||||||
|
('card:0', False, False, ':/images/sd.svg',
|
||||||
|
_('Send to storage card')),
|
||||||
|
'-----',
|
||||||
|
('main:', True, False, ':/images/reader.svg',
|
||||||
|
_('Send to main memory')),
|
||||||
|
('card:0', True, False, ':/images/sd.svg',
|
||||||
|
_('Send to storage card')),
|
||||||
|
'-----',
|
||||||
|
('main:', False, True, ':/images/reader.svg',
|
||||||
|
_('Send specific format to main memory')),
|
||||||
|
('card:0', False, True, ':/images/sd.svg',
|
||||||
|
_('Send specific format to storage card')),
|
||||||
|
|
||||||
|
]
|
||||||
|
if default_account is not None:
|
||||||
|
_actions.insert(2, default_account)
|
||||||
|
_actions.insert(6, list(default_account))
|
||||||
|
_actions[6][1] = True
|
||||||
|
for round in (0, 1):
|
||||||
|
for dest, delete, specific, icon, text in _actions:
|
||||||
|
if dest == '-':
|
||||||
|
(self.set_default_menu if round else self).addSeparator()
|
||||||
|
continue
|
||||||
|
action = DeviceAction(dest, delete, specific, icon, text, self)
|
||||||
|
self._memory.append(action)
|
||||||
|
if round == 1:
|
||||||
|
action.setCheckable(True)
|
||||||
|
action.setText(action.text())
|
||||||
|
self.group.addAction(action)
|
||||||
|
self.set_default_menu.addAction(action)
|
||||||
|
else:
|
||||||
|
self.connect(action, SIGNAL('a_s(QAction)'),
|
||||||
|
self.action_triggered)
|
||||||
|
self.actions.append(action)
|
||||||
|
self.addAction(action)
|
||||||
|
|
||||||
|
|
||||||
|
da = config['default_send_to_device_action']
|
||||||
|
done = False
|
||||||
|
for action in self.group.actions():
|
||||||
|
if repr(action) == da:
|
||||||
|
action.setChecked(True)
|
||||||
|
done = True
|
||||||
|
break
|
||||||
|
if not done:
|
||||||
|
action = list(self.group.actions())[0]
|
||||||
|
action.setChecked(True)
|
||||||
|
config['default_send_to_device_action'] = repr(action)
|
||||||
|
|
||||||
|
self.connect(self.group, SIGNAL('triggered(QAction*)'),
|
||||||
|
self.change_default_action)
|
||||||
|
self.enable_device_actions(False)
|
||||||
|
if opts.accounts:
|
||||||
|
self.addSeparator()
|
||||||
|
self.addMenu(self.email_to_menu)
|
||||||
|
|
||||||
|
def change_default_action(self, action):
|
||||||
|
config['default_send_to_device_action'] = repr(action)
|
||||||
|
action.setChecked(True)
|
||||||
|
|
||||||
|
def action_triggered(self, action):
|
||||||
|
self.emit(SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||||
|
action.dest, action.delete, action.specific)
|
||||||
|
|
||||||
|
def trigger_default(self, *args):
|
||||||
|
r = config['default_send_to_device_action']
|
||||||
|
for action in self.actions:
|
||||||
|
if repr(action) == r:
|
||||||
|
self.action_triggered(action)
|
||||||
|
break
|
||||||
|
|
||||||
|
def enable_device_actions(self, enable):
|
||||||
|
for action in self.actions:
|
||||||
|
if action.dest[:4] in ('main', 'card'):
|
||||||
|
action.setEnabled(enable)
|
||||||
|
|
||||||
|
class Emailer(Thread):
|
||||||
|
|
||||||
|
def __init__(self, timeout=10):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.setDaemon(True)
|
||||||
|
self.job_lock = RLock()
|
||||||
|
self.jobs = []
|
||||||
|
self._run = True
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self._run:
|
||||||
|
job = None
|
||||||
|
with self.job_lock:
|
||||||
|
if self.jobs:
|
||||||
|
job = self.jobs[0]
|
||||||
|
self.jobs = self.jobs[1:]
|
||||||
|
if job is not None:
|
||||||
|
self._send_mails(*job)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._run = False
|
||||||
|
|
||||||
|
def send_mails(self, jobnames, callback, attachments, to_s, subjects,
|
||||||
|
texts, attachment_names):
|
||||||
|
job = (jobnames, callback, attachments, to_s, subjects, texts,
|
||||||
|
attachment_names)
|
||||||
|
with self.job_lock:
|
||||||
|
self.jobs.append(job)
|
||||||
|
|
||||||
|
def _send_mails(self, jobnames, callback, attachments,
|
||||||
|
to_s, subjects, texts, attachment_names):
|
||||||
|
opts = email_config().parse()
|
||||||
|
from_ = opts.from_
|
||||||
|
if not from_:
|
||||||
|
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
|
||||||
|
results = []
|
||||||
|
for i, jobname in enumerate(jobnames):
|
||||||
|
try:
|
||||||
|
msg = compose_mail(from_, to_s[i], texts[i], subjects[i],
|
||||||
|
open(attachments[i], 'rb'),
|
||||||
|
attachment_name = attachment_names[i])
|
||||||
|
efrom, eto = map(extract_email_address, (from_, to_s[i]))
|
||||||
|
eto = [eto]
|
||||||
|
sendmail(msg, efrom, eto, localhost=None, verbose=0,
|
||||||
|
timeout=self.timeout, relay=opts.relay_host,
|
||||||
|
username=opts.relay_username,
|
||||||
|
password=opts.relay_password, port=opts.relay_port,
|
||||||
|
encryption=opts.encryption)
|
||||||
|
results.append([jobname, None, None])
|
||||||
|
except Exception, e:
|
||||||
|
results.append([jobname, e, traceback.format_exc()])
|
||||||
|
callback(results)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceGUI(object):
|
||||||
|
|
||||||
|
def dispatch_sync_event(self, dest, delete, specific):
|
||||||
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
error_dialog(self, _('No books'), _('No books')+' '+\
|
||||||
|
_('selected to send')).exec_()
|
||||||
|
return
|
||||||
|
|
||||||
|
fmt = None
|
||||||
|
if specific:
|
||||||
|
d = ChooseFormatDialog(self, _('Choose format to send to device'),
|
||||||
|
self.device_manager.device_class.FORMATS)
|
||||||
|
d.exec_()
|
||||||
|
fmt = d.format().lower()
|
||||||
|
dest, sub_dest = dest.split(':')
|
||||||
|
if dest in ('main', 'card'):
|
||||||
|
if not self.device_connected or not self.device_manager:
|
||||||
|
error_dialog(self, _('No device'),
|
||||||
|
_('Cannot send: No device is connected')).exec_()
|
||||||
|
return
|
||||||
|
on_card = dest == 'card'
|
||||||
|
if on_card and not self.device_manager.has_card():
|
||||||
|
error_dialog(self, _('No card'),
|
||||||
|
_('Cannot send: Device has no storage card')).exec_()
|
||||||
|
return
|
||||||
|
self.sync_to_device(on_card, delete, fmt)
|
||||||
|
elif dest == 'mail':
|
||||||
|
to, fmts = sub_dest.split(';')
|
||||||
|
fmts = [x.strip().lower() for x in fmts.split(',')]
|
||||||
|
self.send_by_mail(to, fmts, delete)
|
||||||
|
|
||||||
|
def send_by_mail(self, to, fmts, delete_from_library):
|
||||||
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
|
if not rows or len(rows) == 0:
|
||||||
|
return
|
||||||
|
ids = iter(self.library_view.model().id(r) for r in rows)
|
||||||
|
full_metadata = self.library_view.model().get_metadata(
|
||||||
|
rows, full_metadata=True)[-1]
|
||||||
|
files = self.library_view.model().get_preferred_formats(rows,
|
||||||
|
fmts, paths=True, set_metadata=True)
|
||||||
|
files = [getattr(f, 'name', None) for f in files]
|
||||||
|
|
||||||
|
bad, remove_ids, jobnames = [], [], []
|
||||||
|
texts, subjects, attachments, attachment_names = [], [], [], []
|
||||||
|
for f, mi, id in zip(files, full_metadata, ids):
|
||||||
|
t = mi.title
|
||||||
|
if not t:
|
||||||
|
t = _('Unknown')
|
||||||
|
if f is None:
|
||||||
|
bad.append(t)
|
||||||
|
else:
|
||||||
|
remove_ids.append(id)
|
||||||
|
jobnames.append(u'%s:%s'%(id, t))
|
||||||
|
attachments.append(f)
|
||||||
|
subjects.append(_('E-book:')+ ' '+t)
|
||||||
|
a = authors_to_string(mi.authors if mi.authors else \
|
||||||
|
[_('Unknown')])
|
||||||
|
texts.append(_('Attached, you will find the e-book') + \
|
||||||
|
'\n\n' + t + '\n\t' + _('by') + ' ' + a + '\n\n' + \
|
||||||
|
_('in the %s format.') %
|
||||||
|
os.path.splitext(f)[1][1:].upper())
|
||||||
|
prefix = sanitize_file_name(t+' - '+a)
|
||||||
|
if not isinstance(prefix, unicode):
|
||||||
|
prefix = prefix.decode(preferred_encoding, 'replace')
|
||||||
|
attachment_names.append(prefix + os.path.splitext(f)[1])
|
||||||
|
remove = remove_ids if delete_from_library else []
|
||||||
|
|
||||||
|
to_s = list(repeat(to, len(attachments)))
|
||||||
|
if attachments:
|
||||||
|
self.emailer.send_mails(jobnames,
|
||||||
|
Dispatcher(partial(self.emails_sent, remove=remove)),
|
||||||
|
attachments, to_s, subjects, texts, attachment_names)
|
||||||
|
self.status_bar.showMessage(_('Sending email to')+' '+to, 3000)
|
||||||
|
|
||||||
|
if bad:
|
||||||
|
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
|
||||||
|
d = warning_dialog(self, _('No suitable formats'),
|
||||||
|
'<p>'+ _('Could not email the following books '
|
||||||
|
'as no suitable formats were found:<br><ul>%s</ul>')%(bad,))
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def emails_sent(self, results, remove=[]):
|
||||||
|
errors, good = [], []
|
||||||
|
for jobname, exception, tb in results:
|
||||||
|
id = jobname.partition(':')[0]
|
||||||
|
title = jobname.partition(':')[-1]
|
||||||
|
if exception is not None:
|
||||||
|
errors.append([title, exception, tb])
|
||||||
|
else:
|
||||||
|
good.append(title)
|
||||||
|
if errors:
|
||||||
|
errors = '\n'.join([
|
||||||
|
'<li><b>%s</b><br>%s<br>%s<br></li>' %
|
||||||
|
(title, e, tb.replace('\n', '<br>')) for \
|
||||||
|
title, e, tb in errors
|
||||||
|
])
|
||||||
|
ConversionErrorDialog(self, _('Failed to email books'),
|
||||||
|
'<p>'+_('Failed to email the following books:')+\
|
||||||
|
'<ul>%s</ul>'%errors,
|
||||||
|
show=True)
|
||||||
|
else:
|
||||||
|
self.status_bar.showMessage(_('Sent by email:') + ', '.join(good),
|
||||||
|
5000)
|
||||||
|
|
||||||
|
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, Qt.SmoothTransformation)
|
||||||
|
return (p.width(), p.height(), pixmap_to_data(p))
|
||||||
|
|
||||||
|
def sync_news(self):
|
||||||
|
if self.device_connected:
|
||||||
|
ids = list(dynamic.get('news_to_be_synced', set([])))
|
||||||
|
ids = [id for id in ids if self.library_view.model().db.has_id(id)]
|
||||||
|
files = self.library_view.model().get_preferred_formats_from_ids(
|
||||||
|
ids, self.device_manager.device_class.FORMATS)
|
||||||
|
files = [f for f in files if f is not None]
|
||||||
|
if not files:
|
||||||
|
dynamic.set('news_to_be_synced', set([]))
|
||||||
|
return
|
||||||
|
metadata = self.library_view.model().get_metadata(ids,
|
||||||
|
rows_are_ids=True)
|
||||||
|
names = []
|
||||||
|
for mi in metadata:
|
||||||
|
prefix = sanitize_file_name(mi['title'])
|
||||||
|
if not isinstance(prefix, unicode):
|
||||||
|
prefix = prefix.decode(preferred_encoding, 'replace')
|
||||||
|
prefix = ascii_filename(prefix)
|
||||||
|
names.append('%s_%d%s'%(prefix, id,
|
||||||
|
os.path.splitext(f.name)[1]))
|
||||||
|
cdata = mi['cover']
|
||||||
|
if cdata:
|
||||||
|
mi['cover'] = self.cover_to_thumbnail(cdata)
|
||||||
|
dynamic.set('news_to_be_synced', set([]))
|
||||||
|
if config['upload_news_to_device'] and files:
|
||||||
|
remove = ids if \
|
||||||
|
config['delete_news_from_library_on_upload'] else []
|
||||||
|
on_card = self.location_view.model().free[0] < \
|
||||||
|
self.location_view.model().free[1]
|
||||||
|
self.upload_books(files, names, metadata,
|
||||||
|
on_card=on_card,
|
||||||
|
memory=[[f.name for f in files], remove])
|
||||||
|
self.status_bar.showMessage(_('Sending news to device.'), 5000)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_to_device(self, on_card, delete_from_library,
|
||||||
|
specific_format=None):
|
||||||
|
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,
|
||||||
|
paths=True, set_metadata=True,
|
||||||
|
specific_format=specific_format)
|
||||||
|
files = [getattr(f, 'name', None) for f in _files]
|
||||||
|
bad, good, gf, names, remove_ids = [], [], [], [], []
|
||||||
|
for f in files:
|
||||||
|
mi = metadata.next()
|
||||||
|
id = ids.next()
|
||||||
|
if f is None:
|
||||||
|
bad.append(mi['title'])
|
||||||
|
else:
|
||||||
|
remove_ids.append(id)
|
||||||
|
good.append(mi)
|
||||||
|
gf.append(f)
|
||||||
|
t = mi['title']
|
||||||
|
if not t:
|
||||||
|
t = _('Unknown')
|
||||||
|
a = mi['authors']
|
||||||
|
if not a:
|
||||||
|
a = _('Unknown')
|
||||||
|
prefix = sanitize_file_name(t+' - '+a)
|
||||||
|
if not isinstance(prefix, unicode):
|
||||||
|
prefix = prefix.decode(preferred_encoding, 'replace')
|
||||||
|
prefix = ascii_filename(prefix)
|
||||||
|
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
|
||||||
|
remove = remove_ids if delete_from_library else []
|
||||||
|
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
|
||||||
|
self.status_bar.showMessage(_('Sending books to device.'), 5000)
|
||||||
|
if bad:
|
||||||
|
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
|
||||||
|
d = warning_dialog(self, _('No suitable formats'),
|
||||||
|
_('Could not upload the following books to the device, '
|
||||||
|
'as no suitable formats were found:<br><ul>%s</ul>')%(bad,))
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def upload_booklists(self):
|
||||||
|
'''
|
||||||
|
Upload metadata to device.
|
||||||
|
'''
|
||||||
|
self.device_manager.sync_booklists(Dispatcher(self.metadata_synced),
|
||||||
|
self.booklists())
|
||||||
|
|
||||||
|
def metadata_synced(self, job):
|
||||||
|
'''
|
||||||
|
Called once metadata has been uploaded.
|
||||||
|
'''
|
||||||
|
if job.exception is not None:
|
||||||
|
self.device_job_exception(job)
|
||||||
|
return
|
||||||
|
cp, fs = job.result
|
||||||
|
self.location_view.model().update_devices(cp, fs)
|
||||||
|
|
||||||
|
def upload_books(self, files, names, metadata, on_card=False, memory=None):
|
||||||
|
'''
|
||||||
|
Upload books to device.
|
||||||
|
:param files: List of either paths to files or file like objects
|
||||||
|
'''
|
||||||
|
titles = [i['title'] for i in metadata]
|
||||||
|
job = self.device_manager.upload_books(
|
||||||
|
Dispatcher(self.books_uploaded),
|
||||||
|
files, names, on_card=on_card,
|
||||||
|
metadata=metadata, titles=titles
|
||||||
|
)
|
||||||
|
self.upload_memory[job] = (metadata, on_card, memory, files)
|
||||||
|
|
||||||
|
def books_uploaded(self, job):
|
||||||
|
'''
|
||||||
|
Called once books have been uploaded.
|
||||||
|
'''
|
||||||
|
metadata, on_card, memory, files = self.upload_memory.pop(job)
|
||||||
|
|
||||||
|
if job.exception is not None:
|
||||||
|
if isinstance(job.exception, FreeSpaceError):
|
||||||
|
where = 'in main memory.' if 'memory' in str(job.exception) \
|
||||||
|
else 'on the storage card.'
|
||||||
|
titles = '\n'.join(['<li>'+mi['title']+'</li>' \
|
||||||
|
for mi in metadata])
|
||||||
|
d = error_dialog(self, _('No space on device'),
|
||||||
|
_('<p>Cannot upload books to device there '
|
||||||
|
'is no more free space available ')+where+
|
||||||
|
'</p>\n<ul>%s</ul>'%(titles,))
|
||||||
|
d.exec_()
|
||||||
|
else:
|
||||||
|
self.device_job_exception(job)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.device_manager.add_books_to_metadata(job.result,
|
||||||
|
metadata, self.booklists())
|
||||||
|
|
||||||
|
self.upload_booklists()
|
||||||
|
|
||||||
|
view = self.card_view if on_card else self.memory_view
|
||||||
|
view.model().resort(reset=False)
|
||||||
|
view.model().research()
|
||||||
|
for f in files:
|
||||||
|
getattr(f, 'close', lambda : True)()
|
||||||
|
if memory and memory[1]:
|
||||||
|
self.library_view.model().delete_books_by_id(memory[1])
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
import os, re, time, textwrap
|
import os, re, time, textwrap
|
||||||
|
from binascii import hexlify, unhexlify
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \
|
from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \
|
||||||
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
||||||
QStringListModel, QAbstractItemModel, \
|
QStringListModel, QAbstractItemModel, QFont, \
|
||||||
SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
|
SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
|
||||||
QModelIndex, QInputDialog
|
QModelIndex, QInputDialog, QAbstractTableModel
|
||||||
|
|
||||||
from calibre.constants import islinux, iswindows
|
from calibre.constants import islinux, iswindows
|
||||||
from calibre.gui2.dialogs.config_ui import Ui_Dialog
|
from calibre.gui2.dialogs.config_ui import Ui_Dialog
|
||||||
@ -21,14 +22,15 @@ from calibre.library import server_config
|
|||||||
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
|
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
|
||||||
disable_plugin, customize_plugin, \
|
disable_plugin, customize_plugin, \
|
||||||
plugin_customization, add_plugin, remove_plugin
|
plugin_customization, add_plugin, remove_plugin
|
||||||
|
from calibre.utils.smtp import config as smtp_prefs
|
||||||
|
|
||||||
class PluginModel(QAbstractItemModel):
|
class PluginModel(QAbstractItemModel):
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QAbstractItemModel.__init__(self, *args)
|
QAbstractItemModel.__init__(self, *args)
|
||||||
self.icon = QVariant(QIcon(':/images/plugins.svg'))
|
self.icon = QVariant(QIcon(':/images/plugins.svg'))
|
||||||
self.populate()
|
self.populate()
|
||||||
|
|
||||||
def populate(self):
|
def populate(self):
|
||||||
self._data = {}
|
self._data = {}
|
||||||
for plugin in initialized_plugins():
|
for plugin in initialized_plugins():
|
||||||
@ -37,21 +39,21 @@ class PluginModel(QAbstractItemModel):
|
|||||||
else:
|
else:
|
||||||
self._data[plugin.type].append(plugin)
|
self._data[plugin.type].append(plugin)
|
||||||
self.categories = sorted(self._data.keys())
|
self.categories = sorted(self._data.keys())
|
||||||
|
|
||||||
def index(self, row, column, parent):
|
def index(self, row, column, parent):
|
||||||
if not self.hasIndex(row, column, parent):
|
if not self.hasIndex(row, column, parent):
|
||||||
return QModelIndex()
|
return QModelIndex()
|
||||||
|
|
||||||
if parent.isValid():
|
if parent.isValid():
|
||||||
return self.createIndex(row, column, parent.row())
|
return self.createIndex(row, column, parent.row())
|
||||||
else:
|
else:
|
||||||
return self.createIndex(row, column, -1)
|
return self.createIndex(row, column, -1)
|
||||||
|
|
||||||
def parent(self, index):
|
def parent(self, index):
|
||||||
if not index.isValid() or index.internalId() == -1:
|
if not index.isValid() or index.internalId() == -1:
|
||||||
return QModelIndex()
|
return QModelIndex()
|
||||||
return self.createIndex(index.internalId(), 0, -1)
|
return self.createIndex(index.internalId(), 0, -1)
|
||||||
|
|
||||||
def rowCount(self, parent):
|
def rowCount(self, parent):
|
||||||
if not parent.isValid():
|
if not parent.isValid():
|
||||||
return len(self.categories)
|
return len(self.categories)
|
||||||
@ -59,14 +61,14 @@ class PluginModel(QAbstractItemModel):
|
|||||||
category = self.categories[parent.row()]
|
category = self.categories[parent.row()]
|
||||||
return len(self._data[category])
|
return len(self._data[category])
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def index_to_plugin(self, index):
|
def index_to_plugin(self, index):
|
||||||
category = self.categories[index.parent().row()]
|
category = self.categories[index.parent().row()]
|
||||||
return self._data[category][index.row()]
|
return self._data[category][index.row()]
|
||||||
|
|
||||||
def plugin_to_index(self, plugin):
|
def plugin_to_index(self, plugin):
|
||||||
for i, category in enumerate(self.categories):
|
for i, category in enumerate(self.categories):
|
||||||
parent = self.index(i, 0, QModelIndex())
|
parent = self.index(i, 0, QModelIndex())
|
||||||
@ -74,13 +76,13 @@ class PluginModel(QAbstractItemModel):
|
|||||||
if plugin == p:
|
if plugin == p:
|
||||||
return self.index(j, 0, parent)
|
return self.index(j, 0, parent)
|
||||||
return QModelIndex()
|
return QModelIndex()
|
||||||
|
|
||||||
def refresh_plugin(self, plugin, rescan=False):
|
def refresh_plugin(self, plugin, rescan=False):
|
||||||
if rescan:
|
if rescan:
|
||||||
self.populate()
|
self.populate()
|
||||||
idx = self.plugin_to_index(plugin)
|
idx = self.plugin_to_index(plugin)
|
||||||
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), idx, idx)
|
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), idx, idx)
|
||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return 0
|
return 0
|
||||||
@ -90,7 +92,7 @@ class PluginModel(QAbstractItemModel):
|
|||||||
if not is_disabled(self.data(index, Qt.UserRole)):
|
if not is_disabled(self.data(index, Qt.UserRole)):
|
||||||
flags |= Qt.ItemIsEnabled
|
flags |= Qt.ItemIsEnabled
|
||||||
return flags
|
return flags
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return NONE
|
return NONE
|
||||||
@ -113,25 +115,140 @@ class PluginModel(QAbstractItemModel):
|
|||||||
if role == Qt.UserRole:
|
if role == Qt.UserRole:
|
||||||
return plugin
|
return plugin
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryModel(QStringListModel):
|
class CategoryModel(QStringListModel):
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QStringListModel.__init__(self, *args)
|
QStringListModel.__init__(self, *args)
|
||||||
self.setStringList([_('General'), _('Interface'), _('Advanced'),
|
self.setStringList([_('General'), _('Interface'), _('Email\nDelivery'),
|
||||||
_('Content\nServer'), _('Plugins')])
|
_('Advanced'), _('Content\nServer'), _('Plugins')])
|
||||||
self.icons = list(map(QVariant, map(QIcon,
|
self.icons = list(map(QVariant, map(QIcon,
|
||||||
[':/images/dialog_information.svg', ':/images/lookfeel.svg',
|
[':/images/dialog_information.svg', ':/images/lookfeel.svg',
|
||||||
':/images/view.svg', ':/images/network-server.svg',
|
':/images/mail.svg', ':/images/view.svg',
|
||||||
':/images/plugins.svg'])))
|
':/images/network-server.svg', ':/images/plugins.svg'])))
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
if role == Qt.DecorationRole:
|
if role == Qt.DecorationRole:
|
||||||
return self.icons[index.row()]
|
return self.icons[index.row()]
|
||||||
return QStringListModel.data(self, index, role)
|
return QStringListModel.data(self, index, role)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailAccounts(QAbstractTableModel):
|
||||||
|
|
||||||
|
def __init__(self, accounts):
|
||||||
|
QAbstractTableModel.__init__(self)
|
||||||
|
self.accounts = accounts
|
||||||
|
self.account_order = sorted(self.accounts.keys())
|
||||||
|
self.headers = map(QVariant, [_('Email'), _('Formats'), _('Auto send')])
|
||||||
|
self.default_font = QFont()
|
||||||
|
self.default_font.setBold(True)
|
||||||
|
self.default_font = QVariant(self.default_font)
|
||||||
|
self.tooltips =[NONE] + map(QVariant,
|
||||||
|
[_('Formats to email. The first matching format will be sent.'),
|
||||||
|
'<p>'+_('If checked, downloaded news will be automatically '
|
||||||
|
'mailed <br>to this email address '
|
||||||
|
'(provided it is in one of the listed formats).')])
|
||||||
|
|
||||||
|
def rowCount(self, *args):
|
||||||
|
return len(self.account_order)
|
||||||
|
|
||||||
|
def columnCount(self, *args):
|
||||||
|
return 3
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role):
|
||||||
|
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||||
|
return self.headers[section]
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
if row < 0 or row >= self.rowCount():
|
||||||
|
return NONE
|
||||||
|
account = self.account_order[row]
|
||||||
|
if role == Qt.UserRole:
|
||||||
|
return (account, self.accounts[account])
|
||||||
|
if role == Qt.ToolTipRole:
|
||||||
|
return self.tooltips[col]
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
if col == 0:
|
||||||
|
return QVariant(account)
|
||||||
|
if col == 1:
|
||||||
|
return QVariant(self.accounts[account][0])
|
||||||
|
if role == Qt.FontRole and self.accounts[account][2]:
|
||||||
|
return self.default_font
|
||||||
|
if role == Qt.CheckStateRole and col == 2:
|
||||||
|
return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked)
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def flags(self, index):
|
||||||
|
if index.column() == 2:
|
||||||
|
return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable
|
||||||
|
else:
|
||||||
|
return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable
|
||||||
|
|
||||||
|
def setData(self, index, value, role):
|
||||||
|
if not index.isValid():
|
||||||
|
return False
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
account = self.account_order[row]
|
||||||
|
if col == 2:
|
||||||
|
self.accounts[account][1] ^= True
|
||||||
|
elif col == 1:
|
||||||
|
self.accounts[account][0] = unicode(value.toString()).upper()
|
||||||
|
else:
|
||||||
|
na = unicode(value.toString())
|
||||||
|
from email.utils import parseaddr
|
||||||
|
addr = parseaddr(na)[-1]
|
||||||
|
if not addr:
|
||||||
|
return False
|
||||||
|
self.accounts[na] = self.accounts.pop(account)
|
||||||
|
self.account_order[row] = na
|
||||||
|
if '@kindle.com' in addr:
|
||||||
|
self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1'
|
||||||
|
|
||||||
|
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
|
||||||
|
self.index(index.row(), 0), self.index(index.row(), 2))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def make_default(self, index):
|
||||||
|
if index.isValid():
|
||||||
|
row = index.row()
|
||||||
|
for x in self.accounts.values():
|
||||||
|
x[2] = False
|
||||||
|
self.accounts[self.account_order[row]][2] = True
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def add(self):
|
||||||
|
x = _('new email address')
|
||||||
|
y = x
|
||||||
|
c = 0
|
||||||
|
while y in self.accounts:
|
||||||
|
c += 1
|
||||||
|
y = x + str(c)
|
||||||
|
self.accounts[y] = ['MOBI, EPUB', True,
|
||||||
|
len(self.account_order) == 0]
|
||||||
|
self.account_order = sorted(self.accounts.keys())
|
||||||
|
self.reset()
|
||||||
|
return self.index(self.account_order.index(y), 0)
|
||||||
|
|
||||||
|
def remove(self, index):
|
||||||
|
if index.isValid():
|
||||||
|
row = self.index.row()
|
||||||
|
account = self.account_order[row]
|
||||||
|
self.accounts.pop(account)
|
||||||
|
self.account_order = sorted(self.accounts.keys())
|
||||||
|
has_default = False
|
||||||
|
for account in self.account_order:
|
||||||
|
if self.accounts[account][2]:
|
||||||
|
has_default = True
|
||||||
|
break
|
||||||
|
if not has_default and self.account_order:
|
||||||
|
self.accounts[self.account_order[0]][2] = True
|
||||||
|
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
|
||||||
class ConfigDialog(QDialog, Ui_Dialog):
|
class ConfigDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
@ -141,9 +258,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
|
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self._category_model = CategoryModel()
|
self._category_model = CategoryModel()
|
||||||
|
|
||||||
self.connect(self.category_view, SIGNAL('activated(QModelIndex)'), lambda i: self.stackedWidget.setCurrentIndex(i.row()))
|
self.category_view.currentChanged = \
|
||||||
self.connect(self.category_view, SIGNAL('clicked(QModelIndex)'), lambda i: self.stackedWidget.setCurrentIndex(i.row()))
|
lambda n, p: self.stackedWidget.setCurrentIndex(n.row())
|
||||||
self.category_view.setModel(self._category_model)
|
self.category_view.setModel(self._category_model)
|
||||||
self.db = db
|
self.db = db
|
||||||
self.server = server
|
self.server = server
|
||||||
@ -151,7 +268,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.location.setText(path if path else '')
|
self.location.setText(path if path else '')
|
||||||
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
|
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
|
||||||
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
|
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
|
||||||
|
|
||||||
dirs = config['frequently_used_directories']
|
dirs = config['frequently_used_directories']
|
||||||
rn = config['use_roman_numerals_for_series_number']
|
rn = config['use_roman_numerals_for_series_number']
|
||||||
self.timeout.setValue(prefs['network_timeout'])
|
self.timeout.setValue(prefs['network_timeout'])
|
||||||
@ -162,20 +279,20 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.connect(self.remove_button, SIGNAL('clicked(bool)'), self.remove_dir)
|
self.connect(self.remove_button, SIGNAL('clicked(bool)'), self.remove_dir)
|
||||||
if not islinux:
|
if not islinux:
|
||||||
self.dirs_box.setVisible(False)
|
self.dirs_box.setVisible(False)
|
||||||
|
|
||||||
column_map = config['column_map']
|
column_map = config['column_map']
|
||||||
for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]:
|
for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]:
|
||||||
item = QListWidgetItem(BooksModel.headers[col], self.columns)
|
item = QListWidgetItem(BooksModel.headers[col], self.columns)
|
||||||
item.setData(Qt.UserRole, QVariant(col))
|
item.setData(Qt.UserRole, QVariant(col))
|
||||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
||||||
item.setCheckState(Qt.Checked if col in column_map else Qt.Unchecked)
|
item.setCheckState(Qt.Checked if col in column_map else Qt.Unchecked)
|
||||||
|
|
||||||
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
|
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
|
||||||
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
|
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
|
||||||
|
|
||||||
self.filename_pattern = FilenamePattern(self)
|
self.filename_pattern = FilenamePattern(self)
|
||||||
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
|
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
|
||||||
|
|
||||||
icons = config['toolbar_icon_size']
|
icons = config['toolbar_icon_size']
|
||||||
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
|
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
|
||||||
self.show_toolbar_text.setChecked(config['show_text_in_toolbar'])
|
self.show_toolbar_text.setChecked(config['show_text_in_toolbar'])
|
||||||
@ -183,7 +300,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.book_exts = sorted(BOOK_EXTENSIONS)
|
self.book_exts = sorted(BOOK_EXTENSIONS)
|
||||||
for ext in self.book_exts:
|
for ext in self.book_exts:
|
||||||
self.single_format.addItem(ext.upper(), QVariant(ext))
|
self.single_format.addItem(ext.upper(), QVariant(ext))
|
||||||
|
|
||||||
single_format = config['save_to_disk_single_format']
|
single_format = config['save_to_disk_single_format']
|
||||||
self.single_format.setCurrentIndex(self.book_exts.index(single_format))
|
self.single_format.setCurrentIndex(self.book_exts.index(single_format))
|
||||||
self.cover_browse.setValue(config['cover_flow_queue_length'])
|
self.cover_browse.setValue(config['cover_flow_queue_length'])
|
||||||
@ -204,9 +321,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
items.sort(cmp=lambda x, y: cmp(x[1], y[1]))
|
items.sort(cmp=lambda x, y: cmp(x[1], y[1]))
|
||||||
for item in items:
|
for item in items:
|
||||||
self.language.addItem(item[1], QVariant(item[0]))
|
self.language.addItem(item[1], QVariant(item[0]))
|
||||||
|
|
||||||
self.pdf_metadata.setChecked(prefs['read_file_metadata'])
|
self.pdf_metadata.setChecked(prefs['read_file_metadata'])
|
||||||
|
|
||||||
added_html = False
|
added_html = False
|
||||||
for ext in self.book_exts:
|
for ext in self.book_exts:
|
||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
@ -242,7 +359,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.priority.setCurrentIndex(p)
|
self.priority.setCurrentIndex(p)
|
||||||
self.priority.setVisible(iswindows)
|
self.priority.setVisible(iswindows)
|
||||||
self.priority_label.setVisible(iswindows)
|
self.priority_label.setVisible(iswindows)
|
||||||
self.category_view.setCurrentIndex(self._category_model.index(0))
|
|
||||||
self._plugin_model = PluginModel()
|
self._plugin_model = PluginModel()
|
||||||
self.plugin_view.setModel(self._plugin_model)
|
self.plugin_view.setModel(self._plugin_model)
|
||||||
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
|
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
|
||||||
@ -251,7 +367,75 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
|
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
|
||||||
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
|
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
|
||||||
self.separate_cover_flow.setChecked(config['separate_cover_flow'])
|
self.separate_cover_flow.setChecked(config['separate_cover_flow'])
|
||||||
|
self.setup_email_page()
|
||||||
|
self.category_view.setCurrentIndex(self.category_view.model().index(0))
|
||||||
|
|
||||||
|
def setup_email_page(self):
|
||||||
|
opts = smtp_prefs().parse()
|
||||||
|
if opts.from_:
|
||||||
|
self.email_from.setText(opts.from_)
|
||||||
|
self._email_accounts = EmailAccounts(opts.accounts)
|
||||||
|
self.email_view.setModel(self._email_accounts)
|
||||||
|
if opts.relay_host:
|
||||||
|
self.relay_host.setText(opts.relay_host)
|
||||||
|
self.relay_port.setValue(opts.relay_port)
|
||||||
|
if opts.relay_username:
|
||||||
|
self.relay_username.setText(opts.relay_username)
|
||||||
|
if opts.relay_password:
|
||||||
|
self.relay_password.setText(unhexlify(opts.relay_password))
|
||||||
|
(self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True)
|
||||||
|
self.connect(self.relay_use_gmail, SIGNAL('clicked(bool)'),
|
||||||
|
self.create_gmail_relay)
|
||||||
|
self.connect(self.relay_show_password, SIGNAL('stateChanged(int)'),
|
||||||
|
lambda state:self.relay_password.setEchoMode(self.relay_password.Password))
|
||||||
|
self.connect(self.email_add, SIGNAL('clicked(bool)'),
|
||||||
|
self.add_email_account)
|
||||||
|
self.connect(self.email_make_default, SIGNAL('clicked(bool)'),
|
||||||
|
lambda c: self._email_accounts.make_default(self.email_view.currentIndex()))
|
||||||
|
self.email_view.resizeColumnsToContents()
|
||||||
|
|
||||||
|
def add_email_account(self, checked):
|
||||||
|
index = self._email_accounts.add()
|
||||||
|
self.email_view.setCurrentIndex(index)
|
||||||
|
self.email_view.resizeColumnsToContents()
|
||||||
|
self.email_view.edit(index)
|
||||||
|
|
||||||
|
def create_gmail_relay(self, *args):
|
||||||
|
self.relay_username.setText('@gmail.com')
|
||||||
|
self.relay_password.setText('')
|
||||||
|
self.relay_host.setText('smtp.gmail.com')
|
||||||
|
self.relay_port.setValue(587)
|
||||||
|
self.relay_tls.setChecked(True)
|
||||||
|
|
||||||
|
info_dialog(self, _('Finish gmail setup'),
|
||||||
|
_('Dont forget to enter your gmail username and password')).exec_()
|
||||||
|
self.relay_username.setFocus(Qt.OtherFocusReason)
|
||||||
|
self.relay_username.setCursorPosition(0)
|
||||||
|
|
||||||
|
def set_email_settings(self):
|
||||||
|
from_ = unicode(self.email_from.text()).strip()
|
||||||
|
if self._email_accounts.accounts and not from_:
|
||||||
|
error_dialog(self, _('Bad configuration'),
|
||||||
|
_('You must set the From email address')).exec_()
|
||||||
|
return False
|
||||||
|
username = unicode(self.relay_username.text()).strip()
|
||||||
|
password = unicode(self.relay_password.text()).strip()
|
||||||
|
host = unicode(self.relay_host.text()).strip()
|
||||||
|
if host and not (username and password):
|
||||||
|
error_dialog(self, _('Bad configuration'),
|
||||||
|
_('You must set the username and password for '
|
||||||
|
'the mail server.')).exec_()
|
||||||
|
return False
|
||||||
|
conf = smtp_prefs()
|
||||||
|
conf.set('from_', from_)
|
||||||
|
conf.set('accounts', self._email_accounts.accounts)
|
||||||
|
conf.set('relay_host', host if host else None)
|
||||||
|
conf.set('relay_port', self.relay_port.value())
|
||||||
|
conf.set('relay_username', username if username else None)
|
||||||
|
conf.set('relay_password', hexlify(password))
|
||||||
|
conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL')
|
||||||
|
return True
|
||||||
|
|
||||||
def add_plugin(self):
|
def add_plugin(self):
|
||||||
path = unicode(self.plugin_path.text())
|
path = unicode(self.plugin_path.text())
|
||||||
if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'):
|
if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'):
|
||||||
@ -259,22 +443,22 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self._plugin_model.populate()
|
self._plugin_model.populate()
|
||||||
self._plugin_model.reset()
|
self._plugin_model.reset()
|
||||||
else:
|
else:
|
||||||
error_dialog(self, _('No valid plugin path'),
|
error_dialog(self, _('No valid plugin path'),
|
||||||
_('%s is not a valid plugin path')%path).exec_()
|
_('%s is not a valid plugin path')%path).exec_()
|
||||||
|
|
||||||
def find_plugin(self):
|
def find_plugin(self):
|
||||||
path = choose_files(self, 'choose plugin dialog', _('Choose plugin'),
|
path = choose_files(self, 'choose plugin dialog', _('Choose plugin'),
|
||||||
filters=[('Plugins', ['zip'])], all_files=False,
|
filters=[('Plugins', ['zip'])], all_files=False,
|
||||||
select_only_single_file=True)
|
select_only_single_file=True)
|
||||||
if path:
|
if path:
|
||||||
self.plugin_path.setText(path[0])
|
self.plugin_path.setText(path[0])
|
||||||
|
|
||||||
def modify_plugin(self, op=''):
|
def modify_plugin(self, op=''):
|
||||||
index = self.plugin_view.currentIndex()
|
index = self.plugin_view.currentIndex()
|
||||||
if index.isValid():
|
if index.isValid():
|
||||||
plugin = self._plugin_model.index_to_plugin(index)
|
plugin = self._plugin_model.index_to_plugin(index)
|
||||||
if not plugin.can_be_disabled:
|
if not plugin.can_be_disabled:
|
||||||
error_dialog(self,_('Plugin cannot be disabled'),
|
error_dialog(self,_('Plugin cannot be disabled'),
|
||||||
_('The plugin: %s cannot be disabled')%plugin.name).exec_()
|
_('The plugin: %s cannot be disabled')%plugin.name).exec_()
|
||||||
return
|
return
|
||||||
if op == 'toggle':
|
if op == 'toggle':
|
||||||
@ -286,7 +470,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
if op == 'customize':
|
if op == 'customize':
|
||||||
if not plugin.is_customizable():
|
if not plugin.is_customizable():
|
||||||
info_dialog(self, _('Plugin not customizable'),
|
info_dialog(self, _('Plugin not customizable'),
|
||||||
_('Plugin: %s does not need customization')%plugin.name).exec_()
|
_('Plugin: %s does not need customization')%plugin.name).exec_()
|
||||||
return
|
return
|
||||||
help = plugin.customization_help()
|
help = plugin.customization_help()
|
||||||
text, ok = QInputDialog.getText(self, _('Customize %s')%plugin.name,
|
text, ok = QInputDialog.getText(self, _('Customize %s')%plugin.name,
|
||||||
@ -299,22 +483,23 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self._plugin_model.populate()
|
self._plugin_model.populate()
|
||||||
self._plugin_model.reset()
|
self._plugin_model.reset()
|
||||||
else:
|
else:
|
||||||
error_dialog(self, _('Cannot remove builtin plugin'),
|
error_dialog(self, _('Cannot remove builtin plugin'),
|
||||||
plugin.name + _(' cannot be removed. It is a builtin plugin. Try disabling it instead.')).exec_()
|
plugin.name + _(' cannot be removed. It is a '
|
||||||
|
'builtin plugin. Try disabling it instead.')).exec_()
|
||||||
|
|
||||||
|
|
||||||
def up_column(self):
|
def up_column(self):
|
||||||
idx = self.columns.currentRow()
|
idx = self.columns.currentRow()
|
||||||
if idx > 0:
|
if idx > 0:
|
||||||
self.columns.insertItem(idx-1, self.columns.takeItem(idx))
|
self.columns.insertItem(idx-1, self.columns.takeItem(idx))
|
||||||
self.columns.setCurrentRow(idx-1)
|
self.columns.setCurrentRow(idx-1)
|
||||||
|
|
||||||
def down_column(self):
|
def down_column(self):
|
||||||
idx = self.columns.currentRow()
|
idx = self.columns.currentRow()
|
||||||
if idx < self.columns.count()-1:
|
if idx < self.columns.count()-1:
|
||||||
self.columns.insertItem(idx+1, self.columns.takeItem(idx))
|
self.columns.insertItem(idx+1, self.columns.takeItem(idx))
|
||||||
self.columns.setCurrentRow(idx+1)
|
self.columns.setCurrentRow(idx+1)
|
||||||
|
|
||||||
def view_server_logs(self):
|
def view_server_logs(self):
|
||||||
from calibre.library.server import log_access_file, log_error_file
|
from calibre.library.server import log_access_file, log_error_file
|
||||||
d = QDialog(self)
|
d = QDialog(self)
|
||||||
@ -336,7 +521,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
except IOError:
|
except IOError:
|
||||||
el.setPlainText('No access log found')
|
el.setPlainText('No access log found')
|
||||||
d.show()
|
d.show()
|
||||||
|
|
||||||
def set_server_options(self):
|
def set_server_options(self):
|
||||||
c = server_config()
|
c = server_config()
|
||||||
c.set('port', self.port.value())
|
c.set('port', self.port.value())
|
||||||
@ -345,7 +530,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
if not p:
|
if not p:
|
||||||
p = None
|
p = None
|
||||||
c.set('password', p)
|
c.set('password', p)
|
||||||
|
|
||||||
def start_server(self):
|
def start_server(self):
|
||||||
self.set_server_options()
|
self.set_server_options()
|
||||||
from calibre.library.server import start_threaded_server
|
from calibre.library.server import start_threaded_server
|
||||||
@ -353,13 +538,13 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
while not self.server.is_running and self.server.exception is None:
|
while not self.server.is_running and self.server.exception is None:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
if self.server.exception is not None:
|
if self.server.exception is not None:
|
||||||
error_dialog(self, _('Failed to start content server'),
|
error_dialog(self, _('Failed to start content server'),
|
||||||
unicode(self.server.exception)).exec_()
|
unicode(self.server.exception)).exec_()
|
||||||
return
|
return
|
||||||
self.start.setEnabled(False)
|
self.start.setEnabled(False)
|
||||||
self.test.setEnabled(True)
|
self.test.setEnabled(True)
|
||||||
self.stop.setEnabled(True)
|
self.stop.setEnabled(True)
|
||||||
|
|
||||||
def stop_server(self):
|
def stop_server(self):
|
||||||
from calibre.library.server import stop_threaded_server
|
from calibre.library.server import stop_threaded_server
|
||||||
stop_threaded_server(self.server)
|
stop_threaded_server(self.server)
|
||||||
@ -367,16 +552,17 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.start.setEnabled(True)
|
self.start.setEnabled(True)
|
||||||
self.test.setEnabled(False)
|
self.test.setEnabled(False)
|
||||||
self.stop.setEnabled(False)
|
self.stop.setEnabled(False)
|
||||||
|
|
||||||
def test_server(self):
|
def test_server(self):
|
||||||
QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value())))
|
QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value())))
|
||||||
|
|
||||||
def compact(self, toggled):
|
def compact(self, toggled):
|
||||||
d = Vacuum(self, self.db)
|
d = Vacuum(self, self.db)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def browse(self):
|
def browse(self):
|
||||||
dir = choose_dir(self, 'database location dialog', 'Select database location')
|
dir = choose_dir(self, 'database location dialog',
|
||||||
|
_('Select database location'))
|
||||||
if dir:
|
if dir:
|
||||||
self.location.setText(dir)
|
self.location.setText(dir)
|
||||||
|
|
||||||
@ -393,7 +579,10 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
def accept(self):
|
def accept(self):
|
||||||
mcs = unicode(self.max_cover_size.text()).strip()
|
mcs = unicode(self.max_cover_size.text()).strip()
|
||||||
if not re.match(r'\d+x\d+', mcs):
|
if not re.match(r'\d+x\d+', mcs):
|
||||||
error_dialog(self, _('Invalid size'), _('The size %s is invalid. must be of the form widthxheight')%mcs).exec_()
|
error_dialog(self, _('Invalid size'),
|
||||||
|
_('The size %s is invalid. must be of the form widthxheight')%mcs).exec_()
|
||||||
|
return
|
||||||
|
if not self.set_email_settings():
|
||||||
return
|
return
|
||||||
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
|
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
|
||||||
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
|
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
|
||||||
@ -429,18 +618,21 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
if self.viewer.item(i).checkState() == Qt.Checked:
|
if self.viewer.item(i).checkState() == Qt.Checked:
|
||||||
fmts.append(str(self.viewer.item(i).text()))
|
fmts.append(str(self.viewer.item(i).text()))
|
||||||
config['internally_viewed_formats'] = fmts
|
config['internally_viewed_formats'] = fmts
|
||||||
|
|
||||||
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
||||||
d = error_dialog(self, _('Invalid database location'),
|
d = error_dialog(self, _('Invalid database location'),
|
||||||
_('Invalid database location ')+path+_('<br>Must be a directory.'))
|
_('Invalid database location ')+path+
|
||||||
|
_('<br>Must be a directory.'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
elif not os.access(path, os.W_OK):
|
elif not os.access(path, os.W_OK):
|
||||||
d = error_dialog(self, _('Invalid database location'),
|
d = error_dialog(self, _('Invalid database location'),
|
||||||
_('Invalid database location.<br>Cannot write to ')+path)
|
_('Invalid database location.<br>Cannot write to ')+path)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
else:
|
else:
|
||||||
self.database_location = os.path.abspath(path)
|
self.database_location = os.path.abspath(path)
|
||||||
self.directories = [qstring_to_unicode(self.directory_list.item(i).text()) for i in range(self.directory_list.count())]
|
self.directories = [
|
||||||
|
qstring_to_unicode(self.directory_list.item(i).text()) for i in \
|
||||||
|
range(self.directory_list.count())]
|
||||||
config['frequently_used_directories'] = self.directories
|
config['frequently_used_directories'] = self.directories
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
@ -448,7 +640,8 @@ class Vacuum(QMessageBox):
|
|||||||
|
|
||||||
def __init__(self, parent, db):
|
def __init__(self, parent, db):
|
||||||
self.db = db
|
self.db = db
|
||||||
QMessageBox.__init__(self, QMessageBox.Information, _('Compacting...'), _('Compacting database. This may take a while.'),
|
QMessageBox.__init__(self, QMessageBox.Information, _('Compacting...'),
|
||||||
|
_('Compacting database. This may take a while.'),
|
||||||
QMessageBox.NoButton, parent)
|
QMessageBox.NoButton, parent)
|
||||||
QTimer.singleShot(200, self.vacuum)
|
QTimer.singleShot(200, self.vacuum)
|
||||||
|
|
||||||
@ -456,3 +649,11 @@ class Vacuum(QMessageBox):
|
|||||||
self.db.vacuum()
|
self.db.vacuum()
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
|
from PyQt4.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
d=ConfigDialog(None, LibraryDatabase2('/tmp'))
|
||||||
|
d.category_view.setCurrentIndex(d.category_view.model().index(2))
|
||||||
|
d.show()
|
||||||
|
app.exec_()
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>755</width>
|
<width>789</width>
|
||||||
<height>557</height>
|
<height>557</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -437,12 +437,6 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
<zorder>toolbar_button_size</zorder>
|
|
||||||
<zorder>label_4</zorder>
|
|
||||||
<zorder>show_toolbar_text</zorder>
|
|
||||||
<zorder>columns</zorder>
|
|
||||||
<zorder></zorder>
|
|
||||||
<zorder>groupBox_3</zorder>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@ -507,7 +501,6 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
<zorder>columns</zorder>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@ -534,16 +527,287 @@
|
|||||||
</layout>
|
</layout>
|
||||||
<zorder>roman_numerals</zorder>
|
<zorder>roman_numerals</zorder>
|
||||||
<zorder>groupBox_2</zorder>
|
<zorder>groupBox_2</zorder>
|
||||||
<zorder>groupBox</zorder>
|
|
||||||
<zorder>systray_icon</zorder>
|
<zorder>systray_icon</zorder>
|
||||||
<zorder>sync_news</zorder>
|
<zorder>sync_news</zorder>
|
||||||
<zorder>delete_news</zorder>
|
<zorder>delete_news</zorder>
|
||||||
<zorder>separate_cover_flow</zorder>
|
<zorder>separate_cover_flow</zorder>
|
||||||
<zorder>systray_notifications</zorder>
|
<zorder>systray_notifications</zorder>
|
||||||
<zorder>groupBox_3</zorder>
|
|
||||||
<zorder></zorder>
|
<zorder></zorder>
|
||||||
<zorder></zorder>
|
<zorder></zorder>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QWidget" name="page_6" >
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_9" >
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_22" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>calibre can send your books to you (or your reader) by email</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_9" >
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_15" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>Send email &from:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy" >
|
||||||
|
<cstring>email_from</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="email_from" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string><p>This is what will be present in the From: field of emails sent by calibre.<br> Set it to your email address</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_8" >
|
||||||
|
<item>
|
||||||
|
<widget class="QTableView" name="email_view" >
|
||||||
|
<property name="selectionMode" >
|
||||||
|
<enum>QAbstractItemView::SingleSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior" >
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_8" >
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="email_add" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>Add an email address to which to send books</string>
|
||||||
|
</property>
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Add email</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon" >
|
||||||
|
<iconset resource="../images.qrc" >
|
||||||
|
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize" >
|
||||||
|
<size>
|
||||||
|
<width>24</width>
|
||||||
|
<height>24</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolButtonStyle" >
|
||||||
|
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="email_make_default" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>Make &default</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="email_remove" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Remove email</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon" >
|
||||||
|
<iconset resource="../images.qrc" >
|
||||||
|
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize" >
|
||||||
|
<size>
|
||||||
|
<width>24</width>
|
||||||
|
<height>24</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolButtonStyle" >
|
||||||
|
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_10" >
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_5" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string><p>A mail server is useful if the service you are sending mail to only accepts email from well know mail services.</string>
|
||||||
|
</property>
|
||||||
|
<property name="title" >
|
||||||
|
<string>Mail &Server</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_3" >
|
||||||
|
<item row="0" column="0" colspan="4" >
|
||||||
|
<widget class="QLabel" name="label_16" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>calibre can <b>optionally</b> use a server to send mail</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" >
|
||||||
|
<widget class="QLabel" name="label_17" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Hostname:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy" >
|
||||||
|
<cstring>relay_host</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1" colspan="2" >
|
||||||
|
<widget class="QLineEdit" name="relay_host" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>The hostname if your mail server. For e.g. smtp.gmail.com</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="3" >
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_11" >
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_18" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Port:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy" >
|
||||||
|
<cstring>relay_port</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="relay_port" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>The port your mail server listens for connections on. The default is 25</string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum" >
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum" >
|
||||||
|
<number>65555</number>
|
||||||
|
</property>
|
||||||
|
<property name="value" >
|
||||||
|
<number>25</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" >
|
||||||
|
<widget class="QLabel" name="label_19" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Username:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy" >
|
||||||
|
<cstring>relay_username</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1" colspan="2" >
|
||||||
|
<widget class="QLineEdit" name="relay_username" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>Your username on the mail server</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" >
|
||||||
|
<widget class="QLabel" name="label_20" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Password:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy" >
|
||||||
|
<cstring>relay_password</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1" colspan="2" >
|
||||||
|
<widget class="QLineEdit" name="relay_password" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>Your password on the mail server</string>
|
||||||
|
</property>
|
||||||
|
<property name="echoMode" >
|
||||||
|
<enum>QLineEdit::Password</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="3" >
|
||||||
|
<widget class="QCheckBox" name="relay_show_password" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Show</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0" >
|
||||||
|
<widget class="QLabel" name="label_21" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Encryption:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy" >
|
||||||
|
<cstring>relay_tls</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1" >
|
||||||
|
<widget class="QRadioButton" name="relay_tls" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>Use TLS encryption when connecting to the mail server. This is the most common.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text" >
|
||||||
|
<string>&TLS</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="2" colspan="2" >
|
||||||
|
<widget class="QRadioButton" name="relay_ssl" >
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>Use SSL encryption when connecting to the mail server.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text" >
|
||||||
|
<string>&SSL</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="relay_use_gmail" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>Use Gmail</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon" >
|
||||||
|
<iconset resource="../images.qrc" >
|
||||||
|
<normaloff>:/images/gmail_logo.png</normaloff>:/images/gmail_logo.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize" >
|
||||||
|
<size>
|
||||||
|
<width>48</width>
|
||||||
|
<height>48</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolButtonStyle" >
|
||||||
|
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
<widget class="QWidget" name="page_2" >
|
<widget class="QWidget" name="page_2" >
|
||||||
<layout class="QVBoxLayout" >
|
<layout class="QVBoxLayout" >
|
||||||
<item>
|
<item>
|
||||||
|
@ -104,14 +104,15 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
|||||||
self.author = author.strip()
|
self.author = author.strip()
|
||||||
self.publisher = publisher
|
self.publisher = publisher
|
||||||
self.previous_row = None
|
self.previous_row = None
|
||||||
|
self.warning.setVisible(False)
|
||||||
self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen)
|
self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen)
|
||||||
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
||||||
lambda index:self.matches.setCurrentIndex(index))
|
self.show_summary)
|
||||||
self.matches.setMouseTracking(True)
|
self.matches.setMouseTracking(True)
|
||||||
self.fetch_metadata()
|
self.fetch_metadata()
|
||||||
|
|
||||||
|
|
||||||
def show_summary(self, current, previous):
|
def show_summary(self, current, *args):
|
||||||
row = current.row()
|
row = current.row()
|
||||||
if row != self.previous_row:
|
if row != self.previous_row:
|
||||||
summ = self.model.summary(row)
|
summ = self.model.summary(row)
|
||||||
@ -119,6 +120,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
|||||||
self.previous_row = row
|
self.previous_row = row
|
||||||
|
|
||||||
def fetch_metadata(self):
|
def fetch_metadata(self):
|
||||||
|
self.warning.setVisible(False)
|
||||||
key = str(self.key.text())
|
key = str(self.key.text())
|
||||||
if key:
|
if key:
|
||||||
prefs['isbndb_com_key'] = key
|
prefs['isbndb_com_key'] = key
|
||||||
@ -158,14 +160,14 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
|||||||
self.fetcher.exceptions if x[1] is not None]
|
self.fetcher.exceptions if x[1] is not None]
|
||||||
if warnings:
|
if warnings:
|
||||||
warnings='<br>'.join(['<b>%s</b>: %s'%(name, exc) for name,exc in warnings])
|
warnings='<br>'.join(['<b>%s</b>: %s'%(name, exc) for name,exc in warnings])
|
||||||
warning_dialog(self, _('Warning'),
|
self.warning.setText('<p><b>'+ _('Warning')+':</b>'+\
|
||||||
'<p>'+_('Could not fetch metadata from:')+\
|
_('Could not fetch metadata from:')+\
|
||||||
'<br><br>'+warnings+'</p>').exec_()
|
'<br>'+warnings+'</p>')
|
||||||
|
self.warning.setVisible(True)
|
||||||
if self.model.rowCount() < 1:
|
if self.model.rowCount() < 1:
|
||||||
info_dialog(self, _('No metadata found'),
|
info_dialog(self, _('No metadata found'),
|
||||||
_('No metadata found, try adjusting the title and author '
|
_('No metadata found, try adjusting the title and author '
|
||||||
'or the ISBN key.')).exec_()
|
'or the ISBN key.')).exec_()
|
||||||
self.reject()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.matches.setModel(self.model)
|
self.matches.setModel(self.model)
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="tlabel" >
|
<widget class="QLabel" name="tlabel" >
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
<string><p>calibre can find metadata for your books from two locations: <b>Google Books</b> and <b>isbndb.com</b>. <p>To use isbndb.com you must sign up for a <a href="http://www.isbndb.com">free account</a> and exter you access key below.</string>
|
<string><p>calibre can find metadata for your books from two locations: <b>Google Books</b> and <b>isbndb.com</b>. <p>To use isbndb.com you must sign up for a <a href="http://www.isbndb.com">free account</a> and enter your access key below.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment" >
|
<property name="alignment" >
|
||||||
<set>Qt::AlignCenter</set>
|
<set>Qt::AlignCenter</set>
|
||||||
@ -60,6 +60,16 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="warning" >
|
||||||
|
<property name="text" >
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox" >
|
<widget class="QGroupBox" name="groupBox" >
|
||||||
<property name="title" >
|
<property name="title" >
|
||||||
|
@ -428,7 +428,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
prefix += '\n'
|
prefix += '\n'
|
||||||
self.comments.setText(prefix + summ)
|
self.comments.setText(prefix + summ)
|
||||||
else:
|
else:
|
||||||
error_dialog(self, 'Cannot fetch metadata', 'You must specify at least one of ISBN, Title, Authors or Publisher')
|
error_dialog(self, _('Cannot fetch metadata'),
|
||||||
|
_('You must specify at least one of ISBN, Title, '
|
||||||
|
'Authors or Publisher'))
|
||||||
|
|
||||||
def enable_series_index(self, *args):
|
def enable_series_index(self, *args):
|
||||||
self.series_index.setEnabled(True)
|
self.series_index.setEnabled(True)
|
||||||
|
BIN
src/calibre/gui2/images/gmail_logo.png
Normal file
BIN
src/calibre/gui2/images/gmail_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
270
src/calibre/gui2/images/mail.svg
Normal file
270
src/calibre/gui2/images/mail.svg
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg
|
||||||
|
xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/"
|
||||||
|
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="svg3007"
|
||||||
|
sodipodi:version="0.32"
|
||||||
|
inkscape:version="0.45.1"
|
||||||
|
version="1.0"
|
||||||
|
sodipodi:docbase="/home/david/Documents/Projects/KDE/Oxygen/kdelibs/scalable/actions"
|
||||||
|
sodipodi:docname="mail.svgz"
|
||||||
|
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
|
||||||
|
inkscape:export-filename="/home/david/Documents/Projects/KDE/Oxygen/kdelibs/scalable/actions/mail.png"
|
||||||
|
inkscape:export-xdpi="90"
|
||||||
|
inkscape:export-ydpi="90">
|
||||||
|
<defs
|
||||||
|
id="defs3009">
|
||||||
|
<linearGradient
|
||||||
|
id="polygon3293_1_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="615.5"
|
||||||
|
y1="-584.6021"
|
||||||
|
x2="615.5"
|
||||||
|
y2="-595.8521"
|
||||||
|
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#6193CF"
|
||||||
|
id="stop2997" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#EEEEEE"
|
||||||
|
id="stop2999" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="polygon3286_1_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="615.5"
|
||||||
|
y1="-589.8511"
|
||||||
|
x2="615.5"
|
||||||
|
y2="-580.6011"
|
||||||
|
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#6193CF"
|
||||||
|
id="stop2991" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#D1DFF1"
|
||||||
|
id="stop2993" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="rect3244_1_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="59.9995"
|
||||||
|
y1="4"
|
||||||
|
x2="59.9995"
|
||||||
|
y2="72.0005"
|
||||||
|
gradientTransform="matrix(1,0,0,1.0588235,0,-0.2352941)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#A4C0E4"
|
||||||
|
id="stop2983" />
|
||||||
|
<stop
|
||||||
|
offset="0.25"
|
||||||
|
style="stop-color:#D1DFF1"
|
||||||
|
id="stop2985" />
|
||||||
|
<stop
|
||||||
|
offset="0.85"
|
||||||
|
style="stop-color:#FFFFFF"
|
||||||
|
id="stop2987" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#rect3244_1_"
|
||||||
|
id="linearGradient2212"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(1,0,0,1.0588235,0,-0.2352941)"
|
||||||
|
x1="59.9995"
|
||||||
|
y1="4"
|
||||||
|
x2="59.9995"
|
||||||
|
y2="72.0005" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#polygon3286_1_"
|
||||||
|
id="linearGradient2214"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)"
|
||||||
|
x1="615.5"
|
||||||
|
y1="-589.8511"
|
||||||
|
x2="615.5"
|
||||||
|
y2="-580.6011" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#polygon3293_1_"
|
||||||
|
id="linearGradient2216"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)"
|
||||||
|
x1="615.5"
|
||||||
|
y1="-584.6021"
|
||||||
|
x2="615.5"
|
||||||
|
y2="-595.8521" />
|
||||||
|
</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="g2972"
|
||||||
|
width="128px"
|
||||||
|
height="128px"
|
||||||
|
inkscape:showpageshadow="false"
|
||||||
|
inkscape:window-width="794"
|
||||||
|
inkscape:window-height="731"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
showgrid="true"
|
||||||
|
gridspacingx="4px"
|
||||||
|
gridspacingy="4px"
|
||||||
|
gridempspacing="2"
|
||||||
|
showborder="false" />
|
||||||
|
<metadata
|
||||||
|
id="metadata3012">
|
||||||
|
<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">
|
||||||
|
<switch
|
||||||
|
id="switch2966"
|
||||||
|
transform="translate(4,12)">
|
||||||
|
<foreignObject
|
||||||
|
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
id="foreignObject2968">
|
||||||
|
<i:pgfRef
|
||||||
|
xlink:href="#adobe_illustrator_pgf" />
|
||||||
|
</foreignObject>
|
||||||
|
<g
|
||||||
|
i:extraneous="self"
|
||||||
|
id="g2970">
|
||||||
|
<g
|
||||||
|
id="g2972">
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3033"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="59.9995"
|
||||||
|
y1="4"
|
||||||
|
x2="59.9995"
|
||||||
|
y2="72.000504">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#A4C0E4"
|
||||||
|
id="stop3035" />
|
||||||
|
<stop
|
||||||
|
offset="0.25"
|
||||||
|
style="stop-color:#D1DFF1"
|
||||||
|
id="stop3037" />
|
||||||
|
<stop
|
||||||
|
offset="0.85"
|
||||||
|
style="stop-color:#FFFFFF"
|
||||||
|
id="stop3039" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3042"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="615.5"
|
||||||
|
y1="-589.85107"
|
||||||
|
x2="615.5"
|
||||||
|
y2="-580.60107"
|
||||||
|
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#6193CF"
|
||||||
|
id="stop3044" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#D1DFF1"
|
||||||
|
id="stop3046" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3049"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="615.5"
|
||||||
|
y1="-584.60211"
|
||||||
|
x2="615.5"
|
||||||
|
y2="-595.85211"
|
||||||
|
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#6193CF"
|
||||||
|
id="stop3051" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#EEEEEE"
|
||||||
|
id="stop3053" />
|
||||||
|
</linearGradient>
|
||||||
|
<g
|
||||||
|
id="g2202"
|
||||||
|
transform="translate(0,8)">
|
||||||
|
<path
|
||||||
|
style="opacity:0.1"
|
||||||
|
id="path2974"
|
||||||
|
d="M 4,0 C 1.794,0 0,1.8884211 0,4.2105263 L 0,75.789474 C 0,78.111579 1.794,80 4,80 L 116,80 C 118.206,80 120,78.111579 120,75.789474 L 120,4.2105263 C 120,1.8884211 118.206,0 116,0 L 4,0 z " />
|
||||||
|
<path
|
||||||
|
style="opacity:0.15"
|
||||||
|
id="path2976"
|
||||||
|
d="M 4,1 C 2.346,1 1,2.4187568 1,4.1621622 L 1,75.837838 C 1,77.581243 2.346,79 4,79 L 116,79 C 117.654,79 119,77.581243 119,75.837838 L 119,4.1621622 C 119,2.4187568 117.654,1 116,1 L 4,1 z " />
|
||||||
|
<path
|
||||||
|
style="opacity:0.2"
|
||||||
|
id="path2978"
|
||||||
|
d="M 4,2 C 2.897,2 2,2.9468333 2,4.1111111 L 2,75.888889 C 2,77.053167 2.897,78 4,78 L 116,78 C 117.103,78 118,77.053167 118,75.888889 L 118,4.1111111 C 118,2.9468333 117.103,2 116,2 L 4,2 z " />
|
||||||
|
<path
|
||||||
|
style="opacity:0.25"
|
||||||
|
id="path2980"
|
||||||
|
d="M 4,3 C 3.448,3 3,3.4736 3,4.0571428 L 3,75.942857 C 3,76.527457 3.448,77 4,77 L 116,77 C 116.553,77 117,76.527457 117,75.942857 L 117,4.0571428 C 117,3.4736 116.553,3 116,3 L 4,3 z " />
|
||||||
|
<rect
|
||||||
|
style="fill:url(#linearGradient2212)"
|
||||||
|
height="72"
|
||||||
|
width="112"
|
||||||
|
y="4"
|
||||||
|
x="4"
|
||||||
|
id="rect3244_9_" />
|
||||||
|
<polygon
|
||||||
|
style="fill:url(#linearGradient2214)"
|
||||||
|
points="4,8 4,12 60,45 116,12 116,8 60,41 4,8 "
|
||||||
|
id="polygon3286_9_" />
|
||||||
|
<polygon
|
||||||
|
style="fill:url(#linearGradient2216)"
|
||||||
|
points="116,69 116,65 59.997,24 4,65 4,69 59.997,28 116,69 "
|
||||||
|
id="polygon3293_9_" />
|
||||||
|
<polygon
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="polygon3002"
|
||||||
|
points="4,8 60.004,40.967 116,8 116,4 4,4 4,8 " />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 8.4 KiB |
@ -86,15 +86,15 @@ class LibraryDelegate(QItemDelegate):
|
|||||||
return sb
|
return sb
|
||||||
|
|
||||||
class DateDelegate(QStyledItemDelegate):
|
class DateDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
def displayText(self, val, locale):
|
def displayText(self, val, locale):
|
||||||
d = val.toDate()
|
d = val.toDate()
|
||||||
return d.toString('dd MMM yyyy')
|
return d.toString('dd MMM yyyy')
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
def createEditor(self, parent, option, index):
|
||||||
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
||||||
qde.setDisplayFormat('MM/dd/yyyy')
|
qde.setDisplayFormat('MM/dd/yyyy')
|
||||||
qde.setMinimumDate(QDate(-4000,1,1))
|
qde.setMinimumDate(QDate(101,1,1))
|
||||||
qde.setCalendarPopup(True)
|
qde.setCalendarPopup(True)
|
||||||
return qde
|
return qde
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
[1000,900,500,400,100,90,50,40,10,9,5,4,1],
|
[1000,900,500,400,100,90,50,40,10,9,5,4,1],
|
||||||
["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]
|
["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]
|
||||||
)
|
)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'title' : _("Title"),
|
'title' : _("Title"),
|
||||||
'authors' : _("Author(s)"),
|
'authors' : _("Author(s)"),
|
||||||
@ -114,7 +114,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
'tags' : _("Tags"),
|
'tags' : _("Tags"),
|
||||||
'series' : _("Series"),
|
'series' : _("Series"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def roman(cls, num):
|
def roman(cls, num):
|
||||||
if num <= 0 or num >= 4000 or int(num) != num:
|
if num <= 0 or num >= 4000 or int(num) != num:
|
||||||
@ -130,7 +130,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
QAbstractTableModel.__init__(self, parent)
|
QAbstractTableModel.__init__(self, parent)
|
||||||
self.db = None
|
self.db = None
|
||||||
self.column_map = config['column_map']
|
self.column_map = config['column_map']
|
||||||
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
|
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
|
||||||
'tags', 'series', 'timestamp']
|
'tags', 'series', 'timestamp']
|
||||||
self.default_image = QImage(':/images/book.svg')
|
self.default_image = QImage(':/images/book.svg')
|
||||||
self.sorted_on = ('timestamp', Qt.AscendingOrder)
|
self.sorted_on = ('timestamp', Qt.AscendingOrder)
|
||||||
@ -157,10 +157,10 @@ class BooksModel(QAbstractTableModel):
|
|||||||
tidx = self.column_map.index('timestamp')
|
tidx = self.column_map.index('timestamp')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
tidx = -1
|
tidx = -1
|
||||||
|
|
||||||
self.emit(SIGNAL('columns_sorted(int,int)'), idx, tidx)
|
self.emit(SIGNAL('columns_sorted(int,int)'), idx, tidx)
|
||||||
|
|
||||||
|
|
||||||
def set_database(self, db):
|
def set_database(self, db):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.build_data_convertors()
|
self.build_data_convertors()
|
||||||
@ -169,7 +169,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
rows = self.db.refresh_ids(ids)
|
rows = self.db.refresh_ids(ids)
|
||||||
if rows:
|
if rows:
|
||||||
self.refresh_rows(rows, current_row=current_row)
|
self.refresh_rows(rows, current_row=current_row)
|
||||||
|
|
||||||
def refresh_rows(self, rows, current_row=-1):
|
def refresh_rows(self, rows, current_row=-1):
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if self.cover_cache:
|
if self.cover_cache:
|
||||||
@ -191,12 +191,12 @@ class BooksModel(QAbstractTableModel):
|
|||||||
add_duplicates=add_duplicates)
|
add_duplicates=add_duplicates)
|
||||||
self.count_changed()
|
self.count_changed()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def add_news(self, path, recipe):
|
def add_news(self, path, recipe):
|
||||||
ret = self.db.add_news(path, recipe)
|
ret = self.db.add_news(path, recipe)
|
||||||
self.count_changed()
|
self.count_changed()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def count_changed(self, *args):
|
def count_changed(self, *args):
|
||||||
self.emit(SIGNAL('count_changed(int)'), self.db.count())
|
self.emit(SIGNAL('count_changed(int)'), self.db.count())
|
||||||
|
|
||||||
@ -208,12 +208,12 @@ class BooksModel(QAbstractTableModel):
|
|||||||
callback=None):
|
callback=None):
|
||||||
rows = [row.row() for row in rows]
|
rows = [row.row() for row in rows]
|
||||||
if single_format is None:
|
if single_format is None:
|
||||||
return self.db.export_to_dir(path, rows,
|
return self.db.export_to_dir(path, rows,
|
||||||
self.sorted_on[0] == 'authors',
|
self.sorted_on[0] == 'authors',
|
||||||
single_dir=single_dir,
|
single_dir=single_dir,
|
||||||
callback=callback)
|
callback=callback)
|
||||||
else:
|
else:
|
||||||
return self.db.export_single_format_to_dir(path, rows,
|
return self.db.export_single_format_to_dir(path, rows,
|
||||||
single_format,
|
single_format,
|
||||||
callback=callback)
|
callback=callback)
|
||||||
|
|
||||||
@ -225,8 +225,8 @@ class BooksModel(QAbstractTableModel):
|
|||||||
self.count_changed()
|
self.count_changed()
|
||||||
self.clear_caches()
|
self.clear_caches()
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
|
|
||||||
def delete_books_by_id(self, ids):
|
def delete_books_by_id(self, ids):
|
||||||
for id in ids:
|
for id in ids:
|
||||||
try:
|
try:
|
||||||
@ -263,18 +263,18 @@ class BooksModel(QAbstractTableModel):
|
|||||||
self.clear_caches()
|
self.clear_caches()
|
||||||
self.reset()
|
self.reset()
|
||||||
self.sorted_on = (self.column_map[col], order)
|
self.sorted_on = (self.column_map[col], order)
|
||||||
|
|
||||||
|
|
||||||
def refresh(self, reset=True):
|
def refresh(self, reset=True):
|
||||||
try:
|
try:
|
||||||
col = self.column_map.index(self.sorted_on[0])
|
col = self.column_map.index(self.sorted_on[0])
|
||||||
except:
|
except:
|
||||||
col = 0
|
col = 0
|
||||||
self.db.refresh(field=self.column_map[col],
|
self.db.refresh(field=self.column_map[col],
|
||||||
ascending=self.sorted_on[1]==Qt.AscendingOrder)
|
ascending=self.sorted_on[1]==Qt.AscendingOrder)
|
||||||
if reset:
|
if reset:
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def resort(self, reset=True):
|
def resort(self, reset=True):
|
||||||
try:
|
try:
|
||||||
col = self.column_map.index(self.sorted_on[0])
|
col = self.column_map.index(self.sorted_on[0])
|
||||||
@ -412,14 +412,14 @@ class BooksModel(QAbstractTableModel):
|
|||||||
if format is None:
|
if format is None:
|
||||||
ans.append(format)
|
ans.append(format)
|
||||||
else:
|
else:
|
||||||
f = self.db.format(id, format, index_is_id=True, as_file=True,
|
f = self.db.format(id, format, index_is_id=True, as_file=True,
|
||||||
mode=mode)
|
mode=mode)
|
||||||
ans.append(f)
|
ans.append(f)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_preferred_formats(self, rows, formats, paths=False,
|
def get_preferred_formats(self, rows, formats, paths=False,
|
||||||
set_metadata=False, specific_format=None):
|
set_metadata=False, specific_format=None):
|
||||||
ans = []
|
ans = []
|
||||||
if specific_format is not None:
|
if specific_format is not None:
|
||||||
@ -430,7 +430,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
if not fmts:
|
if not fmts:
|
||||||
fmts = ''
|
fmts = ''
|
||||||
db_formats = set(fmts.lower().split(','))
|
db_formats = set(fmts.lower().split(','))
|
||||||
available_formats = set([f.lower() for f in formats])
|
available_formats = set([f.lower() for f in formats])
|
||||||
u = available_formats.intersection(db_formats)
|
u = available_formats.intersection(db_formats)
|
||||||
for f in formats:
|
for f in formats:
|
||||||
if f.lower() in u:
|
if f.lower() in u:
|
||||||
@ -469,7 +469,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
data = self.db.cover(row_number)
|
data = self.db.cover(row_number)
|
||||||
except IndexError: # Happens if database has not yet been refreshed
|
except IndexError: # Happens if database has not yet been refreshed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return self.default_image
|
return self.default_image
|
||||||
img = QImage()
|
img = QImage()
|
||||||
@ -479,7 +479,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
def build_data_convertors(self):
|
def build_data_convertors(self):
|
||||||
|
|
||||||
tidx = FIELD_MAP['title']
|
tidx = FIELD_MAP['title']
|
||||||
aidx = FIELD_MAP['authors']
|
aidx = FIELD_MAP['authors']
|
||||||
sidx = FIELD_MAP['size']
|
sidx = FIELD_MAP['size']
|
||||||
@ -489,44 +489,44 @@ class BooksModel(QAbstractTableModel):
|
|||||||
srdx = FIELD_MAP['series']
|
srdx = FIELD_MAP['series']
|
||||||
tgdx = FIELD_MAP['tags']
|
tgdx = FIELD_MAP['tags']
|
||||||
siix = FIELD_MAP['series_index']
|
siix = FIELD_MAP['series_index']
|
||||||
|
|
||||||
def authors(r):
|
def authors(r):
|
||||||
au = self.db.data[r][aidx]
|
au = self.db.data[r][aidx]
|
||||||
if au:
|
if au:
|
||||||
au = [a.strip().replace('|', ',') for a in au.split(',')]
|
au = [a.strip().replace('|', ',') for a in au.split(',')]
|
||||||
return ' & '.join(au)
|
return ' & '.join(au)
|
||||||
|
|
||||||
def timestamp(r):
|
def timestamp(r):
|
||||||
dt = self.db.data[r][tmdx]
|
dt = self.db.data[r][tmdx]
|
||||||
if dt:
|
if dt:
|
||||||
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
||||||
return QDate(dt.year, dt.month, dt.day)
|
return QDate(dt.year, dt.month, dt.day)
|
||||||
|
|
||||||
def rating(r):
|
def rating(r):
|
||||||
r = self.db.data[r][ridx]
|
r = self.db.data[r][ridx]
|
||||||
r = r/2 if r else 0
|
r = r/2 if r else 0
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def publisher(r):
|
def publisher(r):
|
||||||
pub = self.db.data[r][pidx]
|
pub = self.db.data[r][pidx]
|
||||||
if pub:
|
if pub:
|
||||||
return pub
|
return pub
|
||||||
|
|
||||||
def tags(r):
|
def tags(r):
|
||||||
tags = self.db.data[r][tgdx]
|
tags = self.db.data[r][tgdx]
|
||||||
if tags:
|
if tags:
|
||||||
return ', '.join(tags.split(','))
|
return ', '.join(tags.split(','))
|
||||||
|
|
||||||
def series(r):
|
def series(r):
|
||||||
series = self.db.data[r][srdx]
|
series = self.db.data[r][srdx]
|
||||||
if series:
|
if series:
|
||||||
return series + ' [%d]'%self.db.data[r][siix]
|
return series + ' [%d]'%self.db.data[r][siix]
|
||||||
|
|
||||||
def size(r):
|
def size(r):
|
||||||
size = self.db.data[r][sidx]
|
size = self.db.data[r][sidx]
|
||||||
if size:
|
if size:
|
||||||
return '%.1f'%(float(size)/(1024*1024))
|
return '%.1f'%(float(size)/(1024*1024))
|
||||||
|
|
||||||
self.dc = {
|
self.dc = {
|
||||||
'title' : lambda r : self.db.data[r][tidx],
|
'title' : lambda r : self.db.data[r][tidx],
|
||||||
'authors' : authors,
|
'authors' : authors,
|
||||||
@ -535,7 +535,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
'rating' : rating,
|
'rating' : rating,
|
||||||
'publisher': publisher,
|
'publisher': publisher,
|
||||||
'tags' : tags,
|
'tags' : tags,
|
||||||
'series' : series,
|
'series' : series,
|
||||||
}
|
}
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
@ -573,7 +573,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
val = int(value.toInt()[0]) if column == 'rating' else \
|
val = int(value.toInt()[0]) if column == 'rating' else \
|
||||||
value.toDate() if column == 'timestamp' else \
|
value.toDate() if column == 'timestamp' else \
|
||||||
unicode(value.toString())
|
unicode(value.toString())
|
||||||
id = self.db.id(row)
|
id = self.db.id(row)
|
||||||
if column == 'rating':
|
if column == 'rating':
|
||||||
val = 0 if val < 0 else 5 if val > 5 else val
|
val = 0 if val < 0 else 5 if val > 5 else val
|
||||||
val *= 2
|
val *= 2
|
||||||
@ -598,7 +598,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
index, index)
|
index, index)
|
||||||
if column == self.sorted_on[0]:
|
if column == self.sorted_on[0]:
|
||||||
self.resort()
|
self.resort()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class BooksView(TableView):
|
class BooksView(TableView):
|
||||||
@ -632,7 +632,7 @@ class BooksView(TableView):
|
|||||||
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
||||||
self._model.current_changed)
|
self._model.current_changed)
|
||||||
self.connect(self._model, SIGNAL('columns_sorted(int, int)'), self.columns_sorted, Qt.QueuedConnection)
|
self.connect(self._model, SIGNAL('columns_sorted(int, int)'), self.columns_sorted, Qt.QueuedConnection)
|
||||||
|
|
||||||
def columns_sorted(self, rating_col, timestamp_col):
|
def columns_sorted(self, rating_col, timestamp_col):
|
||||||
for i in range(self.model().columnCount(None)):
|
for i in range(self.model().columnCount(None)):
|
||||||
if self.itemDelegateForColumn(i) == self.rating_delegate:
|
if self.itemDelegateForColumn(i) == self.rating_delegate:
|
||||||
@ -641,8 +641,8 @@ class BooksView(TableView):
|
|||||||
self.setItemDelegateForColumn(rating_col, self.rating_delegate)
|
self.setItemDelegateForColumn(rating_col, self.rating_delegate)
|
||||||
if timestamp_col > -1:
|
if timestamp_col > -1:
|
||||||
self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate)
|
self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate)
|
||||||
|
|
||||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
||||||
save, open_folder, book_details, similar_menu=None):
|
save, open_folder, book_details, similar_menu=None):
|
||||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||||
self.context_menu = QMenu(self)
|
self.context_menu = QMenu(self)
|
||||||
@ -660,18 +660,18 @@ class BooksView(TableView):
|
|||||||
self.context_menu.addAction(book_details)
|
self.context_menu.addAction(book_details)
|
||||||
if similar_menu is not None:
|
if similar_menu is not None:
|
||||||
self.context_menu.addMenu(similar_menu)
|
self.context_menu.addMenu(similar_menu)
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.context_menu.popup(event.globalPos())
|
self.context_menu.popup(event.globalPos())
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
def sortByColumn(self, colname, order):
|
def sortByColumn(self, colname, order):
|
||||||
try:
|
try:
|
||||||
idx = self._model.column_map.index(colname)
|
idx = self._model.column_map.index(colname)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
idx = 0
|
idx = 0
|
||||||
TableView.sortByColumn(self, idx, order)
|
TableView.sortByColumn(self, idx, order)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def paths_from_event(cls, event):
|
def paths_from_event(cls, event):
|
||||||
'''
|
'''
|
||||||
@ -734,23 +734,23 @@ class DeviceBooksView(BooksView):
|
|||||||
|
|
||||||
def connect_dirtied_signal(self, slot):
|
def connect_dirtied_signal(self, slot):
|
||||||
QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
|
QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
|
||||||
|
|
||||||
def sortByColumn(self, col, order):
|
def sortByColumn(self, col, order):
|
||||||
TableView.sortByColumn(self, col, order)
|
TableView.sortByColumn(self, col, order)
|
||||||
|
|
||||||
def dropEvent(self, *args):
|
def dropEvent(self, *args):
|
||||||
error_dialog(self, _('Not allowed'),
|
error_dialog(self, _('Not allowed'),
|
||||||
_('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_()
|
_('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_()
|
||||||
|
|
||||||
class OnDeviceSearch(SearchQueryParser):
|
class OnDeviceSearch(SearchQueryParser):
|
||||||
|
|
||||||
def __init__(self, model):
|
def __init__(self, model):
|
||||||
SearchQueryParser.__init__(self)
|
SearchQueryParser.__init__(self)
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
def universal_set(self):
|
def universal_set(self):
|
||||||
return set(range(0, len(self.model.db)))
|
return set(range(0, len(self.model.db)))
|
||||||
|
|
||||||
def get_matches(self, location, query):
|
def get_matches(self, location, query):
|
||||||
location = location.lower().strip()
|
location = location.lower().strip()
|
||||||
query = query.lower().strip()
|
query = query.lower().strip()
|
||||||
@ -771,7 +771,7 @@ class OnDeviceSearch(SearchQueryParser):
|
|||||||
matches.add(i)
|
matches.add(i)
|
||||||
break
|
break
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
|
||||||
class DeviceBooksModel(BooksModel):
|
class DeviceBooksModel(BooksModel):
|
||||||
|
|
||||||
@ -835,10 +835,10 @@ class DeviceBooksModel(BooksModel):
|
|||||||
if reset:
|
if reset:
|
||||||
self.reset()
|
self.reset()
|
||||||
self.last_search = text
|
self.last_search = text
|
||||||
|
|
||||||
def resort(self, reset):
|
def resort(self, reset):
|
||||||
self.sort(self.sorted_on[0], self.sorted_on[1], reset=reset)
|
self.sort(self.sorted_on[0], self.sorted_on[1], reset=reset)
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
descending = order != Qt.AscendingOrder
|
descending = order != Qt.AscendingOrder
|
||||||
def strcmp(attr):
|
def strcmp(attr):
|
||||||
@ -957,7 +957,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
return QVariant('Marked for deletion')
|
return QVariant('Marked for deletion')
|
||||||
col = index.column()
|
col = index.column()
|
||||||
if col in [0, 1] or (col == 4 and self.db.supports_tags()):
|
if col in [0, 1] or (col == 4 and self.db.supports_tags()):
|
||||||
return QVariant("Double click to <b>edit</b> me<br><br>")
|
return QVariant(_("Double click to <b>edit</b> me<br><br>"))
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
@ -1060,11 +1060,11 @@ class SearchBox(QLineEdit):
|
|||||||
if not all:
|
if not all:
|
||||||
ans = '[' + ans + ']'
|
ans = '[' + ans + ']'
|
||||||
self.set_search_string(ans)
|
self.set_search_string(ans)
|
||||||
|
|
||||||
def search_from_tags(self, tags, all):
|
def search_from_tags(self, tags, all):
|
||||||
joiner = ' and ' if all else ' or '
|
joiner = ' and ' if all else ' or '
|
||||||
self.set_search_string(joiner.join(tags))
|
self.set_search_string(joiner.join(tags))
|
||||||
|
|
||||||
def set_search_string(self, txt):
|
def set_search_string(self, txt):
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
self.setText(txt)
|
self.setText(txt)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<ui version="4.0" >
|
||||||
<ui version="4.0">
|
|
||||||
<author>Kovid Goyal</author>
|
<author>Kovid Goyal</author>
|
||||||
<class>MainWindow</class>
|
<class>MainWindow</class>
|
||||||
<widget class="QMainWindow" name="MainWindow">
|
<widget class="QMainWindow" name="MainWindow" >
|
||||||
<property name="geometry">
|
<property name="geometry" >
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
@ -11,140 +10,149 @@
|
|||||||
<height>822</height>
|
<height>822</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy" >
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="contextMenuPolicy">
|
<property name="contextMenuPolicy" >
|
||||||
<enum>Qt::NoContextMenu</enum>
|
<enum>Qt::NoContextMenu</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle" >
|
||||||
<string>__appname__</string>
|
<string>__appname__</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/library</normaloff>:/library</iconset>
|
<normaloff>:/library</normaloff>:/library</iconset>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="centralwidget">
|
<widget class="QWidget" name="centralwidget" >
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout" >
|
||||||
<item row="0" column="0">
|
<item row="0" column="0" >
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
<layout class="QHBoxLayout" name="horizontalLayout_3" >
|
||||||
<item>
|
<item>
|
||||||
<widget class="LocationView" name="location_view">
|
<widget class="LocationView" name="location_view" >
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy" >
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximumSize">
|
<property name="maximumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>16777215</width>
|
<width>16777215</width>
|
||||||
<height>100</height>
|
<height>100</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="verticalScrollBarPolicy">
|
<property name="verticalScrollBarPolicy" >
|
||||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="horizontalScrollBarPolicy">
|
<property name="horizontalScrollBarPolicy" >
|
||||||
<enum>Qt::ScrollBarAsNeeded</enum>
|
<enum>Qt::ScrollBarAsNeeded</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="tabKeyNavigation">
|
<property name="editTriggers" >
|
||||||
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
|
<property name="tabKeyNavigation" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="showDropIndicator" stdset="0">
|
<property name="showDropIndicator" stdset="0" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="iconSize">
|
<property name="selectionMode" >
|
||||||
|
<enum>QAbstractItemView::NoSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior" >
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>40</width>
|
<width>40</width>
|
||||||
<height>40</height>
|
<height>40</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="movement">
|
<property name="movement" >
|
||||||
<enum>QListView::Static</enum>
|
<enum>QListView::Static</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="flow">
|
<property name="flow" >
|
||||||
<enum>QListView::LeftToRight</enum>
|
<enum>QListView::LeftToRight</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="gridSize">
|
<property name="gridSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>175</width>
|
<width>175</width>
|
||||||
<height>90</height>
|
<height>90</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="viewMode">
|
<property name="viewMode" >
|
||||||
<enum>QListView::ListMode</enum>
|
<enum>QListView::ListMode</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="donate_button">
|
<widget class="QToolButton" name="donate_button" >
|
||||||
<property name="cursor">
|
<property name="cursor" >
|
||||||
<cursorShape>PointingHandCursor</cursorShape>
|
<cursorShape>PointingHandCursor</cursorShape>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>...</string>
|
<string>...</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
|
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="iconSize">
|
<property name="iconSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>64</width>
|
<width>64</width>
|
||||||
<height>64</height>
|
<height>64</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="autoRaise">
|
<property name="autoRaise" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="vanity">
|
<widget class="QLabel" name="vanity" >
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy" >
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximumSize">
|
<property name="maximumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>16777215</width>
|
<width>16777215</width>
|
||||||
<height>90</height>
|
<height>90</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
<property name="textFormat">
|
<property name="textFormat" >
|
||||||
<enum>Qt::RichText</enum>
|
<enum>Qt::RichText</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="openExternalLinks">
|
<property name="openExternalLinks" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
<layout class="QHBoxLayout" name="horizontalLayout_2" >
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2" >
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Output:</string>
|
<string>Output:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="output_format">
|
<widget class="QComboBox" name="output_format" >
|
||||||
<property name="toolTip">
|
<property name="toolTip" >
|
||||||
<string>Set the output format that is used when converting ebooks and downloading news</string>
|
<string>Set the output format that is used when converting ebooks and downloading news</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -155,99 +163,99 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0" >
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout" >
|
||||||
<property name="spacing">
|
<property name="spacing" >
|
||||||
<number>6</number>
|
<number>6</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="margin">
|
<property name="margin" >
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="advanced_search_button">
|
<widget class="QToolButton" name="advanced_search_button" >
|
||||||
<property name="toolTip">
|
<property name="toolTip" >
|
||||||
<string>Advanced search</string>
|
<string>Advanced search</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>...</string>
|
<string>...</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/search.svg</normaloff>:/images/search.svg</iconset>
|
<normaloff>:/images/search.svg</normaloff>:/images/search.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>Alt+S</string>
|
<string>Alt+S</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label" >
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>&Search:</string>
|
<string>&Search:</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
<property name="buddy" >
|
||||||
<cstring>search</cstring>
|
<cstring>search</cstring>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="SearchBox" name="search">
|
<widget class="SearchBox" name="search" >
|
||||||
<property name="enabled">
|
<property name="enabled" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy" >
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
|
||||||
<horstretch>1</horstretch>
|
<horstretch>1</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="acceptDrops">
|
<property name="acceptDrops" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip" >
|
||||||
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="whatsThis">
|
<property name="whatsThis" >
|
||||||
<string>Search the list of books by title, author, publisher, tags and comments<br><br>Words separated by spaces are ANDed</string>
|
<string>Search the list of books by title, author, publisher, tags and comments<br><br>Words separated by spaces are ANDed</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="autoFillBackground">
|
<property name="autoFillBackground" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
<property name="frame">
|
<property name="frame" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="clear_button">
|
<widget class="QToolButton" name="clear_button" >
|
||||||
<property name="toolTip">
|
<property name="toolTip" >
|
||||||
<string>Reset Quick Search</string>
|
<string>Reset Quick Search</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>...</string>
|
<string>...</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/clear_left.svg</normaloff>:/images/clear_left.svg</iconset>
|
<normaloff>:/images/clear_left.svg</normaloff>:/images/clear_left.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="Line" name="line">
|
<widget class="Line" name="line" >
|
||||||
<property name="orientation">
|
<property name="orientation" >
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer>
|
<spacer>
|
||||||
<property name="orientation">
|
<property name="orientation" >
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0" >
|
||||||
<size>
|
<size>
|
||||||
<width>20</width>
|
<width>20</width>
|
||||||
<height>20</height>
|
<height>20</height>
|
||||||
@ -256,77 +264,77 @@
|
|||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="config_button">
|
<widget class="QToolButton" name="config_button" >
|
||||||
<property name="toolTip">
|
<property name="toolTip" >
|
||||||
<string>Configuration</string>
|
<string>Configuration</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>...</string>
|
<string>...</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
|
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="2" column="0" >
|
||||||
<widget class="QStackedWidget" name="stack">
|
<widget class="QStackedWidget" name="stack" >
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy" >
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
||||||
<horstretch>100</horstretch>
|
<horstretch>100</horstretch>
|
||||||
<verstretch>100</verstretch>
|
<verstretch>100</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="currentIndex">
|
<property name="currentIndex" >
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="library">
|
<widget class="QWidget" name="library" >
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2" >
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout" >
|
||||||
<item>
|
<item>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout" >
|
||||||
<item>
|
<item>
|
||||||
<widget class="QRadioButton" name="match_any">
|
<widget class="QRadioButton" name="match_any" >
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Match any</string>
|
<string>Match any</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checked">
|
<property name="checked" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QRadioButton" name="match_all">
|
<widget class="QRadioButton" name="match_all" >
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Match all</string>
|
<string>Match all</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checked">
|
<property name="checked" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="popularity">
|
<widget class="QCheckBox" name="popularity" >
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Sort by &popularity</string>
|
<string>Sort by &popularity</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="TagsView" name="tags_view">
|
<widget class="TagsView" name="tags_view" >
|
||||||
<property name="tabKeyNavigation">
|
<property name="tabKeyNavigation" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="alternatingRowColors">
|
<property name="alternatingRowColors" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="animated">
|
<property name="animated" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="headerHidden">
|
<property name="headerHidden" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -334,35 +342,35 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="BooksView" name="library_view">
|
<widget class="BooksView" name="library_view" >
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy" >
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
||||||
<horstretch>100</horstretch>
|
<horstretch>100</horstretch>
|
||||||
<verstretch>10</verstretch>
|
<verstretch>10</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="acceptDrops">
|
<property name="acceptDrops" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragEnabled">
|
<property name="dragEnabled" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragDropOverwriteMode">
|
<property name="dragDropOverwriteMode" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragDropMode">
|
<property name="dragDropMode" >
|
||||||
<enum>QAbstractItemView::DragDrop</enum>
|
<enum>QAbstractItemView::DragDrop</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="alternatingRowColors">
|
<property name="alternatingRowColors" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="selectionBehavior">
|
<property name="selectionBehavior" >
|
||||||
<enum>QAbstractItemView::SelectRows</enum>
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="showGrid">
|
<property name="showGrid" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -371,76 +379,76 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="main_memory">
|
<widget class="QWidget" name="main_memory" >
|
||||||
<layout class="QGridLayout">
|
<layout class="QGridLayout" >
|
||||||
<item row="0" column="0">
|
<item row="0" column="0" >
|
||||||
<widget class="DeviceBooksView" name="memory_view">
|
<widget class="DeviceBooksView" name="memory_view" >
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy" >
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
||||||
<horstretch>100</horstretch>
|
<horstretch>100</horstretch>
|
||||||
<verstretch>10</verstretch>
|
<verstretch>10</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="acceptDrops">
|
<property name="acceptDrops" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragEnabled">
|
<property name="dragEnabled" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragDropOverwriteMode">
|
<property name="dragDropOverwriteMode" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragDropMode">
|
<property name="dragDropMode" >
|
||||||
<enum>QAbstractItemView::DragDrop</enum>
|
<enum>QAbstractItemView::DragDrop</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="alternatingRowColors">
|
<property name="alternatingRowColors" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="selectionBehavior">
|
<property name="selectionBehavior" >
|
||||||
<enum>QAbstractItemView::SelectRows</enum>
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="showGrid">
|
<property name="showGrid" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page">
|
<widget class="QWidget" name="page" >
|
||||||
<layout class="QGridLayout">
|
<layout class="QGridLayout" >
|
||||||
<item row="0" column="0">
|
<item row="0" column="0" >
|
||||||
<widget class="DeviceBooksView" name="card_view">
|
<widget class="DeviceBooksView" name="card_view" >
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy" >
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
<sizepolicy vsizetype="Expanding" hsizetype="Preferred" >
|
||||||
<horstretch>10</horstretch>
|
<horstretch>10</horstretch>
|
||||||
<verstretch>10</verstretch>
|
<verstretch>10</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="acceptDrops">
|
<property name="acceptDrops" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragEnabled">
|
<property name="dragEnabled" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragDropOverwriteMode">
|
<property name="dragDropOverwriteMode" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="dragDropMode">
|
<property name="dragDropMode" >
|
||||||
<enum>QAbstractItemView::DragDrop</enum>
|
<enum>QAbstractItemView::DragDrop</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="alternatingRowColors">
|
<property name="alternatingRowColors" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="selectionBehavior">
|
<property name="selectionBehavior" >
|
||||||
<enum>QAbstractItemView::SelectRows</enum>
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="showGrid">
|
<property name="showGrid" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -451,234 +459,225 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="tool_bar">
|
<widget class="QToolBar" name="tool_bar" >
|
||||||
<property name="minimumSize">
|
<property name="minimumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="contextMenuPolicy">
|
<property name="contextMenuPolicy" >
|
||||||
<enum>Qt::PreventContextMenu</enum>
|
<enum>Qt::PreventContextMenu</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="movable">
|
<property name="movable" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="orientation">
|
<property name="orientation" >
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="iconSize">
|
<property name="iconSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>48</width>
|
<width>48</width>
|
||||||
<height>48</height>
|
<height>48</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolButtonStyle">
|
<property name="toolButtonStyle" >
|
||||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||||
</property>
|
</property>
|
||||||
<attribute name="toolBarArea">
|
<attribute name="toolBarArea" >
|
||||||
<enum>TopToolBarArea</enum>
|
<enum>TopToolBarArea</enum>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="toolBarBreak">
|
<attribute name="toolBarBreak" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
<addaction name="action_add"/>
|
<addaction name="action_add" />
|
||||||
<addaction name="action_edit"/>
|
<addaction name="action_edit" />
|
||||||
<addaction name="action_convert"/>
|
<addaction name="action_convert" />
|
||||||
<addaction name="action_view"/>
|
<addaction name="action_view" />
|
||||||
<addaction name="action_news"/>
|
<addaction name="action_news" />
|
||||||
<addaction name="separator"/>
|
<addaction name="separator" />
|
||||||
<addaction name="action_sync"/>
|
<addaction name="action_sync" />
|
||||||
<addaction name="action_save"/>
|
<addaction name="action_save" />
|
||||||
<addaction name="action_del"/>
|
<addaction name="action_del" />
|
||||||
<addaction name="separator"/>
|
<addaction name="separator" />
|
||||||
<addaction name="action_preferences"/>
|
<addaction name="action_preferences" />
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusBar">
|
<widget class="QStatusBar" name="statusBar" >
|
||||||
<property name="mouseTracking">
|
<property name="mouseTracking" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="action_add">
|
<action name="action_add" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/add_book.svg</normaloff>:/images/add_book.svg</iconset>
|
<normaloff>:/images/add_book.svg</normaloff>:/images/add_book.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Add books</string>
|
<string>Add books</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>A</string>
|
<string>A</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="autoRepeat">
|
<property name="autoRepeat" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_del">
|
<action name="action_del" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
|
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Remove books</string>
|
<string>Remove books</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip" >
|
||||||
<string>Remove books</string>
|
<string>Remove books</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>Del</string>
|
<string>Del</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_edit">
|
<action name="action_edit" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
|
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Edit meta information</string>
|
<string>Edit meta information</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>E</string>
|
<string>E</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="autoRepeat">
|
<property name="autoRepeat" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_sync">
|
<action name="action_sync" >
|
||||||
<property name="enabled">
|
<property name="enabled" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/sync.svg</normaloff>:/images/sync.svg</iconset>
|
<normaloff>:/images/sync.svg</normaloff>:/images/sync.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Send to device</string>
|
<string>Send to device</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_save">
|
<action name="action_save" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/save.svg</normaloff>:/images/save.svg</iconset>
|
<normaloff>:/images/save.svg</normaloff>:/images/save.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Save to disk</string>
|
<string>Save to disk</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>S</string>
|
<string>S</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_news">
|
<action name="action_news" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/news.svg</normaloff>:/images/news.svg</iconset>
|
<normaloff>:/images/news.svg</normaloff>:/images/news.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Fetch news</string>
|
<string>Fetch news</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>F</string>
|
<string>F</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_convert">
|
<action name="action_convert" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
|
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Convert E-books</string>
|
<string>Convert E-books</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>C</string>
|
<string>C</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_view">
|
<action name="action_view" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/view.svg</normaloff>:/images/view.svg</iconset>
|
<normaloff>:/images/view.svg</normaloff>:/images/view.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>View</string>
|
<string>View</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>V</string>
|
<string>V</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_open_containing_folder">
|
<action name="action_open_containing_folder" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
|
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Open containing folder</string>
|
<string>Open containing folder</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_show_book_details">
|
<action name="action_show_book_details" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/dialog_information.svg</normaloff>:/images/dialog_information.svg</iconset>
|
<normaloff>:/images/dialog_information.svg</normaloff>:/images/dialog_information.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Show book details</string>
|
<string>Show book details</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_books_by_same_author">
|
<action name="action_books_by_same_author" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/user_profile.svg</normaloff>:/images/user_profile.svg</iconset>
|
<normaloff>:/images/user_profile.svg</normaloff>:/images/user_profile.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Books by same author</string>
|
<string>Books by same author</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_books_in_this_series">
|
<action name="action_books_in_this_series" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/books_in_series.svg</normaloff>:/images/books_in_series.svg</iconset>
|
<normaloff>:/images/books_in_series.svg</normaloff>:/images/books_in_series.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Books in this series</string>
|
<string>Books in this series</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_books_by_this_publisher">
|
<action name="action_books_by_this_publisher" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/publisher.png</normaloff>:/images/publisher.png</iconset>
|
<normaloff>:/images/publisher.png</normaloff>:/images/publisher.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Books by this publisher</string>
|
<string>Books by this publisher</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_books_with_the_same_tags">
|
<action name="action_books_with_the_same_tags" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/tags.svg</normaloff>:/images/tags.svg</iconset>
|
<normaloff>:/images/tags.svg</normaloff>:/images/tags.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Books with the same tags</string>
|
<string>Books with the same tags</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_send_specific_format_to_device">
|
<action name="action_preferences" >
|
||||||
<property name="icon">
|
<property name="icon" >
|
||||||
<iconset resource="images.qrc">
|
<iconset resource="images.qrc" >
|
||||||
<normaloff>:/images/book.svg</normaloff>:/images/book.svg</iconset>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Send specific format to device</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="action_preferences">
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="images.qrc">
|
|
||||||
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
|
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text" >
|
||||||
<string>Preferences</string>
|
<string>Preferences</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip" >
|
||||||
<string>Configure calibre</string>
|
<string>Configure calibre</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut" >
|
||||||
<string>Ctrl+P</string>
|
<string>Ctrl+P</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
@ -711,7 +710,7 @@
|
|||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="images.qrc"/>
|
<include location="images.qrc" />
|
||||||
</resources>
|
</resources>
|
||||||
<connections>
|
<connections>
|
||||||
<connection>
|
<connection>
|
||||||
@ -720,11 +719,11 @@
|
|||||||
<receiver>search</receiver>
|
<receiver>search</receiver>
|
||||||
<slot>clear()</slot>
|
<slot>clear()</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel">
|
<hint type="sourcelabel" >
|
||||||
<x>787</x>
|
<x>787</x>
|
||||||
<y>215</y>
|
<y>215</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel">
|
<hint type="destinationlabel" >
|
||||||
<x>755</x>
|
<x>755</x>
|
||||||
<y>213</y>
|
<y>213</y>
|
||||||
</hint>
|
</hint>
|
||||||
|
@ -9,7 +9,7 @@ from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \
|
|||||||
QSyntaxHighlighter, QCursor, QColor, QWidget, QDialog, \
|
QSyntaxHighlighter, QCursor, QColor, QWidget, QDialog, \
|
||||||
QPixmap, QMovie
|
QPixmap, QMovie
|
||||||
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \
|
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \
|
||||||
QObject, QRegExp, QSettings, QSize
|
QRegExp, QSettings, QSize, QModelIndex
|
||||||
|
|
||||||
from calibre.gui2.jobs2 import DetailView
|
from calibre.gui2.jobs2 import DetailView
|
||||||
from calibre.gui2 import human_readable, NONE, TableView, \
|
from calibre.gui2 import human_readable, NONE, TableView, \
|
||||||
@ -22,7 +22,7 @@ from calibre.utils.config import prefs
|
|||||||
from calibre.gui2.dialogs.warning_ui import Ui_Dialog as Ui_WarningDialog
|
from calibre.gui2.dialogs.warning_ui import Ui_Dialog as Ui_WarningDialog
|
||||||
|
|
||||||
class ProgressIndicator(QWidget):
|
class ProgressIndicator(QWidget):
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QWidget.__init__(self, *args)
|
QWidget.__init__(self, *args)
|
||||||
self.setGeometry(0, 0, 300, 350)
|
self.setGeometry(0, 0, 300, 350)
|
||||||
@ -37,7 +37,7 @@ class ProgressIndicator(QWidget):
|
|||||||
self.status.font().setBold(True)
|
self.status.font().setBold(True)
|
||||||
self.status.font().setPointSize(self.font().pointSize()+6)
|
self.status.font().setPointSize(self.font().pointSize()+6)
|
||||||
self.setVisible(False)
|
self.setVisible(False)
|
||||||
|
|
||||||
def start(self, msg=''):
|
def start(self, msg=''):
|
||||||
view = self.parent()
|
view = self.parent()
|
||||||
pwidth, pheight = view.size().width(), view.size().height()
|
pwidth, pheight = view.size().width(), view.size().height()
|
||||||
@ -50,15 +50,15 @@ class ProgressIndicator(QWidget):
|
|||||||
self.status.setText(msg)
|
self.status.setText(msg)
|
||||||
self.setVisible(True)
|
self.setVisible(True)
|
||||||
self.movie.setPaused(False)
|
self.movie.setPaused(False)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self.movie.state() == self.movie.Running:
|
if self.movie.state() == self.movie.Running:
|
||||||
self.movie.setPaused(True)
|
self.movie.setPaused(True)
|
||||||
self.setVisible(False)
|
self.setVisible(False)
|
||||||
|
|
||||||
|
|
||||||
class WarningDialog(QDialog, Ui_WarningDialog):
|
class WarningDialog(QDialog, Ui_WarningDialog):
|
||||||
|
|
||||||
def __init__(self, title, msg, details, parent=None):
|
def __init__(self, title, msg, details, parent=None):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
@ -67,20 +67,20 @@ class WarningDialog(QDialog, Ui_WarningDialog):
|
|||||||
self.details.setText(details)
|
self.details.setText(details)
|
||||||
|
|
||||||
class FilenamePattern(QWidget, Ui_Form):
|
class FilenamePattern(QWidget, Ui_Form):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.connect(self.test_button, SIGNAL('clicked()'), self.do_test)
|
self.connect(self.test_button, SIGNAL('clicked()'), self.do_test)
|
||||||
self.connect(self.re, SIGNAL('returnPressed()'), self.do_test)
|
self.connect(self.re, SIGNAL('returnPressed()'), self.do_test)
|
||||||
self.re.setText(prefs['filename_pattern'])
|
self.re.setText(prefs['filename_pattern'])
|
||||||
|
|
||||||
def do_test(self):
|
def do_test(self):
|
||||||
try:
|
try:
|
||||||
pat = self.pattern()
|
pat = self.pattern()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
error_dialog(self, _('Invalid regular expression'),
|
error_dialog(self, _('Invalid regular expression'),
|
||||||
_('Invalid regular expression: %s')%err).exec_()
|
_('Invalid regular expression: %s')%err).exec_()
|
||||||
return
|
return
|
||||||
mi = metadata_from_filename(qstring_to_unicode(self.filename.text()), pat)
|
mi = metadata_from_filename(qstring_to_unicode(self.filename.text()), pat)
|
||||||
@ -92,49 +92,49 @@ class FilenamePattern(QWidget, Ui_Form):
|
|||||||
self.authors.setText(', '.join(mi.authors))
|
self.authors.setText(', '.join(mi.authors))
|
||||||
else:
|
else:
|
||||||
self.authors.setText(_('No match'))
|
self.authors.setText(_('No match'))
|
||||||
|
|
||||||
if mi.series:
|
if mi.series:
|
||||||
self.series.setText(mi.series)
|
self.series.setText(mi.series)
|
||||||
else:
|
else:
|
||||||
self.series.setText(_('No match'))
|
self.series.setText(_('No match'))
|
||||||
|
|
||||||
if mi.series_index is not None:
|
if mi.series_index is not None:
|
||||||
self.series_index.setText(str(mi.series_index))
|
self.series_index.setText(str(mi.series_index))
|
||||||
else:
|
else:
|
||||||
self.series_index.setText(_('No match'))
|
self.series_index.setText(_('No match'))
|
||||||
|
|
||||||
self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn))
|
self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn))
|
||||||
|
|
||||||
|
|
||||||
def pattern(self):
|
def pattern(self):
|
||||||
pat = qstring_to_unicode(self.re.text())
|
pat = qstring_to_unicode(self.re.text())
|
||||||
return re.compile(pat)
|
return re.compile(pat)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
pat = self.pattern().pattern
|
pat = self.pattern().pattern
|
||||||
prefs['filename_pattern'] = pat
|
prefs['filename_pattern'] = pat
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ImageView(QLabel):
|
class ImageView(QLabel):
|
||||||
|
|
||||||
MAX_WIDTH = 400
|
MAX_WIDTH = 400
|
||||||
MAX_HEIGHT = 300
|
MAX_HEIGHT = 300
|
||||||
DROPABBLE_EXTENSIONS = ('jpg', 'jpeg', 'gif', 'png', 'bmp')
|
DROPABBLE_EXTENSIONS = ('jpg', 'jpeg', 'gif', 'png', 'bmp')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def paths_from_event(cls, event):
|
def paths_from_event(cls, event):
|
||||||
'''
|
'''
|
||||||
Accept a drop event and return a list of paths that can be read from
|
Accept a drop event and return a list of paths that can be read from
|
||||||
and represent files with extensions.
|
and represent files with extensions.
|
||||||
'''
|
'''
|
||||||
if event.mimeData().hasFormat('text/uri-list'):
|
if event.mimeData().hasFormat('text/uri-list'):
|
||||||
urls = [qstring_to_unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
urls = [qstring_to_unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
||||||
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
||||||
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
|
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
if int(event.possibleActions() & Qt.CopyAction) + \
|
||||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
int(event.possibleActions() & Qt.MoveAction) == 0:
|
||||||
@ -142,7 +142,7 @@ class ImageView(QLabel):
|
|||||||
paths = self.paths_from_event(event)
|
paths = self.paths_from_event(event)
|
||||||
if paths:
|
if paths:
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
|
||||||
def dropEvent(self, event):
|
def dropEvent(self, event):
|
||||||
paths = self.paths_from_event(event)
|
paths = self.paths_from_event(event)
|
||||||
event.setDropAction(Qt.CopyAction)
|
event.setDropAction(Qt.CopyAction)
|
||||||
@ -154,19 +154,19 @@ class ImageView(QLabel):
|
|||||||
event.accept()
|
event.accept()
|
||||||
self.emit(SIGNAL('cover_changed()'), paths, Qt.QueuedConnection)
|
self.emit(SIGNAL('cover_changed()'), paths, Qt.QueuedConnection)
|
||||||
break
|
break
|
||||||
|
|
||||||
def dragMoveEvent(self, event):
|
def dragMoveEvent(self, event):
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
|
||||||
def setPixmap(self, pixmap):
|
def setPixmap(self, pixmap):
|
||||||
QLabel.setPixmap(self, pixmap)
|
QLabel.setPixmap(self, pixmap)
|
||||||
width, height = fit_image(pixmap.width(), pixmap.height(), self.MAX_WIDTH, self.MAX_HEIGHT)[1:]
|
width, height = fit_image(pixmap.width(), pixmap.height(), self.MAX_WIDTH, self.MAX_HEIGHT)[1:]
|
||||||
self.setMaximumWidth(width)
|
self.setMaximumWidth(width)
|
||||||
self.setMaximumHeight(height)
|
self.setMaximumHeight(height)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LocationModel(QAbstractListModel):
|
class LocationModel(QAbstractListModel):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
QAbstractListModel.__init__(self, parent)
|
QAbstractListModel.__init__(self, parent)
|
||||||
self.icons = [QVariant(QIcon(':/library')),
|
self.icons = [QVariant(QIcon(':/library')),
|
||||||
@ -182,19 +182,19 @@ class LocationModel(QAbstractListModel):
|
|||||||
_('Click to see the list of books available on your computer'),
|
_('Click to see the list of books available on your computer'),
|
||||||
_('Click to see the list of books in the main memory of your reader'),
|
_('Click to see the list of books in the main memory of your reader'),
|
||||||
_('Click to see the list of books on the storage card in your reader')
|
_('Click to see the list of books on the storage card in your reader')
|
||||||
]
|
]
|
||||||
|
|
||||||
def rowCount(self, parent):
|
def rowCount(self, parent):
|
||||||
return 1 + sum([1 for i in self.free if i >= 0])
|
return 1 + sum([1 for i in self.free if i >= 0])
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
row = index.row()
|
row = index.row()
|
||||||
data = NONE
|
data = NONE
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
text = self.text[row]%(human_readable(self.free[row-1])) if row > 0 \
|
text = self.text[row]%(human_readable(self.free[row-1])) if row > 0 \
|
||||||
else self.text[row]%self.count
|
else self.text[row]%self.count
|
||||||
data = QVariant(text)
|
data = QVariant(text)
|
||||||
elif role == Qt.DecorationRole:
|
elif role == Qt.DecorationRole:
|
||||||
data = self.icons[row]
|
data = self.icons[row]
|
||||||
elif role == Qt.ToolTipRole:
|
elif role == Qt.ToolTipRole:
|
||||||
data = QVariant(self.tooltips[row])
|
data = QVariant(self.tooltips[row])
|
||||||
@ -205,59 +205,62 @@ class LocationModel(QAbstractListModel):
|
|||||||
font.setBold(row == self.highlight_row)
|
font.setBold(row == self.highlight_row)
|
||||||
data = QVariant(font)
|
data = QVariant(font)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def update_devices(self, cp=None, fs=[-1, -1, -1]):
|
def update_devices(self, cp=None, fs=[-1, -1, -1]):
|
||||||
self.free[0] = fs[0]
|
self.free[0] = fs[0]
|
||||||
self.free[1] = max(fs[1:])
|
self.free[1] = max(fs[1:])
|
||||||
if cp == None:
|
if cp == None:
|
||||||
self.free[1] = -1
|
self.free[1] = -1
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def location_changed(self, row):
|
|
||||||
self.highlight_row = row
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
|
def location_changed(self, row):
|
||||||
|
self.highlight_row = row
|
||||||
|
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
|
||||||
|
self.index(0), self.index(self.rowCount(QModelIndex())-1))
|
||||||
|
|
||||||
class LocationView(QListView):
|
class LocationView(QListView):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
QListView.__init__(self, parent)
|
QListView.__init__(self, parent)
|
||||||
self.setModel(LocationModel(self))
|
self.setModel(LocationModel(self))
|
||||||
self.reset()
|
self.reset()
|
||||||
QObject.connect(self.selectionModel(), SIGNAL('currentChanged(QModelIndex, QModelIndex)'), self.current_changed)
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
self.setCursor(Qt.PointingHandCursor)
|
self.currentChanged = self.current_changed
|
||||||
|
|
||||||
def count_changed(self, new_count):
|
def count_changed(self, new_count):
|
||||||
self.model().count = new_count
|
self.model().count = new_count
|
||||||
self.model().reset()
|
self.model().reset()
|
||||||
|
|
||||||
def current_changed(self, current, previous):
|
def current_changed(self, current, previous):
|
||||||
i = current.row()
|
if current.isValid():
|
||||||
location = 'library' if i == 0 else 'main' if i == 1 else 'card'
|
i = current.row()
|
||||||
self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location)
|
location = 'library' if i == 0 else 'main' if i == 1 else 'card'
|
||||||
|
self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location)
|
||||||
|
self.model().location_changed(i)
|
||||||
|
|
||||||
def location_changed(self, row):
|
def location_changed(self, row):
|
||||||
if 0 <= row and row <= 2:
|
if 0 <= row and row <= 2:
|
||||||
self.model().location_changed(row)
|
self.model().location_changed(row)
|
||||||
|
|
||||||
class JobsView(TableView):
|
class JobsView(TableView):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
TableView.__init__(self, parent)
|
TableView.__init__(self, parent)
|
||||||
self.connect(self, SIGNAL('doubleClicked(QModelIndex)'), self.show_details)
|
self.connect(self, SIGNAL('doubleClicked(QModelIndex)'), self.show_details)
|
||||||
|
|
||||||
def show_details(self, index):
|
def show_details(self, index):
|
||||||
row = index.row()
|
row = index.row()
|
||||||
job = self.model().row_to_job(row)
|
job = self.model().row_to_job(row)
|
||||||
d = DetailView(self, job)
|
d = DetailView(self, job)
|
||||||
self.connect(self.model(), SIGNAL('output_received()'), d.update)
|
self.connect(self.model(), SIGNAL('output_received()'), d.update)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
|
|
||||||
class FontFamilyModel(QAbstractListModel):
|
class FontFamilyModel(QAbstractListModel):
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QAbstractListModel.__init__(self, *args)
|
QAbstractListModel.__init__(self, *args)
|
||||||
try:
|
try:
|
||||||
@ -268,10 +271,10 @@ class FontFamilyModel(QAbstractListModel):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.families.sort()
|
self.families.sort()
|
||||||
self.families[:0] = ['None']
|
self.families[:0] = ['None']
|
||||||
|
|
||||||
def rowCount(self, *args):
|
def rowCount(self, *args):
|
||||||
return len(self.families)
|
return len(self.families)
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
try:
|
try:
|
||||||
family = self.families[index.row()]
|
family = self.families[index.row()]
|
||||||
@ -283,52 +286,52 @@ class FontFamilyModel(QAbstractListModel):
|
|||||||
if role == Qt.FontRole:
|
if role == Qt.FontRole:
|
||||||
return QVariant(QFont(family))
|
return QVariant(QFont(family))
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def index_of(self, family):
|
def index_of(self, family):
|
||||||
return self.families.index(family.strip())
|
return self.families.index(family.strip())
|
||||||
|
|
||||||
|
|
||||||
class BasicListItem(QListWidgetItem):
|
class BasicListItem(QListWidgetItem):
|
||||||
|
|
||||||
def __init__(self, text, user_data=None):
|
def __init__(self, text, user_data=None):
|
||||||
QListWidgetItem.__init__(self, text)
|
QListWidgetItem.__init__(self, text)
|
||||||
self.user_data = user_data
|
self.user_data = user_data
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if hasattr(other, 'text'):
|
if hasattr(other, 'text'):
|
||||||
return self.text() == other.text()
|
return self.text() == other.text()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class BasicList(QListWidget):
|
class BasicList(QListWidget):
|
||||||
|
|
||||||
def add_item(self, text, user_data=None, replace=False):
|
def add_item(self, text, user_data=None, replace=False):
|
||||||
item = BasicListItem(text, user_data)
|
item = BasicListItem(text, user_data)
|
||||||
|
|
||||||
for oitem in self.items():
|
for oitem in self.items():
|
||||||
if oitem == item:
|
if oitem == item:
|
||||||
if replace:
|
if replace:
|
||||||
self.takeItem(self.row(oitem))
|
self.takeItem(self.row(oitem))
|
||||||
else:
|
else:
|
||||||
raise ValueError('Item already in list')
|
raise ValueError('Item already in list')
|
||||||
|
|
||||||
self.addItem(item)
|
self.addItem(item)
|
||||||
|
|
||||||
def remove_selected_items(self, *args):
|
def remove_selected_items(self, *args):
|
||||||
for item in self.selectedItems():
|
for item in self.selectedItems():
|
||||||
self.takeItem(self.row(item))
|
self.takeItem(self.row(item))
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
for i in range(self.count()):
|
for i in range(self.count()):
|
||||||
yield self.item(i)
|
yield self.item(i)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PythonHighlighter(QSyntaxHighlighter):
|
class PythonHighlighter(QSyntaxHighlighter):
|
||||||
|
|
||||||
Rules = []
|
Rules = []
|
||||||
Formats = {}
|
Formats = {}
|
||||||
Config = {}
|
Config = {}
|
||||||
|
|
||||||
KEYWORDS = ["and", "as", "assert", "break", "class", "continue", "def",
|
KEYWORDS = ["and", "as", "assert", "break", "class", "continue", "def",
|
||||||
"del", "elif", "else", "except", "exec", "finally", "for", "from",
|
"del", "elif", "else", "except", "exec", "finally", "for", "from",
|
||||||
"global", "if", "import", "in", "is", "lambda", "not", "or",
|
"global", "if", "import", "in", "is", "lambda", "not", "or",
|
||||||
@ -344,7 +347,7 @@ class PythonHighlighter(QSyntaxHighlighter):
|
|||||||
"open", "ord", "pow", "property", "range", "reduce", "repr",
|
"open", "ord", "pow", "property", "range", "reduce", "repr",
|
||||||
"reversed", "round", "set", "setattr", "slice", "sorted",
|
"reversed", "round", "set", "setattr", "slice", "sorted",
|
||||||
"staticmethod", "str", "sum", "super", "tuple", "type", "unichr",
|
"staticmethod", "str", "sum", "super", "tuple", "type", "unichr",
|
||||||
"unicode", "vars", "xrange", "zip"]
|
"unicode", "vars", "xrange", "zip"]
|
||||||
|
|
||||||
CONSTANTS = ["False", "True", "None", "NotImplemented", "Ellipsis"]
|
CONSTANTS = ["False", "True", "None", "NotImplemented", "Ellipsis"]
|
||||||
|
|
||||||
@ -353,7 +356,7 @@ class PythonHighlighter(QSyntaxHighlighter):
|
|||||||
super(PythonHighlighter, self).__init__(parent)
|
super(PythonHighlighter, self).__init__(parent)
|
||||||
if not self.Config:
|
if not self.Config:
|
||||||
self.loadConfig()
|
self.loadConfig()
|
||||||
|
|
||||||
|
|
||||||
self.initializeFormats()
|
self.initializeFormats()
|
||||||
|
|
||||||
@ -392,7 +395,7 @@ class PythonHighlighter(QSyntaxHighlighter):
|
|||||||
if value.isEmpty():
|
if value.isEmpty():
|
||||||
value = default
|
value = default
|
||||||
Config[name] = value
|
Config[name] = value
|
||||||
|
|
||||||
for name in ("window", "shell"):
|
for name in ("window", "shell"):
|
||||||
Config["%swidth" % name] = settings.value("%swidth" % name,
|
Config["%swidth" % name] = settings.value("%swidth" % name,
|
||||||
QVariant(QApplication.desktop() \
|
QVariant(QApplication.desktop() \
|
||||||
@ -421,9 +424,9 @@ class PythonHighlighter(QSyntaxHighlighter):
|
|||||||
sys.stdout = codecs.getwriter("UTF8")(sys.stdout)""")
|
sys.stdout = codecs.getwriter("UTF8")(sys.stdout)""")
|
||||||
setDefaultString("newfile", """\
|
setDefaultString("newfile", """\
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
""")
|
""")
|
||||||
Config["backupsuffix"] = settings.value("backupsuffix",
|
Config["backupsuffix"] = settings.value("backupsuffix",
|
||||||
@ -435,7 +438,7 @@ class PythonHighlighter(QSyntaxHighlighter):
|
|||||||
QVariant(150)).toInt()[0]
|
QVariant(150)).toInt()[0]
|
||||||
Config["maxlinestoscan"] = settings.value("maxlinestoscan",
|
Config["maxlinestoscan"] = settings.value("maxlinestoscan",
|
||||||
QVariant(5000)).toInt()[0]
|
QVariant(5000)).toInt()[0]
|
||||||
Config["pythondocpath"] = settings.value("pythondocpath",
|
Config["pythondocpath"] = settings.value("pythondocpath",
|
||||||
QVariant("http://docs.python.org")).toString()
|
QVariant("http://docs.python.org")).toString()
|
||||||
Config["autohidefinddialog"] = settings.value("autohidefinddialog",
|
Config["autohidefinddialog"] = settings.value("autohidefinddialog",
|
||||||
QVariant(True)).toBool()
|
QVariant(True)).toBool()
|
||||||
@ -548,7 +551,7 @@ class PythonHighlighter(QSyntaxHighlighter):
|
|||||||
if i == -1:
|
if i == -1:
|
||||||
i = text.length()
|
i = text.length()
|
||||||
self.setCurrentBlockState(state)
|
self.setCurrentBlockState(state)
|
||||||
self.setFormat(0, i + 3,
|
self.setFormat(0, i + 3,
|
||||||
PythonHighlighter.Formats["string"])
|
PythonHighlighter.Formats["string"])
|
||||||
elif i > -1:
|
elif i > -1:
|
||||||
self.setCurrentBlockState(state)
|
self.setCurrentBlockState(state)
|
||||||
|
@ -25,7 +25,8 @@ if iswindows:
|
|||||||
else:
|
else:
|
||||||
Structure = _Structure
|
Structure = _Structure
|
||||||
if hasattr(sys, 'frozen') and iswindows:
|
if hasattr(sys, 'frozen') and iswindows:
|
||||||
_libunrar = cdll.LoadLibrary(os.path.join(os.path.dirname(sys.executable), 'unrar.dll'))
|
_libunrar = cdll.LoadLibrary(os.path.join(os.path.dirname(sys.executable),
|
||||||
|
'unrar.dll'))
|
||||||
_libunrar = load_library(_librar_name, cdll)
|
_libunrar = load_library(_librar_name, cdll)
|
||||||
|
|
||||||
RAR_OM_LIST = 0
|
RAR_OM_LIST = 0
|
||||||
@ -95,7 +96,7 @@ class RARHeaderDataEx(Structure):
|
|||||||
|
|
||||||
# Define a callback function
|
# Define a callback function
|
||||||
#CALLBACK_FUNC = CFUNCTYPE(c_int, c_uint, c_long, c_char_p, c_long)
|
#CALLBACK_FUNC = CFUNCTYPE(c_int, c_uint, c_long, c_char_p, c_long)
|
||||||
#def py_callback_func(msg, user_data, p1, p2):
|
#def py_callback_func(msg, user_data, p1, p2):
|
||||||
# return 0
|
# return 0
|
||||||
|
|
||||||
#callback_func = CALLBACK_FUNC(py_callback_func)
|
#callback_func = CALLBACK_FUNC(py_callback_func)
|
||||||
@ -123,7 +124,7 @@ def _interpret_open_error(code, path):
|
|||||||
elif code == ERAR_EOPEN:
|
elif code == ERAR_EOPEN:
|
||||||
msg = 'Cannot open ' + path
|
msg = 'Cannot open ' + path
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def _interpret_process_file_error(code):
|
def _interpret_process_file_error(code):
|
||||||
msg = 'Unknown Error'
|
msg = 'Unknown Error'
|
||||||
if code == ERAR_UNKNOWN_FORMAT:
|
if code == ERAR_UNKNOWN_FORMAT:
|
||||||
@ -145,7 +146,7 @@ def _interpret_process_file_error(code):
|
|||||||
elif code == ERAR_MISSING_PASSWORD:
|
elif code == ERAR_MISSING_PASSWORD:
|
||||||
msg = 'Password is required.'
|
msg = 'Password is required.'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def get_archive_info(flags):
|
def get_archive_info(flags):
|
||||||
ios = StringIO()
|
ios = StringIO()
|
||||||
print >>ios, 'Volume:\t\t', 'yes' if (flags & 1) else 'no'
|
print >>ios, 'Volume:\t\t', 'yes' if (flags & 1) else 'no'
|
||||||
@ -162,7 +163,7 @@ def get_archive_info(flags):
|
|||||||
def extract(path, dir):
|
def extract(path, dir):
|
||||||
"""
|
"""
|
||||||
Extract archive C{filename} into directory C{dir}
|
Extract archive C{filename} into directory C{dir}
|
||||||
"""
|
"""
|
||||||
open_archive_data = RAROpenArchiveDataEx(ArcName=path, OpenMode=RAR_OM_EXTRACT, CmtBuf=None)
|
open_archive_data = RAROpenArchiveDataEx(ArcName=path, OpenMode=RAR_OM_EXTRACT, CmtBuf=None)
|
||||||
arc_data = _libunrar.RAROpenArchiveEx(byref(open_archive_data))
|
arc_data = _libunrar.RAROpenArchiveEx(byref(open_archive_data))
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -173,7 +174,7 @@ def extract(path, dir):
|
|||||||
if open_archive_data.OpenResult != 0:
|
if open_archive_data.OpenResult != 0:
|
||||||
raise UnRARException(_interpret_open_error(open_archive_data.OpenResult, path))
|
raise UnRARException(_interpret_open_error(open_archive_data.OpenResult, path))
|
||||||
print 'Archive:', path
|
print 'Archive:', path
|
||||||
#print get_archive_info(open_archive_data.Flags)
|
#print get_archive_info(open_archive_data.Flags)
|
||||||
header_data = RARHeaderDataEx(CmtBuf=None)
|
header_data = RARHeaderDataEx(CmtBuf=None)
|
||||||
#_libunrar.RARSetCallback(arc_data, callback_func, mode)
|
#_libunrar.RARSetCallback(arc_data, callback_func, mode)
|
||||||
while True:
|
while True:
|
||||||
@ -240,5 +241,5 @@ def extract_member(path, match=re.compile(r'\.(jpg|jpeg|gif|png)\s*$', re.I), na
|
|||||||
open(os.path.join(dir, *header_data.FileNameW.split('/')), 'rb').read()
|
open(os.path.join(dir, *header_data.FileNameW.split('/')), 'rb').read()
|
||||||
finally:
|
finally:
|
||||||
_libunrar.RARCloseArchive(arc_data)
|
_libunrar.RARCloseArchive(arc_data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,14 +61,15 @@ entry_points = {
|
|||||||
'comic2lrf = calibre.ebooks.lrf.comic.convert_from:main',
|
'comic2lrf = calibre.ebooks.lrf.comic.convert_from:main',
|
||||||
'comic2epub = calibre.ebooks.epub.from_comic:main',
|
'comic2epub = calibre.ebooks.epub.from_comic:main',
|
||||||
'comic2mobi = calibre.ebooks.mobi.from_comic:main',
|
'comic2mobi = calibre.ebooks.mobi.from_comic:main',
|
||||||
'comic2pdf = calibre.ebooks.pdf.from_comic:main',
|
'comic2pdf = calibre.ebooks.pdf.from_comic:main',
|
||||||
'calibre-debug = calibre.debug:main',
|
'calibre-debug = calibre.debug:main',
|
||||||
'calibredb = calibre.library.cli:main',
|
'calibredb = calibre.library.cli:main',
|
||||||
'calibre-fontconfig = calibre.utils.fontconfig:main',
|
'calibre-fontconfig = calibre.utils.fontconfig:main',
|
||||||
'calibre-parallel = calibre.parallel:main',
|
'calibre-parallel = calibre.parallel:main',
|
||||||
'calibre-customize = calibre.customize.ui:main',
|
'calibre-customize = calibre.customize.ui:main',
|
||||||
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
|
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
|
||||||
'fetch-ebook-metadata = calibre.ebooks.metadata.fetch:main',
|
'fetch-ebook-metadata = calibre.ebooks.metadata.fetch:main',
|
||||||
|
'calibre-smtp = calibre.utils.smtp:main',
|
||||||
],
|
],
|
||||||
'gui_scripts' : [
|
'gui_scripts' : [
|
||||||
__appname__+' = calibre.gui2.main:main',
|
__appname__+' = calibre.gui2.main:main',
|
||||||
@ -198,6 +199,7 @@ def setup_completion(fatal_errors):
|
|||||||
from calibre.ebooks.metadata.fetch import option_parser as fem_op
|
from calibre.ebooks.metadata.fetch import option_parser as fem_op
|
||||||
from calibre.ebooks.mobi.writer import option_parser as oeb2mobi
|
from calibre.ebooks.mobi.writer import option_parser as oeb2mobi
|
||||||
from calibre.gui2.main import option_parser as guiop
|
from calibre.gui2.main import option_parser as guiop
|
||||||
|
from calibre.utils.email import option_parser as smtp_op
|
||||||
any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip',
|
any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip',
|
||||||
'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt']
|
'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt']
|
||||||
f = open_file('/etc/bash_completion.d/libprs500')
|
f = open_file('/etc/bash_completion.d/libprs500')
|
||||||
@ -246,6 +248,7 @@ def setup_completion(fatal_errors):
|
|||||||
f.write(opts_and_words('feeds2epub', feeds2epub, feed_titles))
|
f.write(opts_and_words('feeds2epub', feeds2epub, feed_titles))
|
||||||
f.write(opts_and_words('feeds2mobi', feeds2mobi, feed_titles))
|
f.write(opts_and_words('feeds2mobi', feeds2mobi, feed_titles))
|
||||||
f.write(opts_and_words('fetch-ebook-metadata', fem_op, []))
|
f.write(opts_and_words('fetch-ebook-metadata', fem_op, []))
|
||||||
|
f.write(opts_and_words('calibre-smtp', smtp_op, []))
|
||||||
f.write(opts_and_exts('html2epub', html2epub, ['html', 'htm', 'xhtm', 'xhtml', 'opf']))
|
f.write(opts_and_exts('html2epub', html2epub, ['html', 'htm', 'xhtm', 'xhtml', 'opf']))
|
||||||
f.write(opts_and_exts('html2oeb', html2oeb, ['html', 'htm', 'xhtm', 'xhtml']))
|
f.write(opts_and_exts('html2oeb', html2oeb, ['html', 'htm', 'xhtm', 'xhtml']))
|
||||||
f.write(opts_and_exts('odt2oeb', odt2oeb, ['odt']))
|
f.write(opts_and_exts('odt2oeb', odt2oeb, ['odt']))
|
||||||
|
@ -45,7 +45,7 @@ PARALLEL_FUNCS = {
|
|||||||
('calibre.gui2.lrf_renderer.main', 'main', {}, None),
|
('calibre.gui2.lrf_renderer.main', 'main', {}, None),
|
||||||
|
|
||||||
'ebook-viewer' :
|
'ebook-viewer' :
|
||||||
('calibre.gui2.viewer.main', 'main', {}, None),
|
('calibre.gui2.viewer.main', 'main', {}, None),
|
||||||
|
|
||||||
'feeds2lrf' :
|
'feeds2lrf' :
|
||||||
('calibre.ebooks.lrf.feeds.convert_from', 'main', {}, 'notification'),
|
('calibre.ebooks.lrf.feeds.convert_from', 'main', {}, 'notification'),
|
||||||
|
@ -13,7 +13,7 @@ def cleanup(path):
|
|||||||
try:
|
try:
|
||||||
import os
|
import os
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ DEPENDENCIES = [
|
|||||||
('lxml', '2.1.5', 'lxml', 'python-lxml', 'python-lxml'),
|
('lxml', '2.1.5', 'lxml', 'python-lxml', 'python-lxml'),
|
||||||
('python-dateutil', '1.4.1', 'python-dateutil', 'python-dateutil', 'python-dateutil'),
|
('python-dateutil', '1.4.1', 'python-dateutil', 'python-dateutil', 'python-dateutil'),
|
||||||
('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'),
|
('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'),
|
||||||
|
('dnspython', '1.6.0', 'dnspython', 'dnspython', 'dnspython', 'dnspython'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
232
src/calibre/utils/smtp.py
Normal file
232
src/calibre/utils/smtp.py
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
'''
|
||||||
|
This module implements a simple commandline SMTP client that supports:
|
||||||
|
|
||||||
|
* Delivery via an SMTP relay with SSL or TLS
|
||||||
|
* Background delivery with failures being saved in a maildir mailbox
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys, traceback, os
|
||||||
|
from email import encoders
|
||||||
|
|
||||||
|
def create_mail(from_, to, subject, text=None, attachment_data=None,
|
||||||
|
attachment_type=None, attachment_name=None):
|
||||||
|
assert text or attachment_data
|
||||||
|
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
||||||
|
outer = MIMEMultipart()
|
||||||
|
outer['Subject'] = subject
|
||||||
|
outer['To'] = to
|
||||||
|
outer['From'] = from_
|
||||||
|
outer.preamble = 'You will not see this in a MIME-aware mail reader.\n'
|
||||||
|
|
||||||
|
if text is not None:
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
msg = MIMEText(text)
|
||||||
|
outer.attach(msg)
|
||||||
|
|
||||||
|
if attachment_data is not None:
|
||||||
|
from email.mime.base import MIMEBase
|
||||||
|
assert attachment_data and attachment_name
|
||||||
|
maintype, subtype = attachment_type.split('/', 1)
|
||||||
|
msg = MIMEBase(maintype, subtype)
|
||||||
|
msg.set_payload(attachment_data)
|
||||||
|
encoders.encode_base64(msg)
|
||||||
|
msg.add_header('Content-Disposition', 'attachment',
|
||||||
|
filename=attachment_name)
|
||||||
|
outer.attach(msg)
|
||||||
|
|
||||||
|
return outer.as_string()
|
||||||
|
|
||||||
|
def get_mx(host):
|
||||||
|
import dns.resolver
|
||||||
|
answers = list(dns.resolver.query(host, 'MX'))
|
||||||
|
answers.sort(cmp=lambda x, y: cmp(int(x.preference), int(y.preference)))
|
||||||
|
return [str(x.exchange) for x in answers]
|
||||||
|
|
||||||
|
def sendmail_direct(from_, to, msg, timeout, localhost, verbose):
|
||||||
|
import smtplib
|
||||||
|
s = smtplib.SMTP(timeout=timeout, local_hostname=localhost)
|
||||||
|
s.set_debuglevel(verbose)
|
||||||
|
hosts = get_mx(to.split('@')[-1].strip())
|
||||||
|
if not hosts:
|
||||||
|
raise ValueError('No mail server found for address: %s'%to)
|
||||||
|
last_error = last_traceback = None
|
||||||
|
for host in hosts:
|
||||||
|
try:
|
||||||
|
s.connect(host, 25)
|
||||||
|
s.sendmail(from_, [to], msg)
|
||||||
|
return s.quit()
|
||||||
|
except Exception, e:
|
||||||
|
last_error, last_traceback = e, traceback.format_exc()
|
||||||
|
if last_error is not None:
|
||||||
|
print last_traceback
|
||||||
|
raise IOError('Failed to send mail: '+repr(last_error))
|
||||||
|
|
||||||
|
|
||||||
|
def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30,
|
||||||
|
relay=None, username=None, password=None, encryption='TLS',
|
||||||
|
port=-1):
|
||||||
|
if relay is None:
|
||||||
|
for x in to:
|
||||||
|
return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
|
||||||
|
import smtplib
|
||||||
|
cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
|
||||||
|
s = cls(timeout=timeout, local_hostname=localhost)
|
||||||
|
s.set_debuglevel(verbose)
|
||||||
|
if port < 0:
|
||||||
|
port = 25 if encryption == 'TLS' else 465
|
||||||
|
s.connect(relay, port)
|
||||||
|
if encryption == 'TLS':
|
||||||
|
s.starttls()
|
||||||
|
s.ehlo()
|
||||||
|
if username is not None and password is not None:
|
||||||
|
s.login(username, password)
|
||||||
|
s.sendmail(from_, to, msg)
|
||||||
|
return s.quit()
|
||||||
|
|
||||||
|
def option_parser():
|
||||||
|
try:
|
||||||
|
from calibre.utils.config import OptionParser
|
||||||
|
OptionParser
|
||||||
|
except ImportError:
|
||||||
|
from optparse import OptionParser
|
||||||
|
import textwrap
|
||||||
|
parser = OptionParser(textwrap.dedent('''\
|
||||||
|
%prog [options] [from to text]
|
||||||
|
|
||||||
|
Send mail using the SMTP protocol. %prog has two modes of operation. In the
|
||||||
|
compose mode you specify from to and text and these are used to build and
|
||||||
|
send an email message. In the filter mode, %prog reads a complete email
|
||||||
|
message from STDIN and sends it.
|
||||||
|
|
||||||
|
text is the body of the email message.
|
||||||
|
If text is not specified, a complete email message is read from STDIN.
|
||||||
|
from is the email address of the sender and to is the email address
|
||||||
|
of the recipient. When a complete email is read from STDIN, from and to
|
||||||
|
are only used in the SMTP negotiation, the message headers are not modified.
|
||||||
|
'''))
|
||||||
|
c=parser.add_option_group('COMPOSE MAIL',
|
||||||
|
'Options to compose an email. Ignored if text is not specified').add_option
|
||||||
|
c('-a', '--attachment', help='File to attach to the email')
|
||||||
|
c('-s', '--subject', help='Subject of the email')
|
||||||
|
|
||||||
|
parser.add_option('-l', '--localhost',
|
||||||
|
help=('Host name of localhost. Used when connecting '
|
||||||
|
'to SMTP server.'))
|
||||||
|
r=parser.add_option_group('SMTP RELAY',
|
||||||
|
'Options to use an SMTP relay server to send mail. '
|
||||||
|
'%prog will try to send the email directly unless --relay is '
|
||||||
|
'specified.').add_option
|
||||||
|
r('-r', '--relay', help=('An SMTP relay server to use to send mail.'))
|
||||||
|
r('-p', '--port', default=-1,
|
||||||
|
help='Port to connect to on relay server. Default is to use 465 if '
|
||||||
|
'encryption method is SSL and 25 otherwise.')
|
||||||
|
r('-u', '--username', help='Username for relay')
|
||||||
|
r('-p', '--password', help='Password for relay')
|
||||||
|
r('-e', '--encryption-method', default='TLS',
|
||||||
|
choices=['TLS', 'SSL'],
|
||||||
|
help='Encryption method to use when connecting to relay. Choices are '
|
||||||
|
'TLS and SSL. Default is TLS.')
|
||||||
|
parser.add_option('-o', '--outbox', help='Path to maildir folder to store '
|
||||||
|
'failed email messages in.')
|
||||||
|
parser.add_option('-f', '--fork', default=False, action='store_true',
|
||||||
|
help='Fork and deliver message in background. '
|
||||||
|
'If you use this option, you should also use --outbox '
|
||||||
|
'to handle delivery failures.')
|
||||||
|
parser.add_option('-t', '--timeout', help='Timeout for connection')
|
||||||
|
parser.add_option('-v', '--verbose', default=0, action='count',
|
||||||
|
help='Be more verbose')
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def extract_email_address(raw):
|
||||||
|
from email.utils import parseaddr
|
||||||
|
return parseaddr(raw)[-1]
|
||||||
|
|
||||||
|
def compose_mail(from_, to, text, subject=None, attachment=None,
|
||||||
|
attachment_name=None):
|
||||||
|
attachment_type = attachment_data = None
|
||||||
|
if attachment is not None:
|
||||||
|
try:
|
||||||
|
from calibre import guess_type
|
||||||
|
guess_type
|
||||||
|
except ImportError:
|
||||||
|
from mimetypes import guess_type
|
||||||
|
attachment_data = attachment.read() if hasattr(attachment, 'read') \
|
||||||
|
else open(attachment, 'rb').read()
|
||||||
|
attachment_type = guess_type(getattr(attachment, 'name', attachment))[0]
|
||||||
|
if attachment_name is None:
|
||||||
|
attachment_name = os.path.basename(getattr(attachment,
|
||||||
|
'name', attachment))
|
||||||
|
subject = subject if subject else 'no subject'
|
||||||
|
return create_mail(from_, to, subject, text=text,
|
||||||
|
attachment_data=attachment_data, attachment_type=attachment_type,
|
||||||
|
attachment_name=attachment_name)
|
||||||
|
|
||||||
|
def main(args=sys.argv):
|
||||||
|
parser = option_parser()
|
||||||
|
opts, args = parser.parse_args(args)
|
||||||
|
|
||||||
|
|
||||||
|
if len(args) > 1:
|
||||||
|
msg = compose_mail(args[1], args[2], args[3], subject=opts.subject,
|
||||||
|
attachment=opts.attachment)
|
||||||
|
from_, to = args[1:3]
|
||||||
|
efrom, eto = map(extract_email_address, (from_, to))
|
||||||
|
eto = [eto]
|
||||||
|
else:
|
||||||
|
msg = sys.stdin.read()
|
||||||
|
from email.parser import Parser
|
||||||
|
from email.utils import getaddresses
|
||||||
|
eml = Parser.parsestr(msg, headersonly=True)
|
||||||
|
tos = eml.get_all('to', [])
|
||||||
|
ccs = eml.get_all('cc', [])
|
||||||
|
eto = getaddresses(tos + ccs)
|
||||||
|
if not eto:
|
||||||
|
raise ValueError('Email from STDIN does not specify any recipients')
|
||||||
|
efrom = getaddresses(eml.get_all('from', []))
|
||||||
|
if not efrom:
|
||||||
|
raise ValueError('Email from STDIN does not specify a sender')
|
||||||
|
efrom = efrom[0]
|
||||||
|
|
||||||
|
|
||||||
|
outbox = None
|
||||||
|
if opts.outbox is not None:
|
||||||
|
outbox = os.path.abspath(os.path.expanduser(opts.outbox))
|
||||||
|
from mailbox import Maildir
|
||||||
|
outbox = Maildir(opts.outbox, factory=None)
|
||||||
|
if opts.fork:
|
||||||
|
if os.fork() != 0:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
sendmail(msg, efrom, eto, localhost=opts.localhost, verbose=opts.verbose,
|
||||||
|
timeout=opts.timeout, relay=opts.relay, username=opts.username,
|
||||||
|
password=opts.password, port=opts.port,
|
||||||
|
encryption=opts.encryption_method)
|
||||||
|
except:
|
||||||
|
if outbox is not None:
|
||||||
|
outbox.add(msg)
|
||||||
|
print 'Delivery failed. Message saved to', opts.outbox
|
||||||
|
raise
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def config(defaults=None):
|
||||||
|
from calibre.utils.config import Config, StringConfig
|
||||||
|
desc = _('Control email delivery')
|
||||||
|
c = Config('smtp',desc) if defaults is None else StringConfig(defaults,desc)
|
||||||
|
c.add_opt('from_')
|
||||||
|
c.add_opt('accounts', default={})
|
||||||
|
c.add_opt('relay_host')
|
||||||
|
c.add_opt('relay_port', default=25)
|
||||||
|
c.add_opt('relay_username')
|
||||||
|
c.add_opt('relay_password')
|
||||||
|
c.add_opt('encryption', default='TLS', choices=['TLS', 'SSL'])
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
@ -43,6 +43,5 @@ class SpeigelOnline(BasicNewsRecipe):
|
|||||||
for y in reversed(soup.contents):
|
for y in reversed(soup.contents):
|
||||||
x.contents[0].insert(0, y)
|
x.contents[0].insert(0, y)
|
||||||
soup = x
|
soup = x
|
||||||
print 1111111
|
|
||||||
|
|
||||||
return soup
|
return soup
|
||||||
|
206
upload.py
206
upload.py
@ -40,11 +40,11 @@ except:
|
|||||||
HOST=get_ip_address('wlan0')
|
HOST=get_ip_address('wlan0')
|
||||||
except:
|
except:
|
||||||
HOST='unknown'
|
HOST='unknown'
|
||||||
|
|
||||||
def newer(targets, sources):
|
def newer(targets, sources):
|
||||||
'''
|
'''
|
||||||
Return True is sources is newer that targets or if targets
|
Return True is sources is newer that targets or if targets
|
||||||
does not exist.
|
does not exist.
|
||||||
'''
|
'''
|
||||||
for f in targets:
|
for f in targets:
|
||||||
if not os.path.exists(f):
|
if not os.path.exists(f):
|
||||||
@ -53,22 +53,22 @@ def newer(targets, sources):
|
|||||||
stimes = map(lambda x: os.stat(x).st_mtime, sources)
|
stimes = map(lambda x: os.stat(x).st_mtime, sources)
|
||||||
newest_source, oldest_target = max(stimes), min(ttimes)
|
newest_source, oldest_target = max(stimes), min(ttimes)
|
||||||
return newest_source > oldest_target
|
return newest_source > oldest_target
|
||||||
|
|
||||||
|
|
||||||
class OptionlessCommand(Command):
|
class OptionlessCommand(Command):
|
||||||
user_options = []
|
user_options = []
|
||||||
def initialize_options(self): pass
|
def initialize_options(self): pass
|
||||||
def finalize_options(self): pass
|
def finalize_options(self): pass
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
for cmd_name in self.get_sub_commands():
|
for cmd_name in self.get_sub_commands():
|
||||||
self.run_command(cmd_name)
|
self.run_command(cmd_name)
|
||||||
|
|
||||||
|
|
||||||
class sdist(OptionlessCommand):
|
class sdist(OptionlessCommand):
|
||||||
|
|
||||||
description = 'Create a source distribution using bzr'
|
description = 'Create a source distribution using bzr'
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
name = os.path.join('dist', '%s-%s.tar.gz'%(__appname__, __version__))
|
name = os.path.join('dist', '%s-%s.tar.gz'%(__appname__, __version__))
|
||||||
check_call(('bzr export '+name).split())
|
check_call(('bzr export '+name).split())
|
||||||
@ -77,9 +77,9 @@ class sdist(OptionlessCommand):
|
|||||||
|
|
||||||
class pot(OptionlessCommand):
|
class pot(OptionlessCommand):
|
||||||
description = '''Create the .pot template for all translatable strings'''
|
description = '''Create the .pot template for all translatable strings'''
|
||||||
|
|
||||||
PATH = os.path.join('src', __appname__, 'translations')
|
PATH = os.path.join('src', __appname__, 'translations')
|
||||||
|
|
||||||
def source_files(self):
|
def source_files(self):
|
||||||
ans = []
|
ans = []
|
||||||
for root, _, files in os.walk(os.path.dirname(self.PATH)):
|
for root, _, files in os.walk(os.path.dirname(self.PATH)):
|
||||||
@ -88,7 +88,7 @@ class pot(OptionlessCommand):
|
|||||||
ans.append(os.path.abspath(os.path.join(root, name)))
|
ans.append(os.path.abspath(os.path.join(root, name)))
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
sys.path.insert(0, os.path.abspath(self.PATH))
|
sys.path.insert(0, os.path.abspath(self.PATH))
|
||||||
try:
|
try:
|
||||||
@ -109,7 +109,7 @@ class pot(OptionlessCommand):
|
|||||||
sys.path.remove(os.path.abspath(self.PATH))
|
sys.path.remove(os.path.abspath(self.PATH))
|
||||||
|
|
||||||
class manual(OptionlessCommand):
|
class manual(OptionlessCommand):
|
||||||
|
|
||||||
description='''Build the User Manual '''
|
description='''Build the User Manual '''
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@ -122,20 +122,20 @@ class manual(OptionlessCommand):
|
|||||||
os.makedirs(d)
|
os.makedirs(d)
|
||||||
if not os.path.exists('.build'+os.sep+'html'):
|
if not os.path.exists('.build'+os.sep+'html'):
|
||||||
os.makedirs('.build'+os.sep+'html')
|
os.makedirs('.build'+os.sep+'html')
|
||||||
check_call(['sphinx-build', '-b', 'custom', '-d',
|
check_call(['sphinx-build', '-b', 'custom', '-d',
|
||||||
'.build/doctrees', '.', '.build/html'])
|
'.build/doctrees', '.', '.build/html'])
|
||||||
finally:
|
finally:
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean(cls):
|
def clean(cls):
|
||||||
path = os.path.join('src', 'calibre', 'manual', '.build')
|
path = os.path.join('src', 'calibre', 'manual', '.build')
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
|
||||||
class resources(OptionlessCommand):
|
class resources(OptionlessCommand):
|
||||||
description='''Compile various resource files used in calibre. '''
|
description='''Compile various resource files used in calibre. '''
|
||||||
|
|
||||||
RESOURCES = dict(
|
RESOURCES = dict(
|
||||||
opf_template = 'ebooks/metadata/opf.xml',
|
opf_template = 'ebooks/metadata/opf.xml',
|
||||||
ncx_template = 'ebooks/metadata/ncx.xml',
|
ncx_template = 'ebooks/metadata/ncx.xml',
|
||||||
@ -145,9 +145,9 @@ class resources(OptionlessCommand):
|
|||||||
jquery_scrollTo = 'gui2/viewer/jquery_scrollTo.js',
|
jquery_scrollTo = 'gui2/viewer/jquery_scrollTo.js',
|
||||||
html_css = 'ebooks/oeb/html.css',
|
html_css = 'ebooks/oeb/html.css',
|
||||||
)
|
)
|
||||||
|
|
||||||
DEST = os.path.join('src', __appname__, 'resources.py')
|
DEST = os.path.join('src', __appname__, 'resources.py')
|
||||||
|
|
||||||
def get_qt_translations(self):
|
def get_qt_translations(self):
|
||||||
data = {}
|
data = {}
|
||||||
translations_found = False
|
translations_found = False
|
||||||
@ -162,7 +162,7 @@ class resources(OptionlessCommand):
|
|||||||
if not translations_found:
|
if not translations_found:
|
||||||
print 'WARNING: Could not find Qt transations'
|
print 'WARNING: Could not find Qt transations'
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_static_resources(self):
|
def get_static_resources(self):
|
||||||
sdir = os.path.join('src', 'calibre', 'library', 'static')
|
sdir = os.path.join('src', 'calibre', 'library', 'static')
|
||||||
resources, max = {}, 0
|
resources, max = {}, 0
|
||||||
@ -171,7 +171,7 @@ class resources(OptionlessCommand):
|
|||||||
mtime = os.stat(os.path.join(sdir, f)).st_mtime
|
mtime = os.stat(os.path.join(sdir, f)).st_mtime
|
||||||
max = mtime if mtime > max else max
|
max = mtime if mtime > max else max
|
||||||
return resources, max
|
return resources, max
|
||||||
|
|
||||||
def get_recipes(self):
|
def get_recipes(self):
|
||||||
sdir = os.path.join('src', 'calibre', 'web', 'feeds', 'recipes')
|
sdir = os.path.join('src', 'calibre', 'web', 'feeds', 'recipes')
|
||||||
resources, max = {}, 0
|
resources, max = {}, 0
|
||||||
@ -181,7 +181,7 @@ class resources(OptionlessCommand):
|
|||||||
mtime = os.stat(os.path.join(sdir, f)).st_mtime
|
mtime = os.stat(os.path.join(sdir, f)).st_mtime
|
||||||
max = mtime if mtime > max else max
|
max = mtime if mtime > max else max
|
||||||
return resources, max
|
return resources, max
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
data, dest, RESOURCES = {}, self.DEST, self.RESOURCES
|
data, dest, RESOURCES = {}, self.DEST, self.RESOURCES
|
||||||
for key in RESOURCES:
|
for key in RESOURCES:
|
||||||
@ -204,7 +204,7 @@ class resources(OptionlessCommand):
|
|||||||
f.write('build_time = "%s"\n\n'%time.strftime('%d %m %Y %H%M%S'))
|
f.write('build_time = "%s"\n\n'%time.strftime('%d %m %Y %H%M%S'))
|
||||||
else:
|
else:
|
||||||
print 'Resources are up to date'
|
print 'Resources are up to date'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean(cls):
|
def clean(cls):
|
||||||
path = cls.DEST
|
path = cls.DEST
|
||||||
@ -216,7 +216,7 @@ class translations(OptionlessCommand):
|
|||||||
description='''Compile the translations'''
|
description='''Compile the translations'''
|
||||||
PATH = os.path.join('src', __appname__, 'translations')
|
PATH = os.path.join('src', __appname__, 'translations')
|
||||||
DEST = os.path.join(PATH, 'compiled.py')
|
DEST = os.path.join(PATH, 'compiled.py')
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
sys.path.insert(0, os.path.abspath(self.PATH))
|
sys.path.insert(0, os.path.abspath(self.PATH))
|
||||||
try:
|
try:
|
||||||
@ -236,21 +236,21 @@ class translations(OptionlessCommand):
|
|||||||
print 'Translations up to date'
|
print 'Translations up to date'
|
||||||
finally:
|
finally:
|
||||||
sys.path.remove(os.path.abspath(self.PATH))
|
sys.path.remove(os.path.abspath(self.PATH))
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean(cls):
|
def clean(cls):
|
||||||
path = cls.DEST
|
path = cls.DEST
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
|
|
||||||
|
|
||||||
class gui(OptionlessCommand):
|
class gui(OptionlessCommand):
|
||||||
description='''Compile all GUI forms and images'''
|
description='''Compile all GUI forms and images'''
|
||||||
PATH = os.path.join('src', __appname__, 'gui2')
|
PATH = os.path.join('src', __appname__, 'gui2')
|
||||||
IMAGES_DEST = os.path.join(PATH, 'images_rc.py')
|
IMAGES_DEST = os.path.join(PATH, 'images_rc.py')
|
||||||
QRC = os.path.join(PATH, 'images.qrc')
|
QRC = os.path.join(PATH, 'images.qrc')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_forms(cls):
|
def find_forms(cls):
|
||||||
forms = []
|
forms = []
|
||||||
@ -258,17 +258,17 @@ class gui(OptionlessCommand):
|
|||||||
for name in files:
|
for name in files:
|
||||||
if name.endswith('.ui'):
|
if name.endswith('.ui'):
|
||||||
forms.append(os.path.abspath(os.path.join(root, name)))
|
forms.append(os.path.abspath(os.path.join(root, name)))
|
||||||
|
|
||||||
return forms
|
return forms
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def form_to_compiled_form(cls, form):
|
def form_to_compiled_form(cls, form):
|
||||||
return form.rpartition('.')[0]+'_ui.py'
|
return form.rpartition('.')[0]+'_ui.py'
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.build_forms()
|
self.build_forms()
|
||||||
self.build_images()
|
self.build_images()
|
||||||
|
|
||||||
def build_images(self):
|
def build_images(self):
|
||||||
cwd, images = os.getcwd(), os.path.basename(self.IMAGES_DEST)
|
cwd, images = os.getcwd(), os.path.basename(self.IMAGES_DEST)
|
||||||
try:
|
try:
|
||||||
@ -296,13 +296,13 @@ class gui(OptionlessCommand):
|
|||||||
print 'Images are up to date'
|
print 'Images are up to date'
|
||||||
finally:
|
finally:
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
|
|
||||||
|
|
||||||
def build_forms(self):
|
def build_forms(self):
|
||||||
from PyQt4.uic import compileUi
|
from PyQt4.uic import compileUi
|
||||||
forms = self.find_forms()
|
forms = self.find_forms()
|
||||||
for form in forms:
|
for form in forms:
|
||||||
compiled_form = self.form_to_compiled_form(form)
|
compiled_form = self.form_to_compiled_form(form)
|
||||||
if not os.path.exists(compiled_form) or os.stat(form).st_mtime > os.stat(compiled_form).st_mtime:
|
if not os.path.exists(compiled_form) or os.stat(form).st_mtime > os.stat(compiled_form).st_mtime:
|
||||||
print 'Compiling form', form
|
print 'Compiling form', form
|
||||||
buf = cStringIO.StringIO()
|
buf = cStringIO.StringIO()
|
||||||
@ -313,21 +313,21 @@ class gui(OptionlessCommand):
|
|||||||
dat = dat.replace('from library import', 'from calibre.gui2.library import')
|
dat = dat.replace('from library import', 'from calibre.gui2.library import')
|
||||||
dat = dat.replace('from widgets import', 'from calibre.gui2.widgets import')
|
dat = dat.replace('from widgets import', 'from calibre.gui2.widgets import')
|
||||||
dat = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(?<!\\)",.+?\)', re.DOTALL).sub(r'_("\1")', dat)
|
dat = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(?<!\\)",.+?\)', re.DOTALL).sub(r'_("\1")', dat)
|
||||||
|
|
||||||
# Workaround bug in Qt 4.4 on Windows
|
# Workaround bug in Qt 4.4 on Windows
|
||||||
if form.endswith('dialogs%sconfig.ui'%os.sep) or form.endswith('dialogs%slrf_single.ui'%os.sep):
|
if form.endswith('dialogs%sconfig.ui'%os.sep) or form.endswith('dialogs%slrf_single.ui'%os.sep):
|
||||||
print 'Implementing Workaround for buggy pyuic in form', form
|
print 'Implementing Workaround for buggy pyuic in form', form
|
||||||
dat = re.sub(r'= QtGui\.QTextEdit\(self\..*?\)', '= QtGui.QTextEdit()', dat)
|
dat = re.sub(r'= QtGui\.QTextEdit\(self\..*?\)', '= QtGui.QTextEdit()', dat)
|
||||||
dat = re.sub(r'= QtGui\.QListWidget\(self\..*?\)', '= QtGui.QListWidget()', dat)
|
dat = re.sub(r'= QtGui\.QListWidget\(self\..*?\)', '= QtGui.QListWidget()', dat)
|
||||||
|
|
||||||
if form.endswith('viewer%smain.ui'%os.sep):
|
if form.endswith('viewer%smain.ui'%os.sep):
|
||||||
print 'Promoting WebView'
|
print 'Promoting WebView'
|
||||||
dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(')
|
dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(')
|
||||||
dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView'
|
dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView'
|
||||||
|
|
||||||
open(compiled_form, 'wb').write(dat)
|
open(compiled_form, 'wb').write(dat)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean(cls):
|
def clean(cls):
|
||||||
forms = cls.find_forms()
|
forms = cls.find_forms()
|
||||||
@ -340,16 +340,16 @@ class gui(OptionlessCommand):
|
|||||||
os.remove(x)
|
os.remove(x)
|
||||||
|
|
||||||
class clean(OptionlessCommand):
|
class clean(OptionlessCommand):
|
||||||
|
|
||||||
description='''Delete all computer generated files in the source tree'''
|
description='''Delete all computer generated files in the source tree'''
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
print 'Cleaning...'
|
print 'Cleaning...'
|
||||||
manual.clean()
|
manual.clean()
|
||||||
gui.clean()
|
gui.clean()
|
||||||
translations.clean()
|
translations.clean()
|
||||||
resources.clean()
|
resources.clean()
|
||||||
|
|
||||||
for f in glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
|
for f in glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
|
||||||
os.remove(f)
|
os.remove(f)
|
||||||
for root, _, files in os.walk('.'):
|
for root, _, files in os.walk('.'):
|
||||||
@ -358,12 +358,12 @@ class clean(OptionlessCommand):
|
|||||||
if name.endswith(t):
|
if name.endswith(t):
|
||||||
os.remove(os.path.join(root, name))
|
os.remove(os.path.join(root, name))
|
||||||
break
|
break
|
||||||
|
|
||||||
for dir in ('build', 'dist', os.path.join('src', 'calibre.egg-info')):
|
for dir in ('build', 'dist', os.path.join('src', 'calibre.egg-info')):
|
||||||
shutil.rmtree(dir, ignore_errors=True)
|
shutil.rmtree(dir, ignore_errors=True)
|
||||||
|
|
||||||
class build_py(_build_py):
|
class build_py(_build_py):
|
||||||
|
|
||||||
def find_data_files(self, package, src_dir):
|
def find_data_files(self, package, src_dir):
|
||||||
"""
|
"""
|
||||||
Return filenames for package's data files in 'src_dir'
|
Return filenames for package's data files in 'src_dir'
|
||||||
@ -377,11 +377,11 @@ class build_py(_build_py):
|
|||||||
pattern = os.path.join(src_dir, convert_path(pattern))
|
pattern = os.path.join(src_dir, convert_path(pattern))
|
||||||
next = glob.glob(pattern)
|
next = glob.glob(pattern)
|
||||||
files.extend(next if next else [pattern])
|
files.extend(next if next else [pattern])
|
||||||
|
|
||||||
return self.exclude_data_files(package, src_dir, files)
|
return self.exclude_data_files(package, src_dir, files)
|
||||||
|
|
||||||
class build(_build):
|
class build(_build):
|
||||||
|
|
||||||
sub_commands = [
|
sub_commands = [
|
||||||
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
||||||
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
|
||||||
@ -391,13 +391,13 @@ class build(_build):
|
|||||||
('build_clib', _build.has_c_libraries),
|
('build_clib', _build.has_c_libraries),
|
||||||
('build_scripts', _build.has_scripts),
|
('build_scripts', _build.has_scripts),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class update(OptionlessCommand):
|
class update(OptionlessCommand):
|
||||||
|
|
||||||
description = 'Rebuild plugins and run develop. Should be called after ' +\
|
description = 'Rebuild plugins and run develop. Should be called after ' +\
|
||||||
' a version update.'
|
' a version update.'
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
for x in ['build', 'dist', 'docs'] + \
|
for x in ['build', 'dist', 'docs'] + \
|
||||||
glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
|
glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
|
||||||
@ -407,24 +407,24 @@ class update(OptionlessCommand):
|
|||||||
os.mkdir(x)
|
os.mkdir(x)
|
||||||
else:
|
else:
|
||||||
os.remove(x)
|
os.remove(x)
|
||||||
|
|
||||||
check_call('python setup.py build_ext build'.split())
|
check_call('python setup.py build_ext build'.split())
|
||||||
check_call('sudo python setup.py develop'.split())
|
check_call('sudo python setup.py develop'.split())
|
||||||
|
|
||||||
class tag_release(OptionlessCommand):
|
class tag_release(OptionlessCommand):
|
||||||
|
|
||||||
description = 'Tag a new release in bzr'
|
description = 'Tag a new release in bzr'
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
print 'Tagging release'
|
print 'Tagging release'
|
||||||
check_call(('bzr tag '+__version__).split())
|
check_call(('bzr tag '+__version__).split())
|
||||||
check_call('bzr commit --unchanged -m'.split() + ['IGN:Tag release'])
|
check_call('bzr commit --unchanged -m'.split() + ['IGN:Tag release'])
|
||||||
|
|
||||||
|
|
||||||
class upload_demo(OptionlessCommand):
|
class upload_demo(OptionlessCommand):
|
||||||
|
|
||||||
description = 'Rebuild and upload various demos'
|
description = 'Rebuild and upload various demos'
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
check_call(
|
check_call(
|
||||||
'''html2lrf --title='Demonstration of html2lrf' --author='Kovid Goyal' '''
|
'''html2lrf --title='Demonstration of html2lrf' --author='Kovid Goyal' '''
|
||||||
@ -432,23 +432,23 @@ class upload_demo(OptionlessCommand):
|
|||||||
'''--serif-family "/usr/share/fonts/corefonts, Times New Roman" '''
|
'''--serif-family "/usr/share/fonts/corefonts, Times New Roman" '''
|
||||||
'''--mono-family "/usr/share/fonts/corefonts, Andale Mono" '''
|
'''--mono-family "/usr/share/fonts/corefonts, Andale Mono" '''
|
||||||
''''''%(HTML2LRF,), shell=True)
|
''''''%(HTML2LRF,), shell=True)
|
||||||
|
|
||||||
check_call(
|
check_call(
|
||||||
'cd src/calibre/ebooks/lrf/html/demo/ && '
|
'cd src/calibre/ebooks/lrf/html/demo/ && '
|
||||||
'zip -j /tmp/html-demo.zip * /tmp/html2lrf.lrf', shell=True)
|
'zip -j /tmp/html-demo.zip * /tmp/html2lrf.lrf', shell=True)
|
||||||
|
|
||||||
check_call('scp /tmp/html-demo.zip divok:%s/'%(DOWNLOADS,), shell=True)
|
check_call('scp /tmp/html-demo.zip divok:%s/'%(DOWNLOADS,), shell=True)
|
||||||
|
|
||||||
check_call(
|
check_call(
|
||||||
'''txt2lrf -t 'Demonstration of txt2lrf' -a 'Kovid Goyal' '''
|
'''txt2lrf -t 'Demonstration of txt2lrf' -a 'Kovid Goyal' '''
|
||||||
'''--header -o /tmp/txt2lrf.lrf %s/demo.txt'''%(TXT2LRF,), shell=True)
|
'''--header -o /tmp/txt2lrf.lrf %s/demo.txt'''%(TXT2LRF,), shell=True)
|
||||||
|
|
||||||
check_call('cd src/calibre/ebooks/lrf/txt/demo/ && '
|
check_call('cd src/calibre/ebooks/lrf/txt/demo/ && '
|
||||||
'zip -j /tmp/txt-demo.zip * /tmp/txt2lrf.lrf', shell=True)
|
'zip -j /tmp/txt-demo.zip * /tmp/txt2lrf.lrf', shell=True)
|
||||||
|
|
||||||
check_call('''scp /tmp/txt-demo.zip divok:%s/'''%(DOWNLOADS,), shell=True)
|
check_call('''scp /tmp/txt-demo.zip divok:%s/'''%(DOWNLOADS,), shell=True)
|
||||||
|
|
||||||
|
|
||||||
def installer_name(ext):
|
def installer_name(ext):
|
||||||
if ext in ('exe', 'dmg'):
|
if ext in ('exe', 'dmg'):
|
||||||
return 'dist/%s-%s.%s'%(__appname__, __version__, ext)
|
return 'dist/%s-%s.%s'%(__appname__, __version__, ext)
|
||||||
@ -467,14 +467,14 @@ class build_linux(OptionlessCommand):
|
|||||||
return os.path.basename(installer)
|
return os.path.basename(installer)
|
||||||
|
|
||||||
class VMInstaller(OptionlessCommand):
|
class VMInstaller(OptionlessCommand):
|
||||||
|
|
||||||
user_options = [('dont-shutdown', 'd', 'Dont shutdown Vm after build')]
|
user_options = [('dont-shutdown', 'd', 'Dont shutdown Vm after build')]
|
||||||
boolean_options = ['dont-shutdown']
|
boolean_options = ['dont-shutdown']
|
||||||
|
|
||||||
def initialize_options(self):
|
def initialize_options(self):
|
||||||
self.dont_shutdown = False
|
self.dont_shutdown = False
|
||||||
|
|
||||||
BUILD_SCRIPT = textwrap.dedent('''\
|
BUILD_SCRIPT = textwrap.dedent('''\
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
export CALIBRE_BUILDBOT=1
|
export CALIBRE_BUILDBOT=1
|
||||||
cd ~/build && \
|
cd ~/build && \
|
||||||
@ -488,10 +488,10 @@ class VMInstaller(OptionlessCommand):
|
|||||||
rm -rf build/* dist/* && \
|
rm -rf build/* dist/* && \
|
||||||
%%s %%s
|
%%s %%s
|
||||||
'''%dict(host=HOST, project=__appname__))
|
'''%dict(host=HOST, project=__appname__))
|
||||||
|
|
||||||
def get_build_script(self, subs):
|
def get_build_script(self, subs):
|
||||||
return self.BUILD_SCRIPT%subs
|
return self.BUILD_SCRIPT%subs
|
||||||
|
|
||||||
def start_vm(self, ssh_host, build_script, sleep=75):
|
def start_vm(self, ssh_host, build_script, sleep=75):
|
||||||
build_script = self.get_build_script(build_script)
|
build_script = self.get_build_script(build_script)
|
||||||
vmware = ('vmware', '-q', '-x', '-n', self.VM)
|
vmware = ('vmware', '-q', '-x', '-n', self.VM)
|
||||||
@ -500,7 +500,7 @@ class VMInstaller(OptionlessCommand):
|
|||||||
t.write(build_script)
|
t.write(build_script)
|
||||||
t.flush()
|
t.flush()
|
||||||
print 'Waiting for VM to startup'
|
print 'Waiting for VM to startup'
|
||||||
while call('ping -q -c1 '+ssh_host, shell=True,
|
while call('ping -q -c1 '+ssh_host, shell=True,
|
||||||
stdout=open('/dev/null', 'w')) != 0:
|
stdout=open('/dev/null', 'w')) != 0:
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
time.sleep(20)
|
time.sleep(20)
|
||||||
@ -513,10 +513,10 @@ class build_windows(VMInstaller):
|
|||||||
VM = '/mnt/backup/calibre_windows_xp_home/calibre_windows_xp_home.vmx'
|
VM = '/mnt/backup/calibre_windows_xp_home/calibre_windows_xp_home.vmx'
|
||||||
if not os.path.exists(VM):
|
if not os.path.exists(VM):
|
||||||
VM = '/home/kovid/calibre_windows_xp_home/calibre_windows_xp_home.vmx'
|
VM = '/home/kovid/calibre_windows_xp_home/calibre_windows_xp_home.vmx'
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
installer = installer_name('exe')
|
installer = installer_name('exe')
|
||||||
self.start_vm('windows', ('python setup.py develop',
|
self.start_vm('windows', ('python setup.py develop',
|
||||||
'python',
|
'python',
|
||||||
r'installer\\windows\\freeze.py'))
|
r'installer\\windows\\freeze.py'))
|
||||||
if os.path.exists('build/py2exe'):
|
if os.path.exists('build/py2exe'):
|
||||||
@ -529,7 +529,7 @@ class build_windows(VMInstaller):
|
|||||||
Popen(('ssh', 'windows', 'shutdown', '-s', '-t', '0'))
|
Popen(('ssh', 'windows', 'shutdown', '-s', '-t', '0'))
|
||||||
self.run_windows_install_jammer(installer)
|
self.run_windows_install_jammer(installer)
|
||||||
return os.path.basename(installer)
|
return os.path.basename(installer)
|
||||||
|
|
||||||
def run_windows_install_jammer(self, installer):
|
def run_windows_install_jammer(self, installer):
|
||||||
ibp = os.path.abspath('installer/windows')
|
ibp = os.path.abspath('installer/windows')
|
||||||
sys.path.insert(0, ibp)
|
sys.path.insert(0, ibp)
|
||||||
@ -546,14 +546,14 @@ class build_osx(VMInstaller):
|
|||||||
VM = '/vmware/Mac OSX/Mac OSX.vmx'
|
VM = '/vmware/Mac OSX/Mac OSX.vmx'
|
||||||
if not os.path.exists(VM):
|
if not os.path.exists(VM):
|
||||||
VM = '/home/kovid/calibre_os_x/Mac OSX.vmx'
|
VM = '/home/kovid/calibre_os_x/Mac OSX.vmx'
|
||||||
|
|
||||||
def get_build_script(self, subs):
|
def get_build_script(self, subs):
|
||||||
return (self.BUILD_SCRIPT%subs).replace('rm ', 'sudo rm ')
|
return (self.BUILD_SCRIPT%subs).replace('rm ', 'sudo rm ')
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
installer = installer_name('dmg')
|
installer = installer_name('dmg')
|
||||||
python = '/Library/Frameworks/Python.framework/Versions/Current/bin/python'
|
python = '/Library/Frameworks/Python.framework/Versions/Current/bin/python'
|
||||||
self.start_vm('osx', ('sudo %s setup.py develop'%python, python,
|
self.start_vm('osx', ('sudo %s setup.py develop'%python, python,
|
||||||
'installer/osx/freeze.py'))
|
'installer/osx/freeze.py'))
|
||||||
check_call(('scp', 'osx:build/calibre/dist/*.dmg', 'dist'))
|
check_call(('scp', 'osx:build/calibre/dist/*.dmg', 'dist'))
|
||||||
if not os.path.exists(installer):
|
if not os.path.exists(installer):
|
||||||
@ -579,7 +579,7 @@ class upload_installers(OptionlessCommand):
|
|||||||
c.perform()
|
c.perform()
|
||||||
c.close()
|
c.close()
|
||||||
return b.getvalue().split() if listonly else b.getvalue().splitlines()
|
return b.getvalue().split() if listonly else b.getvalue().splitlines()
|
||||||
|
|
||||||
def curl_delete_file(self, path, url=MOBILEREAD):
|
def curl_delete_file(self, path, url=MOBILEREAD):
|
||||||
import pycurl
|
import pycurl
|
||||||
c = pycurl.Curl()
|
c = pycurl.Curl()
|
||||||
@ -590,8 +590,8 @@ class upload_installers(OptionlessCommand):
|
|||||||
c.setopt(c.QUOTE, ['dele '+ path])
|
c.setopt(c.QUOTE, ['dele '+ path])
|
||||||
c.perform()
|
c.perform()
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
def curl_upload_file(self, stream, url):
|
def curl_upload_file(self, stream, url):
|
||||||
import pycurl
|
import pycurl
|
||||||
c = pycurl.Curl()
|
c = pycurl.Curl()
|
||||||
@ -618,7 +618,7 @@ class upload_installers(OptionlessCommand):
|
|||||||
stream.seek(0,2)
|
stream.seek(0,2)
|
||||||
if size != stream.tell():
|
if size != stream.tell():
|
||||||
raise RuntimeError('curl failed to upload %s correctly'%getattr(stream, 'name', ''))
|
raise RuntimeError('curl failed to upload %s correctly'%getattr(stream, 'name', ''))
|
||||||
|
|
||||||
def upload_installer(self, name):
|
def upload_installer(self, name):
|
||||||
if not os.path.exists(name):
|
if not os.path.exists(name):
|
||||||
return
|
return
|
||||||
@ -633,19 +633,19 @@ class upload_installers(OptionlessCommand):
|
|||||||
print 'Uploading installers...'
|
print 'Uploading installers...'
|
||||||
for i in ('dmg', 'exe', 'tar.bz2'):
|
for i in ('dmg', 'exe', 'tar.bz2'):
|
||||||
self.upload_installer(installer_name(i))
|
self.upload_installer(installer_name(i))
|
||||||
|
|
||||||
check_call('''ssh divok echo %s \\> %s/latest_version'''\
|
check_call('''ssh divok echo %s \\> %s/latest_version'''\
|
||||||
%(__version__, DOWNLOADS), shell=True)
|
%(__version__, DOWNLOADS), shell=True)
|
||||||
|
|
||||||
class upload_user_manual(OptionlessCommand):
|
class upload_user_manual(OptionlessCommand):
|
||||||
description = 'Build and upload the User Manual'
|
description = 'Build and upload the User Manual'
|
||||||
sub_commands = [('manual', None)]
|
sub_commands = [('manual', None)]
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
OptionlessCommand.run(self)
|
OptionlessCommand.run(self)
|
||||||
check_call(' '.join(['scp', '-r', 'src/calibre/manual/.build/html/*',
|
check_call(' '.join(['scp', '-r', 'src/calibre/manual/.build/html/*',
|
||||||
'divok:%s'%USER_MANUAL]), shell=True)
|
'divok:%s'%USER_MANUAL]), shell=True)
|
||||||
|
|
||||||
class upload_to_pypi(OptionlessCommand):
|
class upload_to_pypi(OptionlessCommand):
|
||||||
description = 'Upload eggs and source to PyPI'
|
description = 'Upload eggs and source to PyPI'
|
||||||
def run(self):
|
def run(self):
|
||||||
@ -658,7 +658,7 @@ class upload_to_pypi(OptionlessCommand):
|
|||||||
os.mkdir('build')
|
os.mkdir('build')
|
||||||
check_call('python setup.py build_ext bdist_egg --exclude-source-files upload'.split())
|
check_call('python setup.py build_ext bdist_egg --exclude-source-files upload'.split())
|
||||||
check_call('python setup.py sdist upload'.split())
|
check_call('python setup.py sdist upload'.split())
|
||||||
|
|
||||||
class stage3(OptionlessCommand):
|
class stage3(OptionlessCommand):
|
||||||
description = 'Stage 3 of the build process'
|
description = 'Stage 3 of the build process'
|
||||||
sub_commands = [
|
sub_commands = [
|
||||||
@ -667,7 +667,7 @@ class stage3(OptionlessCommand):
|
|||||||
('upload_to_pypi', None),
|
('upload_to_pypi', None),
|
||||||
('upload_rss', None),
|
('upload_rss', None),
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def misc(cls):
|
def misc(cls):
|
||||||
check_call('ssh divok rm -f %s/calibre-\*.tar.gz'%DOWNLOADS, shell=True)
|
check_call('ssh divok rm -f %s/calibre-\*.tar.gz'%DOWNLOADS, shell=True)
|
||||||
@ -675,23 +675,23 @@ class stage3(OptionlessCommand):
|
|||||||
check_call('''rm -rf dist/* build/*''', shell=True)
|
check_call('''rm -rf dist/* build/*''', shell=True)
|
||||||
check_call('ssh divok bzr update /var/www/calibre.kovidgoyal.net/calibre/',
|
check_call('ssh divok bzr update /var/www/calibre.kovidgoyal.net/calibre/',
|
||||||
shell=True)
|
shell=True)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
OptionlessCommand.run(self)
|
OptionlessCommand.run(self)
|
||||||
self.misc()
|
self.misc()
|
||||||
|
|
||||||
class stage2(OptionlessCommand):
|
class stage2(OptionlessCommand):
|
||||||
description = 'Stage 2 of the build process'
|
description = 'Stage 2 of the build process'
|
||||||
sub_commands = [
|
sub_commands = [
|
||||||
('build_linux', None),
|
('build_linux', None),
|
||||||
('build_windows', None),
|
('build_windows', None),
|
||||||
('build_osx', None)
|
('build_osx', None)
|
||||||
]
|
]
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
check_call('rm -rf dist/*', shell=True)
|
check_call('rm -rf dist/*', shell=True)
|
||||||
OptionlessCommand.run(self)
|
OptionlessCommand.run(self)
|
||||||
|
|
||||||
class stage1(OptionlessCommand):
|
class stage1(OptionlessCommand):
|
||||||
description = 'Stage 1 of the build process'
|
description = 'Stage 1 of the build process'
|
||||||
sub_commands = [
|
sub_commands = [
|
||||||
@ -699,10 +699,10 @@ class stage1(OptionlessCommand):
|
|||||||
('tag_release', None),
|
('tag_release', None),
|
||||||
('upload_demo', None),
|
('upload_demo', None),
|
||||||
]
|
]
|
||||||
|
|
||||||
class upload(OptionlessCommand):
|
class upload(OptionlessCommand):
|
||||||
description = 'Build and upload calibre to the servers'
|
description = 'Build and upload calibre to the servers'
|
||||||
|
|
||||||
sub_commands = [
|
sub_commands = [
|
||||||
('stage1', None),
|
('stage1', None),
|
||||||
('stage2', None),
|
('stage2', None),
|
||||||
@ -711,13 +711,13 @@ class upload(OptionlessCommand):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
class upload_rss(OptionlessCommand):
|
class upload_rss(OptionlessCommand):
|
||||||
|
|
||||||
from bzrlib import log as blog
|
from bzrlib import log as blog
|
||||||
|
|
||||||
class ChangelogFormatter(blog.LogFormatter):
|
class ChangelogFormatter(blog.LogFormatter):
|
||||||
supports_tags = True
|
supports_tags = True
|
||||||
supports_merge_revisions = False
|
supports_merge_revisions = False
|
||||||
|
|
||||||
def __init__(self, num_of_versions=20):
|
def __init__(self, num_of_versions=20):
|
||||||
from calibre.utils.rss_gen import RSS2
|
from calibre.utils.rss_gen import RSS2
|
||||||
self.num_of_versions = num_of_versions
|
self.num_of_versions = num_of_versions
|
||||||
@ -727,21 +727,21 @@ try:
|
|||||||
description = 'Latest release of calibre',
|
description = 'Latest release of calibre',
|
||||||
lastBuildDate = datetime.utcnow()
|
lastBuildDate = datetime.utcnow()
|
||||||
)
|
)
|
||||||
self.current_entry = None
|
self.current_entry = None
|
||||||
|
|
||||||
def log_revision(self, r):
|
def log_revision(self, r):
|
||||||
from calibre.utils.rss_gen import RSSItem, Guid
|
from calibre.utils.rss_gen import RSSItem, Guid
|
||||||
if len(self.rss.items) > self.num_of_versions-1:
|
if len(self.rss.items) > self.num_of_versions-1:
|
||||||
return
|
return
|
||||||
msg = r.rev.message
|
msg = r.rev.message
|
||||||
match = re.match(r'version\s+(\d+\.\d+.\d+)', msg)
|
match = re.match(r'version\s+(\d+\.\d+.\d+)', msg)
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
if self.current_entry is not None:
|
if self.current_entry is not None:
|
||||||
mkup = '<div><ul>%s</ul></div>'
|
mkup = '<div><ul>%s</ul></div>'
|
||||||
self.current_entry.description = mkup%(''.join(
|
self.current_entry.description = mkup%(''.join(
|
||||||
self.current_entry.description))
|
self.current_entry.description))
|
||||||
|
|
||||||
self.rss.items.append(self.current_entry)
|
self.rss.items.append(self.current_entry)
|
||||||
timestamp = r.rev.timezone + r.rev.timestamp
|
timestamp = r.rev.timezone + r.rev.timestamp
|
||||||
self.current_entry = RSSItem(
|
self.current_entry = RSSItem(
|
||||||
@ -755,13 +755,13 @@ try:
|
|||||||
if re.search(r'[a-zA-Z]', msg) and len(msg.strip()) > 5:
|
if re.search(r'[a-zA-Z]', msg) and len(msg.strip()) > 5:
|
||||||
if 'translation' not in msg and not msg.startswith('IGN'):
|
if 'translation' not in msg and not msg.startswith('IGN'):
|
||||||
msg = msg.replace('<', '<').replace('>', '>')
|
msg = msg.replace('<', '<').replace('>', '>')
|
||||||
msg = re.sub('#(\d+)', r'<a href="http://calibre.kovidgoyal.net/ticket/\1">#\1</a>',
|
msg = re.sub('#(\d+)', r'<a href="http://calibre.kovidgoyal.net/ticket/\1">#\1</a>',
|
||||||
msg)
|
msg)
|
||||||
|
|
||||||
self.current_entry.description.append(
|
self.current_entry.description.append(
|
||||||
'<li>%s</li>'%msg.strip())
|
'<li>%s</li>'%msg.strip())
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
from bzrlib import log, branch
|
from bzrlib import log, branch
|
||||||
bzr_path = os.path.expanduser('~/work/calibre')
|
bzr_path = os.path.expanduser('~/work/calibre')
|
||||||
@ -771,5 +771,5 @@ try:
|
|||||||
lf.rss.write_xml(open('/tmp/releases.xml', 'wb'))
|
lf.rss.write_xml(open('/tmp/releases.xml', 'wb'))
|
||||||
subprocess.check_call('scp /tmp/releases.xml divok:/var/www/calibre.kovidgoyal.net/htdocs/downloads'.split())
|
subprocess.check_call('scp /tmp/releases.xml divok:/var/www/calibre.kovidgoyal.net/htdocs/downloads'.split())
|
||||||
except ImportError:
|
except ImportError:
|
||||||
upload_rss = None
|
upload_rss = None
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user