mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
be42c00f88
@ -4,6 +4,72 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.9
|
||||||
|
date: 2010-07-17
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "New unified toolbar"
|
||||||
|
type: major
|
||||||
|
description: >
|
||||||
|
"A new unified toolbar combines the old toolbar and device display, to save space. Now when a device is connected, buttons
|
||||||
|
are created in the unified toolbar for the device and its storage cards. Click the arrow next to the button to eject the device."
|
||||||
|
|
||||||
|
- title: "Device drivers: Add option to allow calibre to automatically manage metadata on the device in Preferences->Add/Save->Sending to device"
|
||||||
|
|
||||||
|
- title: "BibTeX output for catalogs. The list of books in calibre can now also be output as a .bib file"
|
||||||
|
|
||||||
|
- title: "A new toolbar button to choose/create different calibre libraries. Be careful using it if you also use custom columns."
|
||||||
|
|
||||||
|
- title: "Support for the MiBuk"
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "MOBI metadata: Replace HTML entities in the title read from the MOBI file"
|
||||||
|
|
||||||
|
- title: "Conversion pipeline: Handle elements with percentage sizes that are children of zero size parents correctly."
|
||||||
|
tickets: [6155]
|
||||||
|
|
||||||
|
- title: "Fix regression that made LRF conversion less robust"
|
||||||
|
tickets: [6180]
|
||||||
|
|
||||||
|
- title: "FB2 Input: Handle embedded images correctly, so that EPUB generated from FB2 works with Adobe Digital Editions."
|
||||||
|
tickets: [6183]
|
||||||
|
|
||||||
|
- title: "Fix regression that prevented old news from being deleted in the calibre library if calibre is never kept running for more than an hour"
|
||||||
|
|
||||||
|
- title: "RTF Input: Fix handling of text align and superscript/subscripts"
|
||||||
|
tickets: [3644,5060]
|
||||||
|
|
||||||
|
- title: "Fix long series or publisher names causing convert dialog to become too wide"
|
||||||
|
|
||||||
|
- title: "SONY driver: Fix handling of invalid XML databases with null bytes"
|
||||||
|
tickets: [6165]
|
||||||
|
|
||||||
|
- title: "iTunes driver: Better series_index sorting"
|
||||||
|
|
||||||
|
- title: "Improved editing of dates for custom columns"
|
||||||
|
|
||||||
|
- title: "Linux USB scanner: Don't fail to start calibre if SYFS is not present. Instead simply fail to detect devices"
|
||||||
|
tickets: [6156]
|
||||||
|
|
||||||
|
- title: "Android driver: Show books on device if Aldiko is being used"
|
||||||
|
tickets: [6100]
|
||||||
|
|
||||||
|
- title: "Upgrade to Qt 4.6.3 in all binary builds to ensure proper rendering of the new toolbar icons"
|
||||||
|
|
||||||
|
- title: "Fix handling of entities in epub files by the epub-fix command"
|
||||||
|
tickets: [6136]
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: "EL Pain Impresso"
|
||||||
|
author: Darko Miletic
|
||||||
|
|
||||||
|
- title: "MIT Technology Review, Alternet, Waco Tribune Herald and Orlando Sentinel"
|
||||||
|
author: rty
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Google Reader
|
||||||
|
|
||||||
|
|
||||||
- version: 0.7.8
|
- version: 0.7.8
|
||||||
date: 2010-07-09
|
date: 2010-07-09
|
||||||
|
|
||||||
|
@ -440,6 +440,9 @@ xml_entity_to_unicode = partial(entity_to_unicode, result_exceptions = {
|
|||||||
'>' : '>',
|
'>' : '>',
|
||||||
'&' : '&'})
|
'&' : '&'})
|
||||||
|
|
||||||
|
def replace_entities(raw):
|
||||||
|
return _ent_pat.sub(entity_to_unicode, raw)
|
||||||
|
|
||||||
def prepare_string_for_xml(raw, attribute=False):
|
def prepare_string_for_xml(raw, attribute=False):
|
||||||
raw = _ent_pat.sub(entity_to_unicode, raw)
|
raw = _ent_pat.sub(entity_to_unicode, raw)
|
||||||
raw = raw.replace('&', '&').replace('<', '<').replace('>', '>')
|
raw = raw.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.8'
|
__version__ = '0.7.9'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -60,6 +60,9 @@ class FB2Input(InputFormatPlugin):
|
|||||||
|
|
||||||
transform = etree.XSLT(styledoc)
|
transform = etree.XSLT(styledoc)
|
||||||
result = transform(doc)
|
result = transform(doc)
|
||||||
|
for img in result.xpath('//img[@src]'):
|
||||||
|
src = img.get('src')
|
||||||
|
img.set('src', self.binary_map.get(src, src))
|
||||||
open('index.xhtml', 'wb').write(transform.tostring(result))
|
open('index.xhtml', 'wb').write(transform.tostring(result))
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
mi = get_metadata(stream, 'fb2')
|
mi = get_metadata(stream, 'fb2')
|
||||||
@ -83,9 +86,15 @@ class FB2Input(InputFormatPlugin):
|
|||||||
return os.path.join(os.getcwd(), 'metadata.opf')
|
return os.path.join(os.getcwd(), 'metadata.opf')
|
||||||
|
|
||||||
def extract_embedded_content(self, doc):
|
def extract_embedded_content(self, doc):
|
||||||
|
self.binary_map = {}
|
||||||
for elem in doc.xpath('./*'):
|
for elem in doc.xpath('./*'):
|
||||||
if 'binary' in elem.tag and elem.attrib.has_key('id'):
|
if 'binary' in elem.tag and elem.attrib.has_key('id'):
|
||||||
|
ct = elem.get('content-type', '')
|
||||||
fname = elem.attrib['id']
|
fname = elem.attrib['id']
|
||||||
|
ext = ct.rpartition('/')[-1].lower()
|
||||||
|
if ext in ('png', 'jpeg', 'jpg'):
|
||||||
|
fname += '.' + ext
|
||||||
|
self.binary_map[elem.get('id')] = fname
|
||||||
data = b64decode(elem.text.strip())
|
data = b64decode(elem.text.strip())
|
||||||
open(fname, 'wb').write(data)
|
open(fname, 'wb').write(data)
|
||||||
|
|
||||||
|
@ -368,7 +368,15 @@ class LRFInput(InputFormatPlugin):
|
|||||||
if options.verbose > 2:
|
if options.verbose > 2:
|
||||||
open('lrs.xml', 'wb').write(xml.encode('utf-8'))
|
open('lrs.xml', 'wb').write(xml.encode('utf-8'))
|
||||||
parser = etree.XMLParser(no_network=True, huge_tree=True)
|
parser = etree.XMLParser(no_network=True, huge_tree=True)
|
||||||
doc = etree.fromstring(xml, parser=parser)
|
try:
|
||||||
|
doc = etree.fromstring(xml, parser=parser)
|
||||||
|
except:
|
||||||
|
self.log.warn('Failed to parse XML. Trying to recover')
|
||||||
|
parser = etree.XMLParser(no_network=True, huge_tree=True,
|
||||||
|
recover=True)
|
||||||
|
doc = etree.fromstring(xml, parser=parser)
|
||||||
|
|
||||||
|
|
||||||
char_button_map = {}
|
char_button_map = {}
|
||||||
for x in doc.xpath('//CharButton[@refobj]'):
|
for x in doc.xpath('//CharButton[@refobj]'):
|
||||||
ro = x.get('refobj')
|
ro = x.get('refobj')
|
||||||
|
@ -14,7 +14,8 @@ except ImportError:
|
|||||||
|
|
||||||
from lxml import html, etree
|
from lxml import html, etree
|
||||||
|
|
||||||
from calibre import xml_entity_to_unicode, CurrentDir, entity_to_unicode
|
from calibre import xml_entity_to_unicode, CurrentDir, entity_to_unicode, \
|
||||||
|
replace_entities
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.utils.date import parse_date
|
from calibre.utils.date import parse_date
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
@ -70,7 +71,7 @@ class EXTHHeader(object):
|
|||||||
#else:
|
#else:
|
||||||
# print 'unknown record', id, repr(content)
|
# print 'unknown record', id, repr(content)
|
||||||
if title:
|
if title:
|
||||||
self.mi.title = title
|
self.mi.title = replace_entities(title)
|
||||||
|
|
||||||
def process_metadata(self, id, content, codec):
|
def process_metadata(self, id, content, codec):
|
||||||
if id == 100:
|
if id == 100:
|
||||||
|
@ -475,7 +475,8 @@ class Style(object):
|
|||||||
value = float(m.group(1))
|
value = float(m.group(1))
|
||||||
unit = m.group(2)
|
unit = m.group(2)
|
||||||
if unit == '%':
|
if unit == '%':
|
||||||
base = base or self.width
|
if base is None:
|
||||||
|
base = self.width
|
||||||
result = (value / 100.0) * base
|
result = (value / 100.0) * base
|
||||||
elif unit == 'px':
|
elif unit == 'px':
|
||||||
result = value * 72.0 / self._profile.dpi
|
result = value * 72.0 / self._profile.dpi
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
<item row="9" column="0" colspan="2">
|
<item row="9" column="0" colspan="2">
|
||||||
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="10" column="0">
|
<item row="10" column="0" colspan="2">
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
|
@ -43,12 +43,6 @@
|
|||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximumSize">
|
|
||||||
<size>
|
|
||||||
<width>500</width>
|
|
||||||
<height>16777215</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="minimumContentsLength">
|
<property name="minimumContentsLength">
|
||||||
<number>30</number>
|
<number>30</number>
|
||||||
</property>
|
</property>
|
||||||
|
@ -10,7 +10,7 @@ import os
|
|||||||
from PyQt4.Qt import QDialog
|
from PyQt4.Qt import QDialog
|
||||||
|
|
||||||
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
|
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
|
||||||
from calibre.gui2 import error_dialog, choose_dir
|
from calibre.gui2 import error_dialog, choose_dir, warning_dialog
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre import isbytestring, patheq
|
from calibre import isbytestring, patheq
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
@ -24,6 +24,7 @@ class ChooseLibrary(QDialog, Ui_Dialog):
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.new_db = None
|
self.new_db = None
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
self.location.initialize('choose_library_dialog')
|
||||||
|
|
||||||
lp = db.library_path
|
lp = db.library_path
|
||||||
if isbytestring(lp):
|
if isbytestring(lp):
|
||||||
@ -61,6 +62,12 @@ class ChooseLibrary(QDialog, Ui_Dialog):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def perform_action(self, ac, loc):
|
def perform_action(self, ac, loc):
|
||||||
|
if ac in ('new', 'existing'):
|
||||||
|
warning_dialog(self.parent(), _('Custom columns'),
|
||||||
|
_('If you use custom columns and they differ between '
|
||||||
|
'libraries, you will have various problems. Best '
|
||||||
|
'to ensure you have the same custom columns in each '
|
||||||
|
'library.'), show=True)
|
||||||
if ac in ('new', 'existing'):
|
if ac in ('new', 'existing'):
|
||||||
prefs['library_path'] = loc
|
prefs['library_path'] = loc
|
||||||
self.callback(loc)
|
self.callback(loc)
|
||||||
@ -79,4 +86,5 @@ class ChooseLibrary(QDialog, Ui_Dialog):
|
|||||||
loc):
|
loc):
|
||||||
return
|
return
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
self.location.save_history()
|
||||||
self.perform_action(action, loc)
|
self.perform_action(action, loc)
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
|
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="0" colspan="3">
|
<item row="0" column="0" colspan="4">
|
||||||
<widget class="QLabel" name="old_location">
|
<widget class="QLabel" name="old_location">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Your calibre library is currently located at {0}</string>
|
<string>Your calibre library is currently located at {0}</string>
|
||||||
@ -38,14 +38,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="4" column="0" colspan="4">
|
||||||
<widget class="QLineEdit" name="location">
|
|
||||||
<property name="readOnly">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0" colspan="3">
|
|
||||||
<widget class="QRadioButton" name="existing_library">
|
<widget class="QRadioButton" name="existing_library">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Use &existing library at the new location</string>
|
<string>Use &existing library at the new location</string>
|
||||||
@ -55,21 +48,21 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0" colspan="2">
|
<item row="5" column="0" colspan="3">
|
||||||
<widget class="QRadioButton" name="empty_library">
|
<widget class="QRadioButton" name="empty_library">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Create an empty library at the new location</string>
|
<string>&Create an empty library at the new location</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0" colspan="2">
|
<item row="6" column="0" colspan="3">
|
||||||
<widget class="QRadioButton" name="move_library">
|
<widget class="QRadioButton" name="move_library">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Move current library to new location</string>
|
<string>&Move current library to new location</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
<item row="8" column="2">
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
@ -79,7 +72,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="7" column="2">
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
@ -118,7 +111,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="2">
|
<item row="2" column="3">
|
||||||
<widget class="QToolButton" name="browse_button">
|
<widget class="QToolButton" name="browse_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>...</string>
|
<string>...</string>
|
||||||
@ -129,8 +122,18 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="1" colspan="2">
|
||||||
|
<widget class="HistoryLineEdit" name="location"/>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>HistoryLineEdit</class>
|
||||||
|
<extends>QComboBox</extends>
|
||||||
|
<header>calibre/gui2/widgets.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../../resources/images.qrc"/>
|
<include location="../../../../resources/images.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="stop_all_jobs_button">
|
<widget class="QPushButton" name="stop_all_jobs_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Stop &all jobs</string>
|
<string>Stop &all non device jobs</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -10,7 +10,7 @@ Scheduler for automated recipe downloads
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, SIGNAL, Qt, QTime, QObject, QMenu, \
|
from PyQt4.Qt import QDialog, SIGNAL, Qt, QTime, QObject, QMenu, \
|
||||||
QAction, QIcon, QMutex, QTimer
|
QAction, QIcon, QMutex, QTimer, pyqtSignal
|
||||||
|
|
||||||
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
|
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
|
||||||
from calibre.gui2.search_box import SearchBox2
|
from calibre.gui2.search_box import SearchBox2
|
||||||
@ -203,6 +203,9 @@ class Scheduler(QObject):
|
|||||||
|
|
||||||
INTERVAL = 1 # minutes
|
INTERVAL = 1 # minutes
|
||||||
|
|
||||||
|
delete_old_news = pyqtSignal(object)
|
||||||
|
start_recipe_fetch = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent, db):
|
def __init__(self, parent, db):
|
||||||
QObject.__init__(self, parent)
|
QObject.__init__(self, parent)
|
||||||
self.internet_connection_failed = False
|
self.internet_connection_failed = False
|
||||||
@ -225,20 +228,23 @@ class Scheduler(QObject):
|
|||||||
self.download_all_scheduled)
|
self.download_all_scheduled)
|
||||||
|
|
||||||
self.timer = QTimer(self)
|
self.timer = QTimer(self)
|
||||||
self.timer.start(int(self.INTERVAL * 60000))
|
self.timer.start(int(self.INTERVAL * 60 * 1000))
|
||||||
self.oldest_timer = QTimer()
|
self.oldest_timer = QTimer()
|
||||||
self.connect(self.oldest_timer, SIGNAL('timeout()'), self.oldest_check)
|
self.connect(self.oldest_timer, SIGNAL('timeout()'), self.oldest_check)
|
||||||
self.connect(self.timer, SIGNAL('timeout()'), self.check)
|
self.connect(self.timer, SIGNAL('timeout()'), self.check)
|
||||||
self.oldest = gconf['oldest_news']
|
self.oldest = gconf['oldest_news']
|
||||||
self.oldest_timer.start(int(60 * 60000))
|
self.oldest_timer.start(int(60 * 60 * 1000))
|
||||||
self.oldest_check()
|
QTimer.singleShot(5 * 1000, self.oldest_check)
|
||||||
|
self.database_changed = self.recipe_model.database_changed
|
||||||
|
|
||||||
def oldest_check(self):
|
def oldest_check(self):
|
||||||
if self.oldest > 0:
|
if self.oldest > 0:
|
||||||
delta = timedelta(days=self.oldest)
|
delta = timedelta(days=self.oldest)
|
||||||
ids = self.recipe_model.db.tags_older_than(_('News'), delta)
|
ids = self.recipe_model.db.tags_older_than(_('News'), delta)
|
||||||
if ids:
|
if ids:
|
||||||
self.emit(SIGNAL('delete_old_news(PyQt_PyObject)'), ids)
|
ids = list(ids)
|
||||||
|
if ids:
|
||||||
|
self.delete_old_news.emit(ids)
|
||||||
|
|
||||||
def show_dialog(self, *args):
|
def show_dialog(self, *args):
|
||||||
self.lock.lock()
|
self.lock.lock()
|
||||||
@ -282,7 +288,7 @@ class Scheduler(QObject):
|
|||||||
'urn':urn,
|
'urn':urn,
|
||||||
}
|
}
|
||||||
self.download_queue.add(urn)
|
self.download_queue.add(urn)
|
||||||
self.emit(SIGNAL('start_recipe_fetch(PyQt_PyObject)'), arg)
|
self.start_recipe_fetch.emit(arg)
|
||||||
finally:
|
finally:
|
||||||
self.lock.unlock()
|
self.lock.unlock()
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ from calibre.gui2.widgets import ComboBoxWithHelp
|
|||||||
from calibre import human_readable
|
from calibre import human_readable
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
|
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||||
|
|
||||||
ICON_SIZE = 48
|
ICON_SIZE = 48
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ class LocationManager(QObject): # {{{
|
|||||||
|
|
||||||
ac('library', _('Library'), 'lt.png',
|
ac('library', _('Library'), 'lt.png',
|
||||||
_('Show books in calibre library'))
|
_('Show books in calibre library'))
|
||||||
ac('main', _('Main'), 'reader.svg',
|
ac('main', _('Reader'), 'reader.svg',
|
||||||
_('Show books in the main memory of the device'))
|
_('Show books in the main memory of the device'))
|
||||||
ac('carda', _('Card A'), 'sd.svg',
|
ac('carda', _('Card A'), 'sd.svg',
|
||||||
_('Show books in storage card A'))
|
_('Show books in storage card A'))
|
||||||
@ -307,7 +308,7 @@ class Action(QAction):
|
|||||||
|
|
||||||
class MainWindowMixin(object):
|
class MainWindowMixin(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, db):
|
||||||
self.device_connected = None
|
self.device_connected = None
|
||||||
self.setObjectName('MainWindow')
|
self.setObjectName('MainWindow')
|
||||||
self.setWindowIcon(QIcon(I('library.png')))
|
self.setWindowIcon(QIcon(I('library.png')))
|
||||||
@ -323,6 +324,7 @@ class MainWindowMixin(object):
|
|||||||
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
|
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
|
||||||
self.location_manager = LocationManager(self)
|
self.location_manager = LocationManager(self)
|
||||||
|
|
||||||
|
self.init_scheduler(db)
|
||||||
all_actions = self.setup_actions()
|
all_actions = self.setup_actions()
|
||||||
|
|
||||||
self.search_bar = SearchBar(self)
|
self.search_bar = SearchBar(self)
|
||||||
@ -334,6 +336,11 @@ class MainWindowMixin(object):
|
|||||||
l = self.centralwidget.layout()
|
l = self.centralwidget.layout()
|
||||||
l.addWidget(self.search_bar)
|
l.addWidget(self.search_bar)
|
||||||
|
|
||||||
|
def init_scheduler(self, db):
|
||||||
|
self.scheduler = Scheduler(self, db)
|
||||||
|
self.scheduler.start_recipe_fetch.connect(
|
||||||
|
self.download_scheduled_recipe, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
|
||||||
def read_toolbar_settings(self):
|
def read_toolbar_settings(self):
|
||||||
pass
|
pass
|
||||||
@ -392,6 +399,10 @@ class MainWindowMixin(object):
|
|||||||
ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
|
ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
|
||||||
'tags.svg')
|
'tags.svg')
|
||||||
|
|
||||||
|
self.action_news.setMenu(self.scheduler.news_menu)
|
||||||
|
self.action_news.triggered.connect(
|
||||||
|
self.scheduler.show_dialog)
|
||||||
|
|
||||||
self.action_help.triggered.connect(self.show_help)
|
self.action_help.triggered.connect(self.show_help)
|
||||||
md = QMenu()
|
md = QMenu()
|
||||||
md.addAction(_('Edit metadata individually'),
|
md.addAction(_('Edit metadata individually'),
|
||||||
|
@ -218,9 +218,10 @@ class BooksView(QTableView): # {{{
|
|||||||
# Only save if we have been initialized (set_database called)
|
# Only save if we have been initialized (set_database called)
|
||||||
if len(self.column_map) > 0 and self.was_restored:
|
if len(self.column_map) > 0 and self.was_restored:
|
||||||
state = self.get_state()
|
state = self.get_state()
|
||||||
|
db = getattr(self.model(), 'db', None)
|
||||||
name = unicode(self.objectName())
|
name = unicode(self.objectName())
|
||||||
if name:
|
if name and db is not None:
|
||||||
gprefs.set(name + ' books view state', state)
|
db.prefs.set(name + ' books view state', state)
|
||||||
|
|
||||||
def cleanup_sort_history(self, sort_history):
|
def cleanup_sort_history(self, sort_history):
|
||||||
history = []
|
history = []
|
||||||
@ -298,11 +299,27 @@ class BooksView(QTableView): # {{{
|
|||||||
old_state['column_sizes'][name] += 12
|
old_state['column_sizes'][name] += 12
|
||||||
return old_state
|
return old_state
|
||||||
|
|
||||||
def restore_state(self):
|
def get_old_state(self):
|
||||||
|
ans = None
|
||||||
name = unicode(self.objectName())
|
name = unicode(self.objectName())
|
||||||
old_state = None
|
|
||||||
if name:
|
if name:
|
||||||
old_state = gprefs.get(name + ' books view state', None)
|
name += ' books view state'
|
||||||
|
db = getattr(self.model(), 'db', None)
|
||||||
|
if db is not None:
|
||||||
|
ans = db.prefs.get(name, None)
|
||||||
|
if ans is None:
|
||||||
|
ans = gprefs.get(name, None)
|
||||||
|
try:
|
||||||
|
del gprefs[name]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if ans is not None:
|
||||||
|
db.prefs[name] = ans
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def restore_state(self):
|
||||||
|
old_state = self.get_old_state()
|
||||||
if old_state is None:
|
if old_state is None:
|
||||||
old_state = self.get_default_state()
|
old_state = self.get_default_state()
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@ from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
|
|||||||
gprefs, max_available_height, config, info_dialog
|
gprefs, max_available_height, config, info_dialog
|
||||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
|
||||||
from calibre.gui2.update import UpdateMixin
|
from calibre.gui2.update import UpdateMixin
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
from calibre.gui2.layout import MainWindowMixin
|
from calibre.gui2.layout import MainWindowMixin
|
||||||
@ -119,7 +118,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
self.another_instance_wants_to_talk)
|
self.another_instance_wants_to_talk)
|
||||||
self.check_messages_timer.start(1000)
|
self.check_messages_timer.start(1000)
|
||||||
|
|
||||||
MainWindowMixin.__init__(self)
|
MainWindowMixin.__init__(self, db)
|
||||||
|
|
||||||
# Jobs Button {{{
|
# Jobs Button {{{
|
||||||
self.job_manager = JobManager()
|
self.job_manager = JobManager()
|
||||||
@ -253,18 +252,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
db, server_config().parse())
|
db, server_config().parse())
|
||||||
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
||||||
|
|
||||||
|
|
||||||
self.scheduler = Scheduler(self, self.library_view.model().db)
|
|
||||||
self.action_news.setMenu(self.scheduler.news_menu)
|
|
||||||
self.connect(self.action_news, SIGNAL('triggered(bool)'),
|
|
||||||
self.scheduler.show_dialog)
|
|
||||||
self.connect(self.scheduler, SIGNAL('delete_old_news(PyQt_PyObject)'),
|
|
||||||
self.library_view.model().delete_books_by_id,
|
|
||||||
Qt.QueuedConnection)
|
|
||||||
self.connect(self.scheduler,
|
|
||||||
SIGNAL('start_recipe_fetch(PyQt_PyObject)'),
|
|
||||||
self.download_scheduled_recipe, Qt.QueuedConnection)
|
|
||||||
|
|
||||||
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
||||||
AddAction.__init__(self)
|
AddAction.__init__(self)
|
||||||
|
|
||||||
@ -272,6 +259,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
self.finalize_layout()
|
self.finalize_layout()
|
||||||
self.donate_button.start_animation()
|
self.donate_button.start_animation()
|
||||||
|
|
||||||
|
self.scheduler.delete_old_news.connect(
|
||||||
|
self.library_view.model().delete_books_by_id,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
MainWindow.resizeEvent(self, ev)
|
MainWindow.resizeEvent(self, ev)
|
||||||
self.search.setMaximumWidth(self.width()-150)
|
self.search.setMaximumWidth(self.width()-150)
|
||||||
@ -402,6 +394,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
self.search.clear_to_help()
|
self.search.clear_to_help()
|
||||||
self.book_details.reset_info()
|
self.book_details.reset_info()
|
||||||
self.library_view.model().count_changed()
|
self.library_view.model().count_changed()
|
||||||
|
self.scheduler.database_changed(db)
|
||||||
prefs['library_path'] = self.library_path
|
prefs['library_path'] = self.library_path
|
||||||
|
|
||||||
def show_book_info(self, *args):
|
def show_book_info(self, *args):
|
||||||
|
@ -19,6 +19,7 @@ from calibre.library.schema_upgrades import SchemaUpgrade
|
|||||||
from calibre.library.caches import ResultCache
|
from calibre.library.caches import ResultCache
|
||||||
from calibre.library.custom_columns import CustomColumns
|
from calibre.library.custom_columns import CustomColumns
|
||||||
from calibre.library.sqlite import connect, IntegrityError, DBThread
|
from calibre.library.sqlite import connect, IntegrityError, DBThread
|
||||||
|
from calibre.library.prefs import DBPrefs
|
||||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
|
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
|
||||||
MetaInformation
|
MetaInformation
|
||||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||||
@ -140,6 +141,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.initialize_dynamic()
|
self.initialize_dynamic()
|
||||||
|
|
||||||
def initialize_dynamic(self):
|
def initialize_dynamic(self):
|
||||||
|
self.prefs = DBPrefs(self)
|
||||||
self.conn.executescript('''
|
self.conn.executescript('''
|
||||||
DROP TRIGGER IF EXISTS author_insert_trg;
|
DROP TRIGGER IF EXISTS author_insert_trg;
|
||||||
CREATE TEMP TRIGGER author_insert_trg
|
CREATE TEMP TRIGGER author_insert_trg
|
||||||
|
49
src/calibre/library/prefs.py
Normal file
49
src/calibre/library/prefs.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from calibre.constants import preferred_encoding
|
||||||
|
from calibre.utils.config import to_json, from_json
|
||||||
|
|
||||||
|
class DBPrefs(dict):
|
||||||
|
|
||||||
|
def __init__(self, db):
|
||||||
|
dict.__init__(self)
|
||||||
|
self.db = db
|
||||||
|
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
|
||||||
|
val = self.raw_to_object(val)
|
||||||
|
dict.__setitem__(self, key, val)
|
||||||
|
|
||||||
|
def raw_to_object(self, raw):
|
||||||
|
if not isinstance(raw, unicode):
|
||||||
|
raw = raw.decode(preferred_encoding)
|
||||||
|
return json.loads(raw, object_hook=from_json)
|
||||||
|
|
||||||
|
def to_raw(self, val):
|
||||||
|
return json.dumps(val, indent=2, default=to_json)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, key)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
dict.__delitem__(self, key)
|
||||||
|
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||||
|
self.db.conn.commit()
|
||||||
|
|
||||||
|
def __setitem__(self, key, val):
|
||||||
|
raw = self.to_raw(val)
|
||||||
|
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||||
|
self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key,
|
||||||
|
raw))
|
||||||
|
self.db.conn.commit()
|
||||||
|
dict.__setitem__(self, key, val)
|
||||||
|
|
||||||
|
def set(self, key, val):
|
||||||
|
self.__setitem__(key, val)
|
||||||
|
|
||||||
|
|
@ -387,3 +387,14 @@ class SchemaUpgrade(object):
|
|||||||
|
|
||||||
self.conn.execute('UPDATE authors SET sort=author_to_author_sort(name)')
|
self.conn.execute('UPDATE authors SET sort=author_to_author_sort(name)')
|
||||||
|
|
||||||
|
def upgrade_version_12(self):
|
||||||
|
'DB based preference store'
|
||||||
|
script = '''
|
||||||
|
DROP TABLE IF EXISTS preferences;
|
||||||
|
CREATE TABLE preferences(id INTEGER PRIMARY KEY,
|
||||||
|
key TEXT NON NULL,
|
||||||
|
val TEXT NON NULL,
|
||||||
|
UNIQUE(key));
|
||||||
|
'''
|
||||||
|
self.conn.executescript(script)
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -66,7 +66,7 @@ and save it to a new file.
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import ctypes, sys, os
|
import ctypes, sys, os, glob
|
||||||
from ctypes import util
|
from ctypes import util
|
||||||
iswindows = 'win32' in sys.platform or 'win64' in sys.platform
|
iswindows = 'win32' in sys.platform or 'win64' in sys.platform
|
||||||
isosx = 'darwin' in sys.platform
|
isosx = 'darwin' in sys.platform
|
||||||
@ -85,7 +85,8 @@ elif iswindows:
|
|||||||
_lib = flib if isfrozen else 'CORE_RL_wand_'
|
_lib = flib if isfrozen else 'CORE_RL_wand_'
|
||||||
else:
|
else:
|
||||||
if isfrozen:
|
if isfrozen:
|
||||||
_lib = os.path.join(sys.frozen_path, 'libMagickWand.so.2')
|
_lib = glob.glob(os.path.join(sys.frozen_path,
|
||||||
|
'libMagickWand.so.*'))[-1]
|
||||||
else:
|
else:
|
||||||
_lib = util.find_library('MagickWand')
|
_lib = util.find_library('MagickWand')
|
||||||
if _lib is None:
|
if _lib is None:
|
||||||
|
@ -6,15 +6,16 @@ __docformat__ = 'restructuredtext en'
|
|||||||
'''
|
'''
|
||||||
Manage application-wide preferences.
|
Manage application-wide preferences.
|
||||||
'''
|
'''
|
||||||
import os, re, cPickle, textwrap, traceback, plistlib, json, base64
|
import os, re, cPickle, textwrap, traceback, plistlib, json, base64, datetime
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from optparse import OptionParser as _OptionParser
|
from optparse import OptionParser as _OptionParser
|
||||||
from optparse import IndentedHelpFormatter
|
from optparse import IndentedHelpFormatter
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from calibre.constants import terminal_controller, iswindows, isosx, \
|
from calibre.constants import terminal_controller, iswindows, isosx, \
|
||||||
__appname__, __version__, __author__, plugins
|
__appname__, __version__, __author__, plugins
|
||||||
from calibre.utils.lock import LockError, ExclusiveFile
|
from calibre.utils.lock import LockError, ExclusiveFile
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
|
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
|
||||||
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
|
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
|
||||||
@ -632,27 +633,34 @@ class XMLConfig(dict):
|
|||||||
f.truncate()
|
f.truncate()
|
||||||
f.write(raw)
|
f.write(raw)
|
||||||
|
|
||||||
|
def to_json(obj):
|
||||||
|
if isinstance(obj, bytearray):
|
||||||
|
return {'__class__': 'bytearray',
|
||||||
|
'__value__': base64.standard_b64encode(bytes(obj))}
|
||||||
|
if isinstance(obj, datetime.datetime):
|
||||||
|
from calibre.utils.date import isoformat
|
||||||
|
return {'__class__': 'datetime.datetime',
|
||||||
|
'__value__': isoformat(obj, as_utc=True)}
|
||||||
|
raise TypeError(repr(obj) + ' is not JSON serializable')
|
||||||
|
|
||||||
|
def from_json(obj):
|
||||||
|
if '__class__' in obj:
|
||||||
|
if obj['__class__'] == 'bytearray':
|
||||||
|
return bytearray(base64.standard_b64decode(obj['__value__']))
|
||||||
|
if obj['__class__'] == 'datetime.datetime':
|
||||||
|
from calibre.utils.date import parse_date
|
||||||
|
return parse_date(obj['__value__'], assume_utc=True)
|
||||||
|
return obj
|
||||||
|
|
||||||
class JSONConfig(XMLConfig):
|
class JSONConfig(XMLConfig):
|
||||||
|
|
||||||
EXTENSION = '.json'
|
EXTENSION = '.json'
|
||||||
|
|
||||||
def to_json(self, obj):
|
|
||||||
if isinstance(obj, bytearray):
|
|
||||||
return {'__class__': 'bytearray',
|
|
||||||
'__value__': base64.standard_b64encode(bytes(obj))}
|
|
||||||
raise TypeError(repr(obj) + ' is not JSON serializable')
|
|
||||||
|
|
||||||
def from_json(self, obj):
|
|
||||||
if '__class__' in obj:
|
|
||||||
if obj['__class__'] == 'bytearray':
|
|
||||||
return bytearray(base64.standard_b64decode(obj['__value__']))
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def raw_to_object(self, raw):
|
def raw_to_object(self, raw):
|
||||||
return json.loads(raw.decode('utf-8'), object_hook=self.from_json)
|
return json.loads(raw.decode('utf-8'), object_hook=from_json)
|
||||||
|
|
||||||
def to_raw(self):
|
def to_raw(self):
|
||||||
return json.dumps(self, indent=2, default=self.to_json)
|
return json.dumps(self, indent=2, default=to_json)
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return dict.__getitem__(self, key)
|
return dict.__getitem__(self, key)
|
||||||
|
@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os, copy
|
import os, copy
|
||||||
|
|
||||||
from PyQt4.Qt import QAbstractItemModel, QVariant, Qt, QColor, QFont, QIcon, \
|
from PyQt4.Qt import QAbstractItemModel, QVariant, Qt, QColor, QFont, QIcon, \
|
||||||
QModelIndex, SIGNAL
|
QModelIndex, SIGNAL, QMetaObject, pyqtSlot
|
||||||
|
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.gui2 import NONE
|
from calibre.gui2 import NONE
|
||||||
@ -131,6 +131,16 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
|
|||||||
self.scheduler_config = SchedulerConfig()
|
self.scheduler_config = SchedulerConfig()
|
||||||
self.do_refresh()
|
self.do_refresh()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def do_database_change(self):
|
||||||
|
self.db = self.newdb
|
||||||
|
self.newdb = None
|
||||||
|
self.do_refresh()
|
||||||
|
|
||||||
|
def database_changed(self, db):
|
||||||
|
self.newdb = db
|
||||||
|
QMetaObject.invokeMethod(self, 'do_database_change', Qt.QueuedConnection)
|
||||||
|
|
||||||
def get_builtin_recipe(self, urn, download=True):
|
def get_builtin_recipe(self, urn, download=True):
|
||||||
if download:
|
if download:
|
||||||
try:
|
try:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user