mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
KG 0.7.9
This commit is contained in:
commit
174c09022a
@ -4,6 +4,72 @@
|
||||
# for important features/bug fixes.
|
||||
# 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
|
||||
date: 2010-07-09
|
||||
|
||||
|
38
resources/recipes/alternet.recipe
Normal file
38
resources/recipes/alternet.recipe
Normal file
@ -0,0 +1,38 @@
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Alternet(BasicNewsRecipe):
|
||||
title = u'Alternet'
|
||||
__author__= 'rty'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
publisher = 'alternet.org'
|
||||
category = 'News, Magazine'
|
||||
description = 'News magazine and online community'
|
||||
feeds = [
|
||||
(u'Front Page', u'http://feeds.feedblitz.com/alternet'),
|
||||
(u'Breaking News', u'http://feeds.feedblitz.com/alternet_breaking_news'),
|
||||
(u'Top Ten Campaigns', u'http://feeds.feedblitz.com/alternet_top_10_campaigns'),
|
||||
(u'Special Coverage Areas', u'http://feeds.feedblitz.com/alternet_coverage')
|
||||
]
|
||||
remove_attributes = ['width', 'align','cellspacing']
|
||||
remove_javascript = True
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
language = 'en'
|
||||
encoding = 'UTF-8'
|
||||
temp_files = []
|
||||
articles_are_obfuscated = True
|
||||
|
||||
def get_article_url(self, article):
|
||||
return article.get('link', None)
|
||||
|
||||
def get_obfuscated_article(self, url):
|
||||
br = self.get_browser()
|
||||
br.open(url)
|
||||
response = br.follow_link(url_regex = r'/printversion/[0-9]+', nr = 0)
|
||||
html = response.read()
|
||||
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
|
||||
self.temp_files[-1].write(html)
|
||||
self.temp_files[-1].close()
|
||||
return self.temp_files[-1].name
|
44
resources/recipes/technology_review.recipe
Normal file
44
resources/recipes/technology_review.recipe
Normal file
@ -0,0 +1,44 @@
|
||||
import string
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class TechnologyReview(BasicNewsRecipe):
|
||||
title = u'Technology Review'
|
||||
__author__ = 'rty'
|
||||
description = 'MIT Technology Magazine'
|
||||
publisher = 'Technology Review Inc.'
|
||||
category = 'Technology, Innovation, R&D'
|
||||
oldest_article = 14
|
||||
max_articles_per_feed = 100
|
||||
No_stylesheets = True
|
||||
extra_css = """
|
||||
.ArticleBody {font: normal; text-align: justify}
|
||||
.headline {font: bold x-large}
|
||||
.subheadline {font: italic large}
|
||||
"""
|
||||
feeds = [
|
||||
(u'Computing', u'http://feeds.technologyreview.com/technology_review_Computing'),
|
||||
(u'Web', u'http://feeds.technologyreview.com/technology_review_Web'),
|
||||
(u'Communications', u'http://feeds.technologyreview.com/technology_review_Communications'),
|
||||
(u'Energy', u'http://feeds.technologyreview.com/technology_review_Energy'),
|
||||
(u'Materials', u'http://feeds.technologyreview.com/technology_review_Materials'),
|
||||
(u'Biomedicine', u'http://feeds.technologyreview.com/technology_review_Biotech'),
|
||||
(u'Business', u'http://feeds.technologyreview.com/technology_review_Biztech')
|
||||
]
|
||||
remove_attributes = ['width', 'align','cellspacing']
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'id':['CloseLink','footerAdDiv','copyright']}),
|
||||
]
|
||||
remove_tags_after = [dict(name='div', attrs={'id':'copyright'})]
|
||||
|
||||
def get_article_url(self, article):
|
||||
return article.get('guid', article.get('id', None))
|
||||
|
||||
|
||||
def print_version(self, url):
|
||||
baseurl='http://www.technologyreview.com/printer_friendly_article.aspx?id='
|
||||
split1 = string.split(url,"/")
|
||||
xxx=split1 [4]
|
||||
split2= string.split(xxx,"/")
|
||||
s = baseurl + split2[0]
|
||||
return s
|
@ -265,9 +265,20 @@
|
||||
<xsl:value-of select="@line-height"/>
|
||||
<xsl:text>pt;</xsl:text>
|
||||
</xsl:if>
|
||||
<xsl:if test="(@align = 'just')">
|
||||
<xsl:text>text-align: justify;</xsl:text>
|
||||
</xsl:if>
|
||||
<xsl:if test="(@align = 'cent')">
|
||||
<xsl:text>text-align: center;</xsl:text>
|
||||
</xsl:if>
|
||||
<xsl:if test="(@align = 'left')">
|
||||
<xsl:text>text-align: left;</xsl:text>
|
||||
</xsl:if>
|
||||
<xsl:if test="(@align = 'right')">
|
||||
<xsl:text>text-align: right;</xsl:text>
|
||||
</xsl:if>
|
||||
</xsl:template>
|
||||
|
||||
|
||||
<xsl:template match="rtf:inline">
|
||||
<xsl:variable name="num-attrs" select="count(@*)"/>
|
||||
<xsl:choose>
|
||||
|
@ -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):
|
||||
raw = _ent_pat.sub(entity_to_unicode, raw)
|
||||
raw = raw.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.8'
|
||||
__version__ = '0.7.9'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
@ -60,6 +60,9 @@ class FB2Input(InputFormatPlugin):
|
||||
|
||||
transform = etree.XSLT(styledoc)
|
||||
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))
|
||||
stream.seek(0)
|
||||
mi = get_metadata(stream, 'fb2')
|
||||
@ -83,9 +86,15 @@ class FB2Input(InputFormatPlugin):
|
||||
return os.path.join(os.getcwd(), 'metadata.opf')
|
||||
|
||||
def extract_embedded_content(self, doc):
|
||||
self.binary_map = {}
|
||||
for elem in doc.xpath('./*'):
|
||||
if 'binary' in elem.tag and elem.attrib.has_key('id'):
|
||||
ct = elem.get('content-type', '')
|
||||
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())
|
||||
open(fname, 'wb').write(data)
|
||||
|
||||
|
@ -368,7 +368,15 @@ class LRFInput(InputFormatPlugin):
|
||||
if options.verbose > 2:
|
||||
open('lrs.xml', 'wb').write(xml.encode('utf-8'))
|
||||
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 = {}
|
||||
for x in doc.xpath('//CharButton[@refobj]'):
|
||||
ro = x.get('refobj')
|
||||
|
@ -14,7 +14,8 @@ except ImportError:
|
||||
|
||||
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.date import parse_date
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
@ -70,7 +71,7 @@ class EXTHHeader(object):
|
||||
#else:
|
||||
# print 'unknown record', id, repr(content)
|
||||
if title:
|
||||
self.mi.title = title
|
||||
self.mi.title = replace_entities(title)
|
||||
|
||||
def process_metadata(self, id, content, codec):
|
||||
if id == 100:
|
||||
|
@ -475,7 +475,8 @@ class Style(object):
|
||||
value = float(m.group(1))
|
||||
unit = m.group(2)
|
||||
if unit == '%':
|
||||
base = base or self.width
|
||||
if base is None:
|
||||
base = self.width
|
||||
result = (value / 100.0) * base
|
||||
elif unit == 'px':
|
||||
result = value * 72.0 / self._profile.dpi
|
||||
|
@ -192,12 +192,18 @@ class RTFInput(InputFormatPlugin):
|
||||
from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException
|
||||
self.log = log
|
||||
self.log('Converting RTF to XML...')
|
||||
#Name of the preprocesssed RTF file
|
||||
fname = self.preprocess(stream.name)
|
||||
try:
|
||||
xml = self.generate_xml(fname)
|
||||
except RtfInvalidCodeException, e:
|
||||
raise ValueError(_('This RTF file has a feature calibre does not '
|
||||
'support. Convert it to HTML first and then try it.\n%s')%e)
|
||||
|
||||
'''dataxml = open('dataxml.xml', 'w')
|
||||
dataxml.write(xml)
|
||||
dataxml.close'''
|
||||
|
||||
d = glob.glob(os.path.join('*_rtf_pict_dir', 'picts.rtf'))
|
||||
if d:
|
||||
imap = {}
|
||||
@ -205,6 +211,7 @@ class RTFInput(InputFormatPlugin):
|
||||
imap = self.extract_images(d[0])
|
||||
except:
|
||||
self.log.exception('Failed to extract images...')
|
||||
|
||||
self.log('Parsing XML...')
|
||||
parser = etree.XMLParser(recover=True, no_network=True)
|
||||
doc = etree.fromstring(xml, parser=parser)
|
||||
@ -214,10 +221,10 @@ class RTFInput(InputFormatPlugin):
|
||||
name = imap.get(num, None)
|
||||
if name is not None:
|
||||
pict.set('num', name)
|
||||
|
||||
self.log('Converting XML to HTML...')
|
||||
inline_class = InlineClass(self.log)
|
||||
styledoc = etree.fromstring(P('templates/rtf.xsl', data=True))
|
||||
|
||||
extensions = { ('calibre', 'inline-class') : inline_class }
|
||||
transform = etree.XSLT(styledoc, extensions=extensions)
|
||||
result = transform(doc)
|
||||
|
@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import os, sys
|
||||
from threading import RLock
|
||||
|
||||
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
||||
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
|
||||
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
|
||||
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||
@ -33,10 +33,6 @@ def _config():
|
||||
help=_('Send file to storage card instead of main memory by default'))
|
||||
c.add_opt('confirm_delete', default=False,
|
||||
help=_('Confirm before deleting'))
|
||||
c.add_opt('toolbar_icon_size', default=QSize(48, 48),
|
||||
help=_('Toolbar icon size')) # value QVariant.toSize
|
||||
c.add_opt('show_text_in_toolbar', default=True,
|
||||
help=_('Show button labels in the toolbar'))
|
||||
c.add_opt('main_window_geometry', default=None,
|
||||
help=_('Main window geometry')) # value QVariant.toByteArray
|
||||
c.add_opt('new_version_notification', default=True,
|
||||
|
@ -58,7 +58,7 @@
|
||||
<item row="9" column="0" colspan="2">
|
||||
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<item row="10" column="0" colspan="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
@ -43,12 +43,6 @@
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimumContentsLength">
|
||||
<number>30</number>
|
||||
</property>
|
||||
|
@ -638,7 +638,6 @@ class DeviceMixin(object): # {{{
|
||||
self.device_error_dialog = error_dialog(self, _('Error'),
|
||||
_('Error communicating with device'), ' ')
|
||||
self.device_error_dialog.setModal(Qt.NonModal)
|
||||
self.device_connected = None
|
||||
self.emailer = Emailer()
|
||||
self.emailer.start()
|
||||
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
||||
@ -755,17 +754,14 @@ class DeviceMixin(object): # {{{
|
||||
self.device_manager.device.__class__.get_gui_name()+\
|
||||
_(' detected.'), 3000)
|
||||
self.device_connected = device_kind
|
||||
self.location_view.model().device_connected(self.device_manager.device)
|
||||
self.refresh_ondevice_info (device_connected = True, reset_only = True)
|
||||
else:
|
||||
self.device_connected = None
|
||||
self.status_bar.device_disconnected()
|
||||
self.location_view.model().update_devices()
|
||||
if self.current_view() != self.library_view:
|
||||
self.book_details.reset_info()
|
||||
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
||||
self.refresh_ondevice_info (device_connected = False)
|
||||
self.tool_bar.device_status_changed(bool(connected))
|
||||
self.location_manager.update_devices()
|
||||
self.refresh_ondevice_info(device_connected=False)
|
||||
|
||||
def info_read(self, job):
|
||||
'''
|
||||
@ -774,7 +770,8 @@ class DeviceMixin(object): # {{{
|
||||
if job.failed:
|
||||
return self.device_job_exception(job)
|
||||
info, cp, fs = job.result
|
||||
self.location_view.model().update_devices(cp, fs)
|
||||
self.location_manager.update_devices(cp, fs,
|
||||
self.device_manager.device.icon)
|
||||
self.status_bar.device_connected(info[0])
|
||||
self.device_manager.books(Dispatcher(self.metadata_downloaded))
|
||||
|
||||
@ -1076,9 +1073,9 @@ class DeviceMixin(object): # {{{
|
||||
dynamic.set('catalogs_to_be_synced', set([]))
|
||||
if files:
|
||||
remove = []
|
||||
space = { self.location_view.model().free[0] : None,
|
||||
self.location_view.model().free[1] : 'carda',
|
||||
self.location_view.model().free[2] : 'cardb' }
|
||||
space = { self.location_manager.free[0] : None,
|
||||
self.location_manager.free[1] : 'carda',
|
||||
self.location_manager.free[2] : 'cardb' }
|
||||
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
|
||||
self.upload_books(files, names, metadata,
|
||||
on_card=on_card,
|
||||
@ -1140,9 +1137,9 @@ class DeviceMixin(object): # {{{
|
||||
dynamic.set('news_to_be_synced', set([]))
|
||||
if config['upload_news_to_device'] and files:
|
||||
remove = ids if del_on_upload else []
|
||||
space = { self.location_view.model().free[0] : None,
|
||||
self.location_view.model().free[1] : 'carda',
|
||||
self.location_view.model().free[2] : 'cardb' }
|
||||
space = { self.location_manager.free[0] : None,
|
||||
self.location_manager.free[1] : 'carda',
|
||||
self.location_manager.free[2] : 'cardb' }
|
||||
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
|
||||
self.upload_books(files, names, metadata,
|
||||
on_card=on_card,
|
||||
@ -1263,7 +1260,8 @@ class DeviceMixin(object): # {{{
|
||||
self.device_job_exception(job)
|
||||
return
|
||||
cp, fs = job.result
|
||||
self.location_view.model().update_devices(cp, fs)
|
||||
self.location_manager.update_devices(cp, fs,
|
||||
self.device_manager.device.icon)
|
||||
# reset the views so that up-to-date info is shown. These need to be
|
||||
# here because the sony driver updates collections in sync_booklists
|
||||
self.memory_view.reset()
|
||||
|
90
src/calibre/gui2/dialogs/choose_library.py
Normal file
90
src/calibre/gui2/dialogs/choose_library.py
Normal file
@ -0,0 +1,90 @@
|
||||
#!/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 os
|
||||
|
||||
from PyQt4.Qt import QDialog
|
||||
|
||||
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
|
||||
from calibre.gui2 import error_dialog, choose_dir, warning_dialog
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre import isbytestring, patheq
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.gui2.wizard import move_library
|
||||
|
||||
class ChooseLibrary(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, db, callback, parent):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
self.db = db
|
||||
self.new_db = None
|
||||
self.callback = callback
|
||||
self.location.initialize('choose_library_dialog')
|
||||
|
||||
lp = db.library_path
|
||||
if isbytestring(lp):
|
||||
lp = lp.decode(filesystem_encoding)
|
||||
loc = unicode(self.old_location.text()).format(lp)
|
||||
self.old_location.setText(loc)
|
||||
self.browse_button.clicked.connect(self.choose_loc)
|
||||
|
||||
def choose_loc(self, *args):
|
||||
loc = choose_dir(self, 'choose library location',
|
||||
_('Choose location for calibre library'))
|
||||
if loc is not None:
|
||||
self.location.setText(loc)
|
||||
|
||||
def check_action(self, ac, loc):
|
||||
exists = self.db.exists_at(loc)
|
||||
if patheq(loc, self.db.library_path):
|
||||
error_dialog(self, _('Same as current'),
|
||||
_('The location %s contains the current calibre'
|
||||
' library')%loc, show=True)
|
||||
return False
|
||||
empty = not os.listdir(loc)
|
||||
if ac == 'existing' and not exists:
|
||||
error_dialog(self, _('No existing library found'),
|
||||
_('There is no existing calibre library at %s')%loc,
|
||||
show=True)
|
||||
return False
|
||||
if ac in ('new', 'move') and not empty:
|
||||
error_dialog(self, _('Not empty'),
|
||||
_('The folder %s is not empty. Please choose an empty'
|
||||
' folder')%loc,
|
||||
show=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
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'):
|
||||
prefs['library_path'] = loc
|
||||
self.callback(loc)
|
||||
else:
|
||||
move_library(self.db.library_path, loc, self.parent(),
|
||||
self.callback)
|
||||
|
||||
def accept(self):
|
||||
action = 'move'
|
||||
if self.existing_library.isChecked():
|
||||
action = 'existing'
|
||||
elif self.empty_library.isChecked():
|
||||
action = 'new'
|
||||
loc = os.path.abspath(unicode(self.location.text()).strip())
|
||||
if not loc or not os.path.exists(loc) or not self.check_action(action,
|
||||
loc):
|
||||
return
|
||||
QDialog.accept(self)
|
||||
self.location.save_history()
|
||||
self.perform_action(action, loc)
|
174
src/calibre/gui2/dialogs/choose_library.ui
Normal file
174
src/calibre/gui2/dialogs/choose_library.ui
Normal file
@ -0,0 +1,174 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>602</width>
|
||||
<height>245</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Choose your calibre library</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="4">
|
||||
<widget class="QLabel" name="old_location">
|
||||
<property name="text">
|
||||
<string>Your calibre library is currently located at {0}</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>New &Location:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>location</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="4">
|
||||
<widget class="QRadioButton" name="existing_library">
|
||||
<property name="text">
|
||||
<string>Use &existing library at the new location</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="3">
|
||||
<widget class="QRadioButton" name="empty_library">
|
||||
<property name="text">
|
||||
<string>&Create an empty library at the new location</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="3">
|
||||
<widget class="QRadioButton" name="move_library">
|
||||
<property name="text">
|
||||
<string>&Move current library to new location</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="2">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QToolButton" name="browse_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="HistoryLineEdit" name="location"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>HistoryLineEdit</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>calibre/gui2/widgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -14,7 +14,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
||||
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
|
||||
from calibre.gui2 import error_dialog, config, gprefs, \
|
||||
open_url, open_local_file, \
|
||||
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
||||
warning_dialog, ResizableDialog, question_dialog
|
||||
@ -343,9 +343,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.model = library_view.model()
|
||||
self.db = self.model.db
|
||||
self.server = server
|
||||
path = prefs['library_path']
|
||||
self.location.setText(path if path else '')
|
||||
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
|
||||
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
|
||||
|
||||
input_map = prefs['input_format_order']
|
||||
@ -808,12 +805,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
d = CheckIntegrity(self.db, self)
|
||||
d.exec_()
|
||||
|
||||
def browse(self):
|
||||
dir = choose_dir(self, 'database location dialog',
|
||||
_('Select location for books'))
|
||||
if dir:
|
||||
self.location.setText(dir)
|
||||
|
||||
def accept(self):
|
||||
mcs = unicode(self.max_cover_size.text()).strip()
|
||||
if not re.match(r'\d+x\d+', mcs):
|
||||
@ -834,7 +825,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
|
||||
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
|
||||
prefs['network_timeout'] = int(self.timeout.value())
|
||||
path = unicode(self.location.text())
|
||||
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
|
||||
prefs['input_format_order'] = input_cols
|
||||
|
||||
@ -875,24 +865,13 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString()
|
||||
config['gui_layout'] = unicode(val)
|
||||
|
||||
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
||||
d = error_dialog(self, _('Invalid database location'),
|
||||
_('Invalid database location ')+path+
|
||||
_('<br>Must be a directory.'))
|
||||
d.exec_()
|
||||
elif not os.access(path, os.W_OK):
|
||||
d = error_dialog(self, _('Invalid database location'),
|
||||
_('Invalid database location.<br>Cannot write to ')+path)
|
||||
d.exec_()
|
||||
else:
|
||||
self.database_location = os.path.abspath(path)
|
||||
if must_restart:
|
||||
warning_dialog(self, _('Must restart'),
|
||||
_('The changes you made require that Calibre be '
|
||||
'restarted. Please restart as soon as practical.'),
|
||||
show=True, show_copy_button=False)
|
||||
self.parent.must_restart_before_config = True
|
||||
QDialog.accept(self)
|
||||
if must_restart:
|
||||
warning_dialog(self, _('Must restart'),
|
||||
_('The changes you made require that Calibre be '
|
||||
'restarted. Please restart as soon as practical.'),
|
||||
show=True, show_copy_button=False)
|
||||
self.parent.must_restart_before_config = True
|
||||
QDialog.accept(self)
|
||||
|
||||
class VacThread(QThread):
|
||||
|
||||
|
@ -113,50 +113,6 @@
|
||||
</property>
|
||||
<widget class="QWidget" name="page_3">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>70</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Location of ebooks (The ebooks are stored in folders sorted by author and metadata is stored in the file metadata.db)</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>location</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="_3">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="location"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="browse_button">
|
||||
<property name="toolTip">
|
||||
<string>Browse for the new database location</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../../resources/images.qrc">
|
||||
<normaloff>:/images/mimetypes/dir.svg</normaloff>:/images/mimetypes/dir.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="new_version_notification">
|
||||
<property name="text">
|
||||
|
@ -60,7 +60,7 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="stop_all_jobs_button">
|
||||
<property name="text">
|
||||
<string>Stop &all jobs</string>
|
||||
<string>Stop &all non device jobs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -10,7 +10,7 @@ Scheduler for automated recipe downloads
|
||||
from datetime import timedelta
|
||||
|
||||
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.search_box import SearchBox2
|
||||
@ -203,6 +203,9 @@ class Scheduler(QObject):
|
||||
|
||||
INTERVAL = 1 # minutes
|
||||
|
||||
delete_old_news = pyqtSignal(object)
|
||||
start_recipe_fetch = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, db):
|
||||
QObject.__init__(self, parent)
|
||||
self.internet_connection_failed = False
|
||||
@ -225,20 +228,23 @@ class Scheduler(QObject):
|
||||
self.download_all_scheduled)
|
||||
|
||||
self.timer = QTimer(self)
|
||||
self.timer.start(int(self.INTERVAL * 60000))
|
||||
self.timer.start(int(self.INTERVAL * 60 * 1000))
|
||||
self.oldest_timer = QTimer()
|
||||
self.connect(self.oldest_timer, SIGNAL('timeout()'), self.oldest_check)
|
||||
self.connect(self.timer, SIGNAL('timeout()'), self.check)
|
||||
self.oldest = gconf['oldest_news']
|
||||
self.oldest_timer.start(int(60 * 60000))
|
||||
self.oldest_check()
|
||||
self.oldest_timer.start(int(60 * 60 * 1000))
|
||||
QTimer.singleShot(5 * 1000, self.oldest_check)
|
||||
self.database_changed = self.recipe_model.database_changed
|
||||
|
||||
def oldest_check(self):
|
||||
if self.oldest > 0:
|
||||
delta = timedelta(days=self.oldest)
|
||||
ids = self.recipe_model.db.tags_older_than(_('News'), delta)
|
||||
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):
|
||||
self.lock.lock()
|
||||
@ -282,7 +288,7 @@ class Scheduler(QObject):
|
||||
'urn':urn,
|
||||
}
|
||||
self.download_queue.add(urn)
|
||||
self.emit(SIGNAL('start_recipe_fetch(PyQt_PyObject)'), arg)
|
||||
self.start_recipe_fetch.emit(arg)
|
||||
finally:
|
||||
self.lock.unlock()
|
||||
|
||||
|
@ -7,14 +7,13 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import functools, sys, os
|
||||
|
||||
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QIcon, QStackedWidget, \
|
||||
QSize, QSizePolicy, QStatusBar, QUrl, QLabel, QFont
|
||||
from PyQt4.Qt import QMenu, Qt, QStackedWidget, \
|
||||
QSize, QSizePolicy, QStatusBar, QLabel, QFont
|
||||
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.constants import isosx, __appname__, preferred_encoding, \
|
||||
__version__
|
||||
from calibre.gui2 import config, is_widescreen, open_url
|
||||
from calibre.gui2 import config, is_widescreen
|
||||
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
||||
from calibre.gui2.widgets import Splitter
|
||||
from calibre.gui2.tag_view import TagBrowserWidget
|
||||
@ -28,157 +27,6 @@ def partial(*args, **kwargs):
|
||||
_keep_refs.append(ans)
|
||||
return ans
|
||||
|
||||
class SaveMenu(QMenu): # {{{
|
||||
|
||||
save_fmt = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent):
|
||||
QMenu.__init__(self, _('Save single format to disk...'), parent)
|
||||
for ext in sorted(BOOK_EXTENSIONS):
|
||||
action = self.addAction(ext.upper())
|
||||
setattr(self, 'do_'+ext, partial(self.do, ext))
|
||||
action.triggered.connect(
|
||||
getattr(self, 'do_'+ext))
|
||||
|
||||
def do(self, ext, *args):
|
||||
self.save_fmt.emit(ext)
|
||||
|
||||
# }}}
|
||||
|
||||
class ToolbarMixin(object): # {{{
|
||||
|
||||
def __init__(self):
|
||||
self.action_help.triggered.connect(self.show_help)
|
||||
md = QMenu()
|
||||
md.addAction(_('Edit metadata individually'),
|
||||
partial(self.edit_metadata, False, bulk=False))
|
||||
md.addSeparator()
|
||||
md.addAction(_('Edit metadata in bulk'),
|
||||
partial(self.edit_metadata, False, bulk=True))
|
||||
md.addSeparator()
|
||||
md.addAction(_('Download metadata and covers'),
|
||||
partial(self.download_metadata, False, covers=True),
|
||||
Qt.ControlModifier+Qt.Key_D)
|
||||
md.addAction(_('Download only metadata'),
|
||||
partial(self.download_metadata, False, covers=False))
|
||||
md.addAction(_('Download only covers'),
|
||||
partial(self.download_metadata, False, covers=True,
|
||||
set_metadata=False, set_social_metadata=False))
|
||||
md.addAction(_('Download only social metadata'),
|
||||
partial(self.download_metadata, False, covers=False,
|
||||
set_metadata=False, set_social_metadata=True))
|
||||
self.metadata_menu = md
|
||||
|
||||
mb = QMenu()
|
||||
mb.addAction(_('Merge into first selected book - delete others'),
|
||||
self.merge_books)
|
||||
mb.addSeparator()
|
||||
mb.addAction(_('Merge into first selected book - keep others'),
|
||||
partial(self.merge_books, safe_merge=True))
|
||||
self.merge_menu = mb
|
||||
self.action_merge.setMenu(mb)
|
||||
md.addSeparator()
|
||||
md.addAction(self.action_merge)
|
||||
|
||||
self.add_menu = QMenu()
|
||||
self.add_menu.addAction(_('Add books from a single directory'),
|
||||
self.add_books)
|
||||
self.add_menu.addAction(_('Add books from directories, including '
|
||||
'sub-directories (One book per directory, assumes every ebook '
|
||||
'file is the same book in a different format)'),
|
||||
self.add_recursive_single)
|
||||
self.add_menu.addAction(_('Add books from directories, including '
|
||||
'sub directories (Multiple books per directory, assumes every '
|
||||
'ebook file is a different book)'), self.add_recursive_multiple)
|
||||
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
|
||||
'formats)'), self.add_empty)
|
||||
self.action_add.setMenu(self.add_menu)
|
||||
self.action_add.triggered.connect(self.add_books)
|
||||
self.action_del.triggered.connect(self.delete_books)
|
||||
self.action_edit.triggered.connect(self.edit_metadata)
|
||||
self.action_merge.triggered.connect(self.merge_books)
|
||||
|
||||
self.action_save.triggered.connect(self.save_to_disk)
|
||||
self.save_menu = QMenu()
|
||||
self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk,
|
||||
False))
|
||||
self.save_menu.addAction(_('Save to disk in a single directory'),
|
||||
partial(self.save_to_single_dir, False))
|
||||
self.save_menu.addAction(_('Save only %s format to disk')%
|
||||
prefs['output_format'].upper(),
|
||||
partial(self.save_single_format_to_disk, False))
|
||||
self.save_menu.addAction(
|
||||
_('Save only %s format to disk in a single directory')%
|
||||
prefs['output_format'].upper(),
|
||||
partial(self.save_single_fmt_to_single_dir, False))
|
||||
self.save_sub_menu = SaveMenu(self)
|
||||
self.save_menu.addMenu(self.save_sub_menu)
|
||||
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
|
||||
|
||||
self.action_view.triggered.connect(self.view_book)
|
||||
self.view_menu = QMenu()
|
||||
self.view_menu.addAction(_('View'), partial(self.view_book, False))
|
||||
ac = self.view_menu.addAction(_('View specific format'))
|
||||
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
|
||||
self.action_view.setMenu(self.view_menu)
|
||||
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
|
||||
|
||||
self.delete_menu = QMenu()
|
||||
self.delete_menu.addAction(_('Remove selected books'), self.delete_books)
|
||||
self.delete_menu.addAction(
|
||||
_('Remove files of a specific format from selected books..'),
|
||||
self.delete_selected_formats)
|
||||
self.delete_menu.addAction(
|
||||
_('Remove all formats from selected books, except...'),
|
||||
self.delete_all_but_selected_formats)
|
||||
self.delete_menu.addAction(
|
||||
_('Remove covers from selected books'), self.delete_covers)
|
||||
self.delete_menu.addSeparator()
|
||||
self.delete_menu.addAction(
|
||||
_('Remove matching books from device'),
|
||||
self.remove_matching_books_from_device)
|
||||
self.action_del.setMenu(self.delete_menu)
|
||||
|
||||
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
||||
self.addAction(self.action_open_containing_folder)
|
||||
self.action_open_containing_folder.triggered.connect(self.view_folder)
|
||||
self.action_sync.setShortcut(Qt.Key_D)
|
||||
self.action_sync.setEnabled(True)
|
||||
self.create_device_menu()
|
||||
self.action_sync.triggered.connect(
|
||||
self._sync_action_triggered)
|
||||
|
||||
self.action_edit.setMenu(md)
|
||||
self.action_save.setMenu(self.save_menu)
|
||||
|
||||
cm = QMenu()
|
||||
cm.addAction(_('Convert individually'), partial(self.convert_ebook,
|
||||
False, bulk=False))
|
||||
cm.addAction(_('Bulk convert'),
|
||||
partial(self.convert_ebook, False, bulk=True))
|
||||
cm.addSeparator()
|
||||
ac = cm.addAction(
|
||||
_('Create catalog of books in your calibre library'))
|
||||
ac.triggered.connect(self.generate_catalog)
|
||||
self.action_convert.setMenu(cm)
|
||||
self.action_convert.triggered.connect(self.convert_ebook)
|
||||
self.convert_menu = cm
|
||||
|
||||
pm = QMenu()
|
||||
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
|
||||
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
|
||||
self.run_wizard)
|
||||
self.action_preferences.setMenu(pm)
|
||||
self.preferences_menu = pm
|
||||
for x in (self.preferences_action, self.action_preferences):
|
||||
x.triggered.connect(self.do_config)
|
||||
|
||||
def show_help(self, *args):
|
||||
open_url(QUrl('http://calibre-ebook.com/user_manual'))
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
class LibraryViewMixin(object): # {{{
|
||||
|
||||
def __init__(self, db):
|
||||
|
@ -6,108 +6,106 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from operator import attrgetter
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, QVariant, \
|
||||
QAbstractListModel, QFont, QApplication, QPalette, pyqtSignal, QToolButton, \
|
||||
QModelIndex, QListView, QAbstractButton, QPainter, QPixmap, QColor, \
|
||||
QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout
|
||||
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \
|
||||
pyqtSignal, QToolButton, \
|
||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup, \
|
||||
QMenu, QUrl
|
||||
|
||||
from calibre.constants import __appname__, filesystem_encoding
|
||||
from calibre.constants import __appname__, isosx
|
||||
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
|
||||
from calibre.gui2.throbber import ThrobbingButton
|
||||
from calibre.gui2 import NONE, config
|
||||
from calibre.gui2 import config, open_url
|
||||
from calibre.gui2.widgets import ComboBoxWithHelp
|
||||
from calibre import human_readable
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||
|
||||
ICON_SIZE = 48
|
||||
|
||||
# Location View {{{
|
||||
class SaveMenu(QMenu): # {{{
|
||||
|
||||
class LocationModel(QAbstractListModel): # {{{
|
||||
|
||||
devicesChanged = pyqtSignal()
|
||||
save_fmt = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent):
|
||||
QAbstractListModel.__init__(self, parent)
|
||||
self.icons = [QVariant(QIcon(I('library.png'))),
|
||||
QVariant(QIcon(I('reader.svg'))),
|
||||
QVariant(QIcon(I('sd.svg'))),
|
||||
QVariant(QIcon(I('sd.svg')))]
|
||||
self.text = [_('Library\n%d books'),
|
||||
_('Reader\n%s'),
|
||||
_('Card A\n%s'),
|
||||
_('Card B\n%s')]
|
||||
QMenu.__init__(self, _('Save single format to disk...'), parent)
|
||||
for ext in sorted(BOOK_EXTENSIONS):
|
||||
action = self.addAction(ext.upper())
|
||||
setattr(self, 'do_'+ext, partial(self.do, ext))
|
||||
action.triggered.connect(
|
||||
getattr(self, 'do_'+ext))
|
||||
|
||||
def do(self, ext, *args):
|
||||
self.save_fmt.emit(ext)
|
||||
|
||||
# }}}
|
||||
|
||||
class LocationManager(QObject): # {{{
|
||||
|
||||
locations_changed = pyqtSignal()
|
||||
unmount_device = pyqtSignal()
|
||||
location_selected = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.free = [-1, -1, -1]
|
||||
self.count = 0
|
||||
self.highlight_row = 0
|
||||
self.library_tooltip = _('Click to see the books available on your computer')
|
||||
self.tooltips = [
|
||||
self.library_tooltip,
|
||||
_('Click to see the books in the main memory of your reader'),
|
||||
_('Click to see the books on storage card A in your reader'),
|
||||
_('Click to see the books on storage card B in your reader')
|
||||
]
|
||||
self.location_actions = QActionGroup(self)
|
||||
self.location_actions.setExclusive(True)
|
||||
self.current_location = 'library'
|
||||
self._mem = []
|
||||
self.tooltips = {}
|
||||
|
||||
def database_changed(self, db):
|
||||
lp = db.library_path
|
||||
if not isinstance(lp, unicode):
|
||||
lp = lp.decode(filesystem_encoding, 'replace')
|
||||
self.tooltips[0] = self.library_tooltip + '\n\n' + \
|
||||
_('Books located at') + ' ' + lp
|
||||
self.dataChanged.emit(self.index(0), self.index(0))
|
||||
def ac(name, text, icon, tooltip):
|
||||
icon = QIcon(I(icon))
|
||||
ac = self.location_actions.addAction(icon, text)
|
||||
setattr(self, 'location_'+name, ac)
|
||||
ac.setAutoRepeat(False)
|
||||
ac.setCheckable(True)
|
||||
receiver = partial(self._location_selected, name)
|
||||
ac.triggered.connect(receiver)
|
||||
self.tooltips[name] = tooltip
|
||||
if name != 'library':
|
||||
m = QMenu(parent)
|
||||
self._mem.append(m)
|
||||
a = m.addAction(icon, tooltip)
|
||||
a.triggered.connect(receiver)
|
||||
self._mem.append(a)
|
||||
a = m.addAction(QIcon(I('eject.svg')), _('Eject this device'))
|
||||
a.triggered.connect(self._eject_requested)
|
||||
ac.setMenu(m)
|
||||
self._mem.append(a)
|
||||
else:
|
||||
ac.setToolTip(tooltip)
|
||||
|
||||
def rowCount(self, *args):
|
||||
return 1 + len([i for i in self.free if i >= 0])
|
||||
return ac
|
||||
|
||||
def get_device_row(self, row):
|
||||
if row == 2 and self.free[1] == -1 and self.free[2] > -1:
|
||||
row = 3
|
||||
return row
|
||||
ac('library', _('Library'), 'lt.png',
|
||||
_('Show books in calibre library'))
|
||||
ac('main', _('Reader'), 'reader.svg',
|
||||
_('Show books in the main memory of the device'))
|
||||
ac('carda', _('Card A'), 'sd.svg',
|
||||
_('Show books in storage card A'))
|
||||
ac('cardb', _('Card B'), 'sd.svg',
|
||||
_('Show books in storage card B'))
|
||||
|
||||
def get_tooltip(self, row, drow):
|
||||
ans = self.tooltips[row]
|
||||
if row > 0:
|
||||
fs = self.free[drow-1]
|
||||
if fs > -1:
|
||||
ans += '\n\n%s '%(human_readable(fs)) + _('free')
|
||||
return ans
|
||||
def _location_selected(self, location, *args):
|
||||
if location != self.current_location and hasattr(self,
|
||||
'location_'+location):
|
||||
self.current_location = location
|
||||
self.location_selected.emit(location)
|
||||
getattr(self, 'location_'+location).setChecked(True)
|
||||
|
||||
def data(self, index, role):
|
||||
row = index.row()
|
||||
drow = self.get_device_row(row)
|
||||
data = NONE
|
||||
if role == Qt.DisplayRole:
|
||||
text = self.text[drow]%(human_readable(self.free[drow-1])) if row > 0 \
|
||||
else self.text[drow]%self.count
|
||||
data = QVariant(text)
|
||||
elif role == Qt.DecorationRole:
|
||||
data = self.icons[drow]
|
||||
elif role in (Qt.ToolTipRole, Qt.StatusTipRole):
|
||||
ans = self.get_tooltip(row, drow)
|
||||
data = QVariant(ans)
|
||||
elif role == Qt.SizeHintRole:
|
||||
data = QVariant(QSize(155, 90))
|
||||
elif role == Qt.FontRole:
|
||||
font = QFont('monospace')
|
||||
font.setBold(row == self.highlight_row)
|
||||
data = QVariant(font)
|
||||
elif role == Qt.ForegroundRole and row == self.highlight_row:
|
||||
return QVariant(QApplication.palette().brush(
|
||||
QPalette.HighlightedText))
|
||||
elif role == Qt.BackgroundRole and row == self.highlight_row:
|
||||
return QVariant(QApplication.palette().brush(
|
||||
QPalette.Highlight))
|
||||
def _eject_requested(self, *args):
|
||||
self.unmount_device.emit()
|
||||
|
||||
return data
|
||||
|
||||
def device_connected(self, dev):
|
||||
self.icons[1] = QIcon(dev.icon)
|
||||
self.dataChanged.emit(self.index(1), self.index(1))
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
return NONE
|
||||
|
||||
def update_devices(self, cp=(None, None), fs=[-1, -1, -1]):
|
||||
def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None):
|
||||
if icon is None:
|
||||
icon = I('reader.svg')
|
||||
self.location_main.setIcon(QIcon(icon))
|
||||
had_device = self.has_device
|
||||
if cp is None:
|
||||
cp = (None, None)
|
||||
if isinstance(cp, (str, unicode)):
|
||||
@ -120,137 +118,34 @@ class LocationModel(QAbstractListModel): # {{{
|
||||
cpa, cpb = cp
|
||||
self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1
|
||||
self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1
|
||||
self.reset()
|
||||
self.devicesChanged.emit()
|
||||
self.update_tooltips()
|
||||
if self.has_device != had_device:
|
||||
self.locations_changed.emit()
|
||||
if not self.has_device:
|
||||
self.location_library.trigger()
|
||||
|
||||
def location_changed(self, row):
|
||||
self.highlight_row = row
|
||||
self.dataChanged.emit(
|
||||
self.index(0), self.index(self.rowCount(QModelIndex())-1))
|
||||
def update_tooltips(self):
|
||||
for i, loc in enumerate(('main', 'carda', 'cardb')):
|
||||
t = self.tooltips[loc]
|
||||
if self.free[i] > -1:
|
||||
t += u'\n\n%s '%human_readable(self.free[i]) + _('available')
|
||||
ac = getattr(self, 'location_'+loc)
|
||||
ac.setToolTip(t)
|
||||
ac.setWhatsThis(t)
|
||||
ac.setStatusTip(t)
|
||||
|
||||
def location_for_row(self, row):
|
||||
if row == 0: return 'library'
|
||||
if row == 1: return 'main'
|
||||
if row == 3: return 'cardb'
|
||||
return 'carda' if self.free[1] > -1 else 'cardb'
|
||||
|
||||
# }}}
|
||||
|
||||
class LocationView(QListView):
|
||||
|
||||
umount_device = pyqtSignal()
|
||||
location_selected = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent):
|
||||
QListView.__init__(self, parent)
|
||||
self.setModel(LocationModel(self))
|
||||
self.reset()
|
||||
self.currentChanged = self.current_changed
|
||||
|
||||
self.eject_button = EjectButton(self)
|
||||
self.eject_button.hide()
|
||||
|
||||
self.entered.connect(self.item_entered)
|
||||
self.viewportEntered.connect(self.viewport_entered)
|
||||
self.eject_button.clicked.connect(self.eject_clicked)
|
||||
self.model().devicesChanged.connect(self.eject_button.hide)
|
||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding))
|
||||
self.setMouseTracking(True)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
self.setEditTriggers(self.NoEditTriggers)
|
||||
self.setTabKeyNavigation(True)
|
||||
self.setProperty("showDropIndicator", True)
|
||||
self.setSelectionMode(self.SingleSelection)
|
||||
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
||||
self.setMovement(self.Static)
|
||||
self.setFlow(self.LeftToRight)
|
||||
self.setGridSize(QSize(175, ICON_SIZE))
|
||||
self.setViewMode(self.ListMode)
|
||||
self.setWordWrap(True)
|
||||
self.setObjectName("location_view")
|
||||
self.setMaximumSize(QSize(600, ICON_SIZE+16))
|
||||
self.setMinimumWidth(400)
|
||||
|
||||
def eject_clicked(self, *args):
|
||||
self.umount_device.emit()
|
||||
|
||||
def count_changed(self, new_count):
|
||||
self.model().count = new_count
|
||||
self.model().reset()
|
||||
|
||||
@property
|
||||
def book_count(self):
|
||||
return self.model().count
|
||||
|
||||
def current_changed(self, current, previous):
|
||||
if current.isValid():
|
||||
i = current.row()
|
||||
location = self.model().location_for_row(i)
|
||||
self.location_selected.emit(location)
|
||||
self.model().location_changed(i)
|
||||
|
||||
def location_changed(self, row):
|
||||
if 0 <= row and row <= 3:
|
||||
self.model().location_changed(row)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self.unsetCursor()
|
||||
self.eject_button.hide()
|
||||
|
||||
def item_entered(self, location):
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
self.eject_button.hide()
|
||||
|
||||
if location.row() == 1:
|
||||
rect = self.visualRect(location)
|
||||
|
||||
self.eject_button.resize(rect.height()/2, rect.height()/2)
|
||||
|
||||
x, y = rect.left(), rect.top()
|
||||
x = x + (rect.width() - self.eject_button.width() - 2)
|
||||
y += 6
|
||||
|
||||
self.eject_button.move(x, y)
|
||||
self.eject_button.show()
|
||||
|
||||
def viewport_entered(self):
|
||||
self.unsetCursor()
|
||||
self.eject_button.hide()
|
||||
|
||||
|
||||
class EjectButton(QAbstractButton):
|
||||
|
||||
def __init__(self, parent):
|
||||
QAbstractButton.__init__(self, parent)
|
||||
self.mouse_over = False
|
||||
self.setMouseTracking(True)
|
||||
|
||||
def enterEvent(self, event):
|
||||
self.mouse_over = True
|
||||
QAbstractButton.enterEvent(self, event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self.mouse_over = False
|
||||
QAbstractButton.leaveEvent(self, event)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setClipRect(event.rect())
|
||||
image = QPixmap(I('eject')).scaledToHeight(event.rect().height(),
|
||||
Qt.SmoothTransformation)
|
||||
|
||||
if not self.mouse_over:
|
||||
alpha_mask = QPixmap(image.width(), image.height())
|
||||
color = QColor(128, 128, 128)
|
||||
alpha_mask.fill(color)
|
||||
image.setAlphaChannel(alpha_mask)
|
||||
|
||||
painter.drawPixmap(0, 0, image)
|
||||
|
||||
|
||||
def has_device(self):
|
||||
return max(self.free) > -1
|
||||
|
||||
@property
|
||||
def available_actions(self):
|
||||
ans = [self.location_library]
|
||||
for i, loc in enumerate(('main', 'carda', 'cardb')):
|
||||
if self.free[i] > -1:
|
||||
ans.append(getattr(self, 'location_'+loc))
|
||||
return ans
|
||||
|
||||
# }}}
|
||||
|
||||
@ -326,7 +221,7 @@ class SearchBar(QWidget): # {{{
|
||||
|
||||
class ToolBar(QToolBar): # {{{
|
||||
|
||||
def __init__(self, actions, donate, location_view, parent=None):
|
||||
def __init__(self, actions, donate, location_manager, parent=None):
|
||||
QToolBar.__init__(self, parent)
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
self.setMovable(False)
|
||||
@ -335,11 +230,12 @@ class ToolBar(QToolBar): # {{{
|
||||
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
|
||||
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
||||
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
||||
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
|
||||
|
||||
self.showing_device = False
|
||||
self.all_actions = actions
|
||||
self.donate = donate
|
||||
self.location_view = location_view
|
||||
self.location_manager = location_manager
|
||||
self.location_manager.locations_changed.connect(self.build_bar)
|
||||
self.d_widget = QWidget()
|
||||
self.d_widget.setLayout(QVBoxLayout())
|
||||
self.d_widget.layout().addWidget(donate)
|
||||
@ -350,40 +246,45 @@ class ToolBar(QToolBar): # {{{
|
||||
def contextMenuEvent(self, *args):
|
||||
pass
|
||||
|
||||
def device_status_changed(self, connected):
|
||||
self.showing_device = connected
|
||||
self.build_bar()
|
||||
|
||||
def build_bar(self):
|
||||
order_field = 'device' if self.showing_device else 'normal'
|
||||
showing_device = self.location_manager.has_device
|
||||
order_field = 'device' if showing_device else 'normal'
|
||||
o = attrgetter(order_field+'_order')
|
||||
sepvals = [2] if self.showing_device else [1]
|
||||
sepvals = [2] if showing_device else [1]
|
||||
sepvals += [3]
|
||||
actions = [x for x in self.all_actions if o(x) > -1]
|
||||
actions.sort(cmp=lambda x,y : cmp(o(x), o(y)))
|
||||
self.clear()
|
||||
for x in actions:
|
||||
self.addAction(x)
|
||||
ch = self.widgetForAction(x)
|
||||
|
||||
|
||||
def setup_tool_button(ac):
|
||||
ch = self.widgetForAction(ac)
|
||||
ch.setCursor(Qt.PointingHandCursor)
|
||||
ch.setAutoRaise(True)
|
||||
|
||||
if x.action_name == 'choose_library':
|
||||
self.location_action = self.addWidget(self.location_view)
|
||||
self.choose_action = x
|
||||
if config['show_donate_button']:
|
||||
self.addWidget(self.d_widget)
|
||||
if x.action_name not in ('choose_library', 'help'):
|
||||
if ac.menu() is not None:
|
||||
ch.setPopupMode(ch.MenuButtonPopup)
|
||||
|
||||
for x in actions:
|
||||
self.addAction(x)
|
||||
setup_tool_button(x)
|
||||
|
||||
if x.action_name == 'choose_library':
|
||||
self.choose_action = x
|
||||
if showing_device:
|
||||
self.addSeparator()
|
||||
for ac in self.location_manager.available_actions:
|
||||
self.addAction(ac)
|
||||
setup_tool_button(ac)
|
||||
self.addSeparator()
|
||||
self.location_manager.location_library.trigger()
|
||||
elif config['show_donate_button']:
|
||||
self.addWidget(self.d_widget)
|
||||
|
||||
for x in actions:
|
||||
if x.separator_before in sepvals:
|
||||
self.insertSeparator(x)
|
||||
|
||||
|
||||
self.location_action.setVisible(self.showing_device)
|
||||
self.choose_action.setVisible(not self.showing_device)
|
||||
self.choose_action.setVisible(not showing_device)
|
||||
|
||||
def count_changed(self, new_count):
|
||||
text = _('%d books')%new_count
|
||||
@ -397,6 +298,9 @@ class ToolBar(QToolBar): # {{{
|
||||
self.setToolButtonStyle(style)
|
||||
QToolBar.resizeEvent(self, ev)
|
||||
|
||||
def database_changed(self, db):
|
||||
pass
|
||||
|
||||
# }}}
|
||||
|
||||
class Action(QAction):
|
||||
@ -404,7 +308,8 @@ class Action(QAction):
|
||||
|
||||
class MainWindowMixin(object):
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, db):
|
||||
self.device_connected = None
|
||||
self.setObjectName('MainWindow')
|
||||
self.setWindowIcon(QIcon(I('library.png')))
|
||||
self.setWindowTitle(__appname__)
|
||||
@ -417,9 +322,36 @@ class MainWindowMixin(object):
|
||||
self.resize(1012, 740)
|
||||
self.donate_button = ThrobbingButton(self.centralwidget)
|
||||
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
|
||||
self.location_manager = LocationManager(self)
|
||||
|
||||
# Actions {{{
|
||||
self.init_scheduler(db)
|
||||
all_actions = self.setup_actions()
|
||||
|
||||
self.search_bar = SearchBar(self)
|
||||
self.tool_bar = ToolBar(all_actions, self.donate_button,
|
||||
self.location_manager, self)
|
||||
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
||||
self.tool_bar.choose_action.triggered.connect(self.choose_library)
|
||||
|
||||
l = self.centralwidget.layout()
|
||||
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):
|
||||
pass
|
||||
|
||||
def choose_library(self, *args):
|
||||
from calibre.gui2.dialogs.choose_library import ChooseLibrary
|
||||
db = self.library_view.model().db
|
||||
c = ChooseLibrary(db, self.library_moved, self)
|
||||
c.exec_()
|
||||
|
||||
def setup_actions(self): # {{{
|
||||
all_actions = []
|
||||
|
||||
def ac(normal_order, device_order, separator_before,
|
||||
@ -467,17 +399,139 @@ class MainWindowMixin(object):
|
||||
ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
|
||||
'tags.svg')
|
||||
|
||||
# }}}
|
||||
self.action_news.setMenu(self.scheduler.news_menu)
|
||||
self.action_news.triggered.connect(
|
||||
self.scheduler.show_dialog)
|
||||
|
||||
self.location_view = LocationView(self.centralwidget)
|
||||
self.search_bar = SearchBar(self)
|
||||
self.tool_bar = ToolBar(all_actions, self.donate_button, self.location_view, self)
|
||||
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
||||
self.action_help.triggered.connect(self.show_help)
|
||||
md = QMenu()
|
||||
md.addAction(_('Edit metadata individually'),
|
||||
partial(self.edit_metadata, False, bulk=False))
|
||||
md.addSeparator()
|
||||
md.addAction(_('Edit metadata in bulk'),
|
||||
partial(self.edit_metadata, False, bulk=True))
|
||||
md.addSeparator()
|
||||
md.addAction(_('Download metadata and covers'),
|
||||
partial(self.download_metadata, False, covers=True),
|
||||
Qt.ControlModifier+Qt.Key_D)
|
||||
md.addAction(_('Download only metadata'),
|
||||
partial(self.download_metadata, False, covers=False))
|
||||
md.addAction(_('Download only covers'),
|
||||
partial(self.download_metadata, False, covers=True,
|
||||
set_metadata=False, set_social_metadata=False))
|
||||
md.addAction(_('Download only social metadata'),
|
||||
partial(self.download_metadata, False, covers=False,
|
||||
set_metadata=False, set_social_metadata=True))
|
||||
self.metadata_menu = md
|
||||
|
||||
l = self.centralwidget.layout()
|
||||
l.addWidget(self.search_bar)
|
||||
mb = QMenu()
|
||||
mb.addAction(_('Merge into first selected book - delete others'),
|
||||
self.merge_books)
|
||||
mb.addSeparator()
|
||||
mb.addAction(_('Merge into first selected book - keep others'),
|
||||
partial(self.merge_books, safe_merge=True))
|
||||
self.merge_menu = mb
|
||||
self.action_merge.setMenu(mb)
|
||||
md.addSeparator()
|
||||
md.addAction(self.action_merge)
|
||||
|
||||
self.add_menu = QMenu()
|
||||
self.add_menu.addAction(_('Add books from a single directory'),
|
||||
self.add_books)
|
||||
self.add_menu.addAction(_('Add books from directories, including '
|
||||
'sub-directories (One book per directory, assumes every ebook '
|
||||
'file is the same book in a different format)'),
|
||||
self.add_recursive_single)
|
||||
self.add_menu.addAction(_('Add books from directories, including '
|
||||
'sub directories (Multiple books per directory, assumes every '
|
||||
'ebook file is a different book)'), self.add_recursive_multiple)
|
||||
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
|
||||
'formats)'), self.add_empty)
|
||||
self.action_add.setMenu(self.add_menu)
|
||||
self.action_add.triggered.connect(self.add_books)
|
||||
self.action_del.triggered.connect(self.delete_books)
|
||||
self.action_edit.triggered.connect(self.edit_metadata)
|
||||
self.action_merge.triggered.connect(self.merge_books)
|
||||
|
||||
self.action_save.triggered.connect(self.save_to_disk)
|
||||
self.save_menu = QMenu()
|
||||
self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk,
|
||||
False))
|
||||
self.save_menu.addAction(_('Save to disk in a single directory'),
|
||||
partial(self.save_to_single_dir, False))
|
||||
self.save_menu.addAction(_('Save only %s format to disk')%
|
||||
prefs['output_format'].upper(),
|
||||
partial(self.save_single_format_to_disk, False))
|
||||
self.save_menu.addAction(
|
||||
_('Save only %s format to disk in a single directory')%
|
||||
prefs['output_format'].upper(),
|
||||
partial(self.save_single_fmt_to_single_dir, False))
|
||||
self.save_sub_menu = SaveMenu(self)
|
||||
self.save_menu.addMenu(self.save_sub_menu)
|
||||
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
|
||||
|
||||
self.action_view.triggered.connect(self.view_book)
|
||||
self.view_menu = QMenu()
|
||||
self.view_menu.addAction(_('View'), partial(self.view_book, False))
|
||||
ac = self.view_menu.addAction(_('View specific format'))
|
||||
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
|
||||
self.action_view.setMenu(self.view_menu)
|
||||
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
|
||||
|
||||
self.delete_menu = QMenu()
|
||||
self.delete_menu.addAction(_('Remove selected books'), self.delete_books)
|
||||
self.delete_menu.addAction(
|
||||
_('Remove files of a specific format from selected books..'),
|
||||
self.delete_selected_formats)
|
||||
self.delete_menu.addAction(
|
||||
_('Remove all formats from selected books, except...'),
|
||||
self.delete_all_but_selected_formats)
|
||||
self.delete_menu.addAction(
|
||||
_('Remove covers from selected books'), self.delete_covers)
|
||||
self.delete_menu.addSeparator()
|
||||
self.delete_menu.addAction(
|
||||
_('Remove matching books from device'),
|
||||
self.remove_matching_books_from_device)
|
||||
self.action_del.setMenu(self.delete_menu)
|
||||
|
||||
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
||||
self.addAction(self.action_open_containing_folder)
|
||||
self.action_open_containing_folder.triggered.connect(self.view_folder)
|
||||
self.action_sync.setShortcut(Qt.Key_D)
|
||||
self.action_sync.setEnabled(True)
|
||||
self.create_device_menu()
|
||||
self.action_sync.triggered.connect(
|
||||
self._sync_action_triggered)
|
||||
|
||||
self.action_edit.setMenu(md)
|
||||
self.action_save.setMenu(self.save_menu)
|
||||
|
||||
cm = QMenu()
|
||||
cm.addAction(_('Convert individually'), partial(self.convert_ebook,
|
||||
False, bulk=False))
|
||||
cm.addAction(_('Bulk convert'),
|
||||
partial(self.convert_ebook, False, bulk=True))
|
||||
cm.addSeparator()
|
||||
ac = cm.addAction(
|
||||
_('Create catalog of books in your calibre library'))
|
||||
ac.triggered.connect(self.generate_catalog)
|
||||
self.action_convert.setMenu(cm)
|
||||
self.action_convert.triggered.connect(self.convert_ebook)
|
||||
self.convert_menu = cm
|
||||
|
||||
pm = QMenu()
|
||||
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
|
||||
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
|
||||
self.run_wizard)
|
||||
self.action_preferences.setMenu(pm)
|
||||
self.preferences_menu = pm
|
||||
for x in (self.preferences_action, self.action_preferences):
|
||||
x.triggered.connect(self.do_config)
|
||||
|
||||
return all_actions
|
||||
# }}}
|
||||
|
||||
def show_help(self, *args):
|
||||
open_url(QUrl('http://calibre-ebook.com/user_manual'))
|
||||
|
||||
|
||||
def read_toolbar_settings(self):
|
||||
pass
|
||||
|
||||
|
@ -12,13 +12,13 @@ __docformat__ = 'restructuredtext en'
|
||||
import collections, os, sys, textwrap, time
|
||||
from Queue import Queue, Empty
|
||||
from threading import Thread
|
||||
from PyQt4.Qt import Qt, SIGNAL, QObject, QTimer, \
|
||||
from PyQt4.Qt import Qt, SIGNAL, QTimer, \
|
||||
QPixmap, QMenu, QIcon, pyqtSignal, \
|
||||
QDialog, \
|
||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||
QMessageBox, QHelpEvent
|
||||
|
||||
from calibre import prints, patheq
|
||||
from calibre import prints
|
||||
from calibre.constants import __appname__, isosx
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.config import prefs, dynamic
|
||||
@ -27,8 +27,6 @@ from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
|
||||
gprefs, max_available_height, config, info_dialog
|
||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||
from calibre.gui2.widgets import ProgressIndicator
|
||||
from calibre.gui2.wizard import move_library
|
||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||
from calibre.gui2.update import UpdateMixin
|
||||
from calibre.gui2.main_window import MainWindow
|
||||
from calibre.gui2.layout import MainWindowMixin
|
||||
@ -38,7 +36,7 @@ from calibre.gui2.dialogs.config import ConfigDialog
|
||||
|
||||
from calibre.gui2.dialogs.book_info import BookInfo
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
|
||||
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||
from calibre.gui2.tag_view import TagBrowserMixin
|
||||
@ -91,7 +89,7 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
|
||||
AnnotationsAction, AddAction, DeleteAction,
|
||||
@ -120,7 +118,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
self.another_instance_wants_to_talk)
|
||||
self.check_messages_timer.start(1000)
|
||||
|
||||
MainWindowMixin.__init__(self)
|
||||
MainWindowMixin.__init__(self, db)
|
||||
|
||||
# Jobs Button {{{
|
||||
self.job_manager = JobManager()
|
||||
@ -192,21 +190,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
####################### Start spare job server ########################
|
||||
QTimer.singleShot(1000, self.add_spare_server)
|
||||
|
||||
####################### Location View ########################
|
||||
QObject.connect(self.location_view,
|
||||
SIGNAL('location_selected(PyQt_PyObject)'),
|
||||
self.location_selected)
|
||||
QObject.connect(self.location_view,
|
||||
SIGNAL('umount_device()'),
|
||||
self.device_manager.umount_device)
|
||||
####################### Location Manager ########################
|
||||
self.location_manager.location_selected.connect(self.location_selected)
|
||||
self.location_manager.unmount_device.connect(self.device_manager.umount_device)
|
||||
self.eject_action.triggered.connect(self.device_manager.umount_device)
|
||||
|
||||
#################### Update notification ###################
|
||||
UpdateMixin.__init__(self, opts)
|
||||
|
||||
####################### Setup Toolbar #####################
|
||||
ToolbarMixin.__init__(self)
|
||||
|
||||
####################### Search boxes ########################
|
||||
SavedSearchBoxMixin.__init__(self)
|
||||
SearchBoxMixin.__init__(self)
|
||||
@ -218,7 +209,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
|
||||
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||
self.hide_windows()
|
||||
for t in (self.location_view, self.tool_bar):
|
||||
for t in (self.tool_bar, ):
|
||||
self.library_view.model().count_changed_signal.connect \
|
||||
(t.count_changed)
|
||||
if not gprefs.get('quick_start_guide_added', False):
|
||||
@ -235,8 +226,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
self.db_images.reset()
|
||||
|
||||
self.library_view.model().count_changed()
|
||||
self.location_view.model().database_changed(self.library_view.model().db)
|
||||
self.library_view.model().database_changed.connect(self.location_view.model().database_changed,
|
||||
self.tool_bar.database_changed(self.library_view.model().db)
|
||||
self.library_view.model().database_changed.connect(self.tool_bar.database_changed,
|
||||
type=Qt.QueuedConnection)
|
||||
|
||||
########################### Tags Browser ##############################
|
||||
@ -261,18 +252,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
db, server_config().parse())
|
||||
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)
|
||||
AddAction.__init__(self)
|
||||
|
||||
@ -280,6 +259,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
self.finalize_layout()
|
||||
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):
|
||||
MainWindow.resizeEvent(self, ev)
|
||||
self.search.setMaximumWidth(self.width()-150)
|
||||
@ -396,10 +380,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
self.tags_view.recount()
|
||||
self.create_device_menu()
|
||||
self.set_device_menu_items_state(bool(self.device_connected))
|
||||
if not patheq(self.library_path, d.database_location):
|
||||
newloc = d.database_location
|
||||
move_library(self.library_path, newloc, self,
|
||||
self.library_moved)
|
||||
|
||||
def library_moved(self, newloc):
|
||||
if newloc is None: return
|
||||
@ -414,6 +394,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
|
||||
self.search.clear_to_help()
|
||||
self.book_details.reset_info()
|
||||
self.library_view.model().count_changed()
|
||||
self.scheduler.database_changed(db)
|
||||
prefs['library_path'] = self.library_path
|
||||
|
||||
def show_book_info(self, *args):
|
||||
|
@ -116,6 +116,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
# missing functions
|
||||
self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter')
|
||||
|
||||
@classmethod
|
||||
def exists_at(cls, path):
|
||||
return path and os.path.exists(os.path.join(path, 'metadata.db'))
|
||||
|
||||
def __init__(self, library_path, row_factory=False):
|
||||
self.field_metadata = FieldMetadata()
|
||||
if not os.path.exists(library_path):
|
||||
|
@ -104,30 +104,46 @@ will appear in the next release of |app|.
|
||||
How does |app| manage collections on my SONY reader?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When |app| connects with the device, it retrieves all collections for the books on the device. The collections
|
||||
When |app| connects with the reader, it retrieves all collections for the books on the reader. The collections
|
||||
of which books are members are shown on the device view.
|
||||
|
||||
When you send a book to the device, |app| will add the book to collections based on the metadata for that book. By
|
||||
When you send a book to the reader, |app| will add the book to collections based on the metadata for that book. By
|
||||
default, collections are created from tags and series. You can control what metadata is used by going to
|
||||
Preferences->Plugins->Device Interface plugins and customizing the SONY device interface plugin. If you remove all
|
||||
values, |app| will not add the book to any collection.
|
||||
|
||||
Collection management is largely controlled by 'Preserve device collections' found at Preferences->Add/Save->Sending
|
||||
to device. If checked (the default), managing collections is left to the user; |app| will not delete already
|
||||
existing collections for a book on your device when you resend the book to the device, but |app| will add the book to
|
||||
collections if necessary. To ensure that the collections for a book are based only on current |app| metadata, first
|
||||
delete the books from the device, then resend the books. You can edit collections directly on the device view by
|
||||
double-clicking or right-clicking in the collections column.
|
||||
Collection management is largely controlled by the 'Metadata management' option found at
|
||||
Preferences->Add/Save->Sending to device. If set to 'Manual' (the default), managing collections is left to
|
||||
the user; |app| will not delete already existing collections for a book on your reader when you resend the
|
||||
book to the reader, but |app| will add the book to collections if necessary. To ensure that the collections
|
||||
for a book are based only on current |app| metadata, first delete the books from the reader, then resend the
|
||||
books. You can edit collections directly on the device view by double-clicking or right-clicking in the
|
||||
collections column.
|
||||
|
||||
If 'Preserve device collections' is not checked, then |app| will manage collections. Collections will be built using
|
||||
|app| metadata exclusively. Sending a book to the device will correct the collections for that book so its
|
||||
collections exactly match the book's metadata. Collections are added and deleted as necessary. Editing collections on
|
||||
the device pane is not permitted, because collections not in the metadata will be removed automatically.
|
||||
If 'Metadata management' is set to 'Only on send', then |app| will manage collections more aggressively.
|
||||
Collections will be built using |app| metadata exclusively. Sending a book to the reader will correct the
|
||||
collections for that book so its collections exactly match the book's metadata, adding and deleting
|
||||
collections as necessary. Editing collections on the device view is not permitted, because collections not in
|
||||
the metadata will be removed automatically.
|
||||
|
||||
In summary, check 'Preserve device collections' if you want to manage collections yourself. Collections for a book
|
||||
will never be removed by |app|, but can be removed by you by editing on the device view. Uncheck 'Preserve device
|
||||
collections' if you want |app| to manage the collections, adding books to and removing books from collections as
|
||||
needed.
|
||||
If 'Metadata management' is set to 'Automatic management', then |app| will update metadata and collections
|
||||
both when the reader is connected and when books are sent. When calibre detects the reader and generates the
|
||||
list of books on the reader, it will send metadata from the library to the reader for all books on the reader
|
||||
that are in the library (On device is True), adding and removing books from collections as indicated by the
|
||||
metadata and device customization. When a book is sent, |app| corrects the metadata for that book, adding and
|
||||
deleting collections. Manual editing of metadata on the device view is not allowed. Note that this option
|
||||
specifies sending metadata, not books. The book files on the reader are not changed.
|
||||
|
||||
In summary, choose 'manual management' if you want to manage collections yourself. Collections for a book
|
||||
will never be removed by |app|, but can be removed by you by editing on the device view. Choose 'Only on
|
||||
send' if you want |app| to manage collections when you send a book, adding books to and removing books from
|
||||
collections as needed. Choose 'Automatic management' if you want |app| to keep collections up to date
|
||||
whenever the reader is connected.
|
||||
|
||||
If you use multiple installations of calibre to manage your reader, then option 'Automatic management' may not
|
||||
be what you want. Connecting the reader to one library will reset the metadata to what is in that library.
|
||||
Connecting to the other library will reset the metadata to what is in that other library. Metadata in books
|
||||
found in both libraries will be flopped back and forth.
|
||||
|
||||
Can I use both |app| and the SONY software to manage my reader?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
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
@ -4,9 +4,9 @@
|
||||
bibliograph packages.
|
||||
From http://pypi.python.org/pypi/bibliograph.core/
|
||||
from Tom Gross <itconsense@gmail.com>
|
||||
|
||||
|
||||
Adapted for calibre use
|
||||
|
||||
|
||||
Zope Public License (ZPL) Version 2.1
|
||||
|
||||
A copyright notice accompanies this license document that
|
||||
@ -65,7 +65,7 @@
|
||||
__docformat__ = 'reStructuredText'
|
||||
__author__ = 'sengian <sengian1 at gmail.com>'
|
||||
|
||||
import os, re, string
|
||||
import re, string
|
||||
|
||||
utf8enc2latex_mapping = {
|
||||
# This is a mapping of Unicode characters to LaTeX equivalents.
|
||||
@ -75,7 +75,7 @@ utf8enc2latex_mapping = {
|
||||
#
|
||||
# The extraction has been done by the "create_unimap.py" script
|
||||
# located at <http://docutils.sf.net/tools/dev/create_unimap.py>.
|
||||
|
||||
|
||||
#Fix some encoding problem between cp1252 and latin1
|
||||
# from http://www.microsoft.com/typography/unicode/1252.htm
|
||||
u'\x80': '{\\mbox{\\texteuro}}', # EURO SIGN
|
||||
@ -2521,7 +2521,7 @@ def escapeSpecialCharacters(text):
|
||||
for c in escape:
|
||||
text = text.replace(c, '\\' + c )
|
||||
return text
|
||||
|
||||
|
||||
#Calibre functions
|
||||
#Go from an unicode entry to ASCII Bibtex format without encoding
|
||||
#Option to go to official ASCII Bibtex or unofficial UTF-8
|
||||
@ -2533,7 +2533,7 @@ def utf8ToBibtex(text, asccii_bibtex = True):
|
||||
if asccii_bibtex :
|
||||
text = resolveUnicode(text)
|
||||
return escapeSpecialCharacters(text)
|
||||
|
||||
|
||||
def bibtex_author_format(item):
|
||||
#Format authors for Bibtex compliance (get a list as input)
|
||||
return utf8ToBibtex(u' and'.join([author for author in item]))
|
||||
|
@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import os, copy
|
||||
|
||||
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.gui2 import NONE
|
||||
@ -131,6 +131,16 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
|
||||
self.scheduler_config = SchedulerConfig()
|
||||
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):
|
||||
if download:
|
||||
try:
|
||||
|
Loading…
x
Reference in New Issue
Block a user