Sync to trunk.

This commit is contained in:
John Schember 2010-07-24 15:38:16 -04:00
commit 38fede7bc4
114 changed files with 58114 additions and 43909 deletions

View File

@ -4,6 +4,130 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.10
date: 2010-07-23
new features:
- title: "Allow user customization of static resources such as icons and templates"
type: major
description: >
"You can now change the icons used in the User Interface and other static resources. Details on how to
do this are at: http://calibre-ebook.com/user_manual/customize.html#overriding-icons-templates-etcetera"
- title: "Split the 'Send to device' button into two buttons, 'Connect/share' and 'Send to device'. The new 'Send to device' button will now only be available when a device is connected."
- title: "Store column layout, saved searches and user categories seprately per calibre library. This makes it possible to easily switch between libraries with different custom column setups"
- title: "See the last modofied date for each format in the edit metadata dialog via a tooltip"
tickets: [6252]
- title: "PD Novel driver: Add support for uploading cover thumbnails to device"
- title: "More sophisticated metadata extraction from HTML files"
tickets: [6223]
bug fixes:
- title: "Fix problems with a few windows installs caused by the upgrade to Qt 4.6.3 in the previous release. These would manifest as a not working Add Books button, or deletes not actually deleting files, etc."
- title: "Restore configurability of toolbar, which was temporarily removed in 0.7.9. You can once again set icon size via Preferences->Interface"
- title: "Fix regression in iTunes driver in 0.7.9 when sending series info"
- title: "Search: Fix parsing of search terms that contain a word that starts with 'and' or 'or' and is not the first word"
- title: "When merging records also merge metadata in custom columns"
tickets: [6120]
- title: "When scrolling to show a particular row, handle the case when the first column is a custom column"
tickets: [6176]
- title: "Fix SD card detection for The Augen Book"
tickets: [6224]
- title: "CHM Input: Fix a couple of bugs that could cause crashes"
tickets: [6240]
- title: "Conversion pipeline: Handle zero width elements with non zero indents gracefully"
tickets: [6230]
new recipes:
- title: "daum.net"
author: trustin
- title: "MIT Technology Review, Alternet, Waco Tribune Herald and Orlando Sentinel"
author: rty
improved recipes:
- The BBC
- heise
- 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,24 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg1307"
sodipodi:version="0.32"
inkscape:version="0.43"
inkscape:version="0.47 r22583"
version="1.0"
sodipodi:docbase="/home/pinheiro/Documents/pics/new oxygen/svg"
sodipodi:docname="love.svg">
sodipodi:docname="donate.svg">
<defs
id="defs1309">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 64 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="128 : 64 : 1"
inkscape:persp3d-origin="64 : 42.666667 : 1"
id="perspective44" />
<linearGradient
inkscape:collect="always"
id="linearGradient2231">
@ -180,8 +187,8 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="7.851329"
inkscape:cx="92.691163"
inkscape:cy="92.473338"
inkscape:cx="60.937831"
inkscape:cy="61.488995"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:document-units="px"
@ -189,10 +196,11 @@
guidetolerance="0.1px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1106"
inkscape:window-height="958"
inkscape:window-x="597"
inkscape:window-y="25">
inkscape:window-width="1680"
inkscape:window-height="997"
inkscape:window-x="-4"
inkscape:window-y="30"
inkscape:window-maximized="1">
<sodipodi:guide
orientation="horizontal"
position="32.487481"
@ -245,26 +253,19 @@
id="path2276"
d="M 50.892799,3.2812959 L 50.892799,0.48658747 L 50.892799,3.2812959 z "
style="fill:#ffffff;fill-opacity:0.75688076;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<path
sodipodi:type="arc"
style="opacity:0.38139535;fill:url(#radialGradient3297);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="path3289"
sodipodi:cx="63.912209"
sodipodi:cy="115.70919"
sodipodi:rx="63.912209"
sodipodi:ry="12.641975"
d="M 127.82442 115.70919 A 63.912209 12.641975 0 1 1 0,115.70919 A 63.912209 12.641975 0 1 1 127.82442 115.70919 z"
transform="matrix(1,0,0,0.416667,0,74.87151)" />
<path
style="fill:url(#radialGradient2335);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 35.325021,6.2016208 C 32.278871,6.2210338 29.045555,6.6687791 25.645673,7.6089386 C 5.9380713,13.058619 0.404709,29.342113 5.3805953,48.506873 C 12.126047,74.487157 36.855395,101.02725 64.150803,115.92895 L 64.150803,116.02417 C 64.162016,116.00826 64.173539,115.99248 64.184766,115.97656 C 64.195995,115.99248 64.207516,116.00826 64.218732,116.02417 L 64.218732,115.92895 C 90.473794,101.59521 116.24349,74.487157 122.98895,48.506873 C 127.96481,29.342113 122.43148,13.058619 102.72386,7.6089386 C 83.422254,2.2715258 69.549778,12.840101 64.184766,27.183808 C 59.764775,15.366673 49.572303,6.1108179 35.325021,6.2016208 z "
id="path2245"
sodipodi:nodetypes="cssccsccsscc" />
<g
id="g2850">
<path
id="path2369"
d="M 35.325021,6.2016208 C 32.278871,6.2210338 29.045555,6.6687791 25.645673,7.6089386 C 5.9380713,13.058619 0.404709,29.342113 5.3805953,48.506873 C 12.126047,74.487157 37.113186,101.16799 64.150803,115.92895 L 64.150803,116.02417 C 64.162016,116.00826 64.173539,115.99248 64.184766,115.97656 C 64.195995,115.99248 64.207516,116.00826 64.218732,116.02417 L 64.218732,115.92895 C 90.398445,101.63635 116.24349,74.487157 122.98895,48.506873 C 127.96481,29.342113 122.43148,13.058619 102.72386,7.6089386 C 83.422254,2.2715258 69.549778,12.840101 64.184766,27.183808 C 59.764775,15.366673 49.572303,6.1108179 35.325021,6.2016208 z "
sodipodi:nodetypes="cssccsccsscc"
style="opacity:0.4713115;fill:url(#linearGradient2379);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:nodetypes="cssccsccsscc" />
d="M 35.325021,6.2016208 C 32.278871,6.2210338 29.045555,6.6687791 25.645673,7.6089386 C 5.9380713,13.058619 0.404709,29.342113 5.3805953,48.506873 C 12.126047,74.487157 37.113186,101.16799 64.150803,115.92895 L 64.150803,116.02417 C 64.162016,116.00826 64.173539,115.99248 64.184766,115.97656 C 64.195995,115.99248 64.207516,116.00826 64.218732,116.02417 L 64.218732,115.92895 C 90.398445,101.63635 116.24349,74.487157 122.98895,48.506873 C 127.96481,29.342113 122.43148,13.058619 102.72386,7.6089386 C 83.422254,2.2715258 69.549778,12.840101 64.184766,27.183808 C 59.764775,15.366673 49.572303,6.1108179 35.325021,6.2016208 z "
id="path2369" />
</g>
<path
style="opacity:0.1762295;fill:url(#linearGradient2331);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.29999995;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 34.451605,6.2067207 C 31.659392,6.2976073 28.7301,6.7682297 25.648957,7.6202497 C 7.7889432,12.559022 1.5815371,26.389172 4.2759909,43.204304 C 27.13595,75.72273 65.297627,95.42612 91.41193,91.971053 C 105.43169,77.948778 119.04939,63.70497 122.99185,48.520401 C 127.96773,29.355639 122.42255,13.069929 102.71494,7.6202497 C 83.413331,2.2828362 69.546961,12.850845 64.181949,27.194552 C 59.761957,15.377418 49.555176,6.1159177 35.307894,6.2067207 C 35.022317,6.2085406 34.740456,6.1973187 34.451605,6.2067207 z "

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
resources/images/lt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View 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

View File

@ -3,14 +3,13 @@ __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
news.bbc.co.uk
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class BBC(BasicNewsRecipe):
title = 'The BBC'
__author__ = 'Darko Miletic'
description = 'Global news and current affairs from the British Broadcasting Corporation'
title = 'BBC News'
__author__ = 'Darko Miletic, Starson17'
description = 'News from UK. '
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
@ -23,7 +22,6 @@ class BBC(BasicNewsRecipe):
publication_type = 'newsportal'
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
conversion_options = {
'comments' : description
,'tags' : category
@ -33,14 +31,15 @@ class BBC(BasicNewsRecipe):
}
keep_only_tags = [
dict(attrs={'id' :['meta-information','story-body']})
,dict(attrs={'class':['mxb' ,'storybody' ]})
dict(name='div', attrs={'class':['layout-block-a layout-block']})
,dict(attrs={'class':['story-body','storybody']})
]
remove_tags = [
dict(name=['object','link','table'])
,dict(attrs={'class':['caption','caption full-width','story-actions','hidden','sharesb','audioInStoryC']})
dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
'story-feature wide ', 'story-feature narrow']})
]
remove_tags_after = dict(attrs={'class':'sharesb'})
remove_attributes = ['width','height']
feeds = [

View File

@ -8,7 +8,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class BBC(BasicNewsRecipe):
title = 'BBC News (fast)'
__author__ = 'Darko Miletic'
__author__ = 'Darko Miletic, Starson17'
description = 'News from UK. A much faster version that does not download pictures'
oldest_article = 2
max_articles_per_feed = 100
@ -31,14 +31,16 @@ class BBC(BasicNewsRecipe):
}
keep_only_tags = [
dict(attrs={'id' :['meta-information','story-body']})
,dict(attrs={'class':['mxb' ,'storybody' ]})
dict(name='div', attrs={'class':['layout-block-a layout-block']})
,dict(attrs={'class':['story-body','storybody']})
]
remove_tags = [
dict(name=['object','link','table','img'])
,dict(attrs={'class':['caption','caption full-width','story-actions','hidden','sharesb','audioInStoryC']})
dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
'story-feature wide ', 'story-feature narrow']})
, dict(name=['img'])
]
remove_tags_after = dict(attrs={'class':'sharesb'})
remove_attributes = ['width','height']
feeds = [

View File

@ -0,0 +1,112 @@
import re
from datetime import date, timedelta
from calibre.web.feeds.recipes import BasicNewsRecipe
class MediaDaumRecipe(BasicNewsRecipe):
title = u'\uBBF8\uB514\uC5B4 \uB2E4\uC74C \uC624\uB298\uC758 \uC8FC\uC694 \uB274\uC2A4'
description = 'Articles from media.daum.net'
__author__ = 'trustin'
language = 'ko'
max_articles = 100
timefmt = ''
masthead_url = 'http://img-media.daum-img.net/2010ci/service_news.gif'
cover_margins = (18,18,'grey99')
no_stylesheets = True
remove_tags_before = dict(id='GS_con')
remove_tags_after = dict(id='GS_con')
remove_tags = [dict(attrs={'class':[
'bline',
'GS_vod',
]}),
dict(id=[
'GS_swf_poll',
'ad250',
]),
dict(name=['script', 'noscript', 'style', 'object'])]
preprocess_regexps = [
(re.compile(r'<\s+', re.DOTALL|re.IGNORECASE),
lambda match: '&lt; '),
(re.compile(r'(<br[^>]*>[ \t\r\n]*){3,}', re.DOTALL|re.IGNORECASE),
lambda match: ''),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</p>', re.DOTALL|re.IGNORECASE),
lambda match: '</p>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</td>', re.DOTALL|re.IGNORECASE),
lambda match: '</td>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</strong>', re.DOTALL|re.IGNORECASE),
lambda match: '</strong>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</b>', re.DOTALL|re.IGNORECASE),
lambda match: '</b>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</em>', re.DOTALL|re.IGNORECASE),
lambda match: '</em>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</i>', re.DOTALL|re.IGNORECASE),
lambda match: '</i>'),
(re.compile(u'\(\uB05D\)[ \t\r\n]*<br[^>]*>.*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<div', re.DOTALL|re.IGNORECASE),
lambda match: '<div'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<p', re.DOTALL|re.IGNORECASE),
lambda match: '<p'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<table', re.DOTALL|re.IGNORECASE),
lambda match: '<table'),
(re.compile(r'<strong>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<strong>'),
(re.compile(r'<b>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<b>'),
(re.compile(r'<em>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<em>'),
(re.compile(r'<i>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<i>'),
(re.compile(u'(<br[^>]*>[ \t\r\n]*)*(\u25B6|\u25CF|\u261E|\u24D2|\(c\))*\[[^\]]*(\u24D2|\(c\)|\uAE30\uC0AC|\uC778\uAE30[^\]]*\uB274\uC2A4)[^\]]*\].*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
]
def parse_index(self):
today = date.today();
articles = []
articles = self.parse_list_page(articles, today)
articles = self.parse_list_page(articles, today - timedelta(1))
return [('\uBBF8\uB514\uC5B4 \uB2E4\uC74C \uC624\uB298\uC758 \uC8FC\uC694 \uB274\uC2A4', articles)]
def parse_list_page(self, articles, date):
if len(articles) >= self.max_articles:
return articles
for page in range(1, 10):
soup = self.index_to_soup('http://media.daum.net/primary/total/list.html?cateid=100044&date=%(date)s&page=%(page)d' % {'date': date.strftime('%Y%m%d'), 'page': page})
done = True
for item in soup.findAll('dl'):
dt = item.find('dt', { 'class': 'tit' })
dd = item.find('dd', { 'class': 'txt' })
if dt is None:
break
a = dt.find('a', href=True)
url = 'http://media.daum.net/primary/total/' + a['href']
title = self.tag_to_string(dt)
if dd is None:
description = ''
else:
description = self.tag_to_string(dd)
articles.append(dict(title=title, description=description, url=url, content=''))
done = len(articles) >= self.max_articles
if done:
break
if done:
break
return articles
def preprocess_html(self, soup):
return self.strip_anchors(soup)
def strip_anchors(self, soup):
for para in soup.findAll(True):
aTags = para.findAll('a')
for a in aTags:
if a.img is None:
a.replaceWith(a.renderContents().decode('utf-8','replace'))
return soup

View File

@ -4,29 +4,27 @@ from calibre import __appname__
class GoogleReader(BasicNewsRecipe):
title = 'Google Reader'
description = 'This recipe downloads feeds you have tagged from your Google Reader account.'
description = 'This recipe fetches from your Google Reader account unread Starred items and unread Feeds you have placed in a folder via the manage subscriptions feature.'
needs_subscription = True
__author__ = 'davec'
__author__ = 'davec, rollercoaster, Starson17'
base_url = 'http://www.google.com/reader/atom/'
max_articles_per_feed = 50
oldest_article = 365
max_articles_per_feed = 250
get_options = '?n=%d&xt=user/-/state/com.google/read' % max_articles_per_feed
use_embedded_content = True
def get_browser(self):
br = BasicNewsRecipe.get_browser()
br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
request = urllib.urlencode([('Email', self.username), ('Passwd', self.password),
('service', 'reader'), ('source', __appname__)])
('service', 'reader'), ('accountType', 'HOSTED_OR_GOOGLE'), ('source', __appname__)])
response = br.open('https://www.google.com/accounts/ClientLogin', request)
sid = re.search('SID=(\S*)', response.read()).group(1)
auth = re.search('Auth=(\S*)', response.read()).group(1)
cookies = mechanize.CookieJar()
br = mechanize.build_opener(mechanize.HTTPCookieProcessor(cookies))
cookies.set_cookie(mechanize.Cookie(None, 'SID', sid, None, False, '.google.com', True, True, '/', True, False, None, True, '', '', None))
br.addheaders = [('Authorization', 'GoogleLogin auth='+auth)]
return br
def get_feeds(self):
feeds = []
soup = self.index_to_soup('http://www.google.com/reader/api/0/tag/list')

View File

@ -3,10 +3,10 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
from calibre import __appname__
class GoogleReaderUber(BasicNewsRecipe):
title = 'Google Reader Uber'
description = 'This recipe downloads all unread feedsfrom your Google Reader account.'
title = 'Google Reader uber'
description = 'Fetches all feeds from your Google Reader account including the uncategorized items.'
needs_subscription = True
__author__ = 'rollercoaster, davec'
__author__ = 'davec, rollercoaster, Starson17'
base_url = 'http://www.google.com/reader/atom/'
oldest_article = 365
max_articles_per_feed = 250
@ -14,20 +14,17 @@ class GoogleReaderUber(BasicNewsRecipe):
use_embedded_content = True
def get_browser(self):
br = BasicNewsRecipe.get_browser()
br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
request = urllib.urlencode([('Email', self.username), ('Passwd', self.password),
('service', 'reader'), ('source', __appname__)])
('service', 'reader'), ('accountType', 'HOSTED_OR_GOOGLE'), ('source', __appname__)])
response = br.open('https://www.google.com/accounts/ClientLogin', request)
sid = re.search('SID=(\S*)', response.read()).group(1)
auth = re.search('Auth=(\S*)', response.read()).group(1)
cookies = mechanize.CookieJar()
br = mechanize.build_opener(mechanize.HTTPCookieProcessor(cookies))
cookies.set_cookie(mechanize.Cookie(None, 'SID', sid, None, False, '.google.com', True, True, '/', True, False, None, True, '', '', None))
br.addheaders = [('Authorization', 'GoogleLogin auth='+auth)]
return br
def get_feeds(self):
feeds = []
soup = self.index_to_soup('http://www.google.com/reader/api/0/tag/list')

View File

@ -19,6 +19,32 @@ class heiseDe(BasicNewsRecipe):
max_articles_per_feed = 40
no_stylesheets = True
extra_css = '''
.bild_links, .bild_bu_links {
float:left;
line-height:105%;
margin:12px 1.4em 12px 0;
}
.bild_rechts, .bild_bu {
float:right;
line-height:105%;
margin:12px 0 12px 1em;
text-align:right;
}
.bild_zentriert {
clear:both;
line-height:105%;
margin:.2em auto;
text-align:center;
}
span.bild_links, span.bild_rechts, span.bild_zentriert {
display:block;
}
'''
remove_tags = [dict(id='navi_top'),
dict(id='navi_bottom'),
dict(id='logo'),

View File

@ -6,6 +6,7 @@ class AdvancedUserRecipe1257302745(BasicNewsRecipe):
language = 'en'
__author__ = 'onyxrev'
max_articles_per_feed = 100
no_stylesheets = True
remove_tags_before = {'class':'storytitle'}
remove_tags_after = dict(name='div', attrs={'id':'storytext' })

View File

@ -0,0 +1,38 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1279258912(BasicNewsRecipe):
title = u'Orlando Sentinel'
oldest_article = 3
max_articles_per_feed = 100
feeds = [
(u'News', u'http://feeds.feedburner.com/orlandosentinel/news'),
(u'Opinion', u'http://feeds.feedburner.com/orlandosentinel/news/opinion'),
(u'Business', u'http://feeds.feedburner.com/orlandosentinel/business'),
(u'Technology', u'http://feeds.feedburner.com/orlandosentinel/technology'),
(u'Space and Science', u'http://feeds.feedburner.com/orlandosentinel/news/space'),
(u'Entertainment', u'http://feeds.feedburner.com/orlandosentinel/entertainment'),
(u'Life and Family', u'http://feeds.feedburner.com/orlandosentinel/features/lifestyle'),
]
__author__ = 'rty'
pubisher = 'OrlandoSentinel.com'
description = 'Orlando, Florida, Newspaper'
category = 'News, Orlando, Florida'
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'en'
encoding = 'utf-8'
conversion_options = {'linearize_tables':True}
masthead_url = 'http://www.orlandosentinel.com/media/graphic/2009-07/46844851.gif'
keep_only_tags = [
dict(name='div', attrs={'class':'story'})
]
remove_tags = [
dict(name='div', attrs={'class':['articlerail','tools','comment-group','clearfix']}),
]
remove_tags_after = [
dict(name='p', attrs={'class':'copyright'}),
]

View 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

View File

@ -0,0 +1,34 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1278773519(BasicNewsRecipe):
title = u'Waco Tribune Herald'
__author__ = 'rty'
pubisher = 'A Robinson Media Company'
description = 'Waco, Texas, Newspaper'
category = 'News, Texas, Waco'
oldest_article = 7
max_articles_per_feed = 100
feeds = [
(u'News', u'http://www.wacotrib.com/news/index.rss2'),
(u'Sports', u'http://www.wacotrib.com/sports/index.rss2'),
(u'AccessWaco', u'http://www.wacotrib.com/accesswaco/index.rss2'),
(u'Opinions', u'http://www.wacotrib.com/opinion/index.rss2')
]
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'en'
encoding = 'utf-8'
conversion_options = {'linearize_tables':True}
masthead_url = 'http://media.wacotrib.com/designimages/wacotrib_logo.jpg'
keep_only_tags = [
dict(name='div', attrs={'class':'twoColumn left'}),
]
remove_tags = [
dict(name='div', attrs={'class':'right blueLinks'}),
]
remove_tags_after = [
dict(name='div', attrs={'class':'dottedRule'}),
]

View File

@ -111,7 +111,6 @@
or (@shadow = 'true')
or (@hidden = 'true')
or (@outline = 'true')
">
<emph rend = "paragraph-emph">
<xsl:apply-templates/>
@ -266,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>
@ -277,6 +287,26 @@
<xsl:value-of select="count(preceding::rtf:footnote) + 1"/>
<xsl:text>]</xsl:text>
</xsl:when>
<xsl:when test="(@superscript = 'true')">
<xsl:element name="sup">
<xsl:element name="span">
<xsl:attribute name="class">
<c:inline-class/>
</xsl:attribute>
<xsl:apply-templates/>
</xsl:element>
</xsl:element>
</xsl:when>
<xsl:when test="(@underscript = 'true')">
<xsl:element name="sub">
<xsl:element name="span">
<xsl:attribute name="class">
<c:inline-class/>
</xsl:attribute>
<xsl:apply-templates/>
</xsl:element>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="span">
<xsl:attribute name="class">

View File

@ -2,6 +2,7 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, os, re, logging, time, mimetypes, \
__builtin__, warnings, multiprocessing
from urllib import getproxies
@ -13,12 +14,13 @@ from functools import partial
warnings.simplefilter('ignore', DeprecationWarning)
from calibre.startup import plugins, winutil, winutilerror
from calibre.constants import iswindows, isosx, islinux, isfreebsd, isfrozen, \
terminal_controller, preferred_encoding, \
__appname__, __version__, __author__, \
win32event, win32api, winerror, fcntl, \
filesystem_encoding
filesystem_encoding, plugins, config_dir
from calibre.startup import winutil, winutilerror
import mechanize
if False:
@ -361,6 +363,8 @@ def strftime(fmt, t=None):
before 1900 '''
if t is None:
t = time.localtime()
if hasattr(t, 'timetuple'):
t = t.timetuple()
early_year = t[0] < 1900
if early_year:
replacement = 1900 if t[0]%4 == 0 else 1901
@ -438,6 +442,9 @@ xml_entity_to_unicode = partial(entity_to_unicode, result_exceptions = {
'>' : '&gt;',
'&' : '&amp;'})
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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
@ -481,7 +488,6 @@ def ipython(user_ns=None):
sys.argv = ['ipython']
if user_ns is None:
user_ns = locals()
from calibre.utils.config import config_dir
ipydir = os.path.join(config_dir, ('_' if iswindows else '.')+'ipython')
os.environ['IPYTHONDIR'] = ipydir
if not os.path.exists(ipydir):

View File

@ -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.10'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re
@ -14,7 +14,7 @@ numeric_version = tuple(_ver)
Various run time constants.
'''
import sys, locale, codecs
import sys, locale, codecs, os
from calibre.utils.terminfo import TerminalController
terminal_controller = TerminalController(sys.stdout)
@ -47,7 +47,7 @@ def debug():
global DEBUG
DEBUG = True
################################################################################
# plugins {{{
plugins = None
if plugins is None:
# Load plugins
@ -80,3 +80,22 @@ if plugins is None:
return plugins
plugins = load_plugins()
# }}}
# config_dir {{{
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
elif iswindows:
if plugins['winutil'][0] is None:
raise Exception(plugins['winutil'][1])
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
if not os.access(config_dir, os.W_OK|os.X_OK):
config_dir = os.path.expanduser('~')
config_dir = os.path.join(config_dir, 'calibre')
elif isosx:
config_dir = os.path.expanduser('~/Library/Preferences/calibre')
else:
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre')
# }}}

View File

@ -446,7 +446,7 @@ from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
BOOQ, ELONEX, POCKETBOOK301, MENTOR
from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK
from calibre.devices.jetbook.driver import JETBOOK, MIBUK
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
from calibre.devices.nook.driver import NOOK
from calibre.devices.prs505.driver import PRS505
@ -467,12 +467,12 @@ from calibre.devices.kobo.driver import KOBO
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing
from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.library.catalog import CSV_XML, EPUB_MOBI
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested
from calibre.ebooks.epub.fix.epubcheck import Epubcheck
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, Unmanifested, Epubcheck]
LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested, Epubcheck]
plugins += [
ComicInput,
EPUBInput,
@ -517,6 +517,7 @@ plugins += [
IREXDR1000,
IREXDR800,
JETBOOK,
MIBUK,
SHINEBOOK,
POCKETBOOK360,
POCKETBOOK301,

View File

@ -30,7 +30,8 @@ class ANDROID(USBMS):
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
# Samsung
0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]},
0x04e8 : { 0x681d : [0x0222, 0x0400],
0x681c : [0x0222, 0x0224, 0x0400]},
# Acer
0x502 : { 0x3203 : [0x0100]},

View File

@ -2668,7 +2668,7 @@ class ITUNES(DriverBase):
index = metadata.series_index
integer = int(index)
fraction = index-integer
series_index = '%04d%%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
if lb_added:
lb_added.SortName = "%s %s" % (metadata.series, series_index)
lb_added.Genre = metadata.series

View File

@ -43,6 +43,7 @@ class THEBOOK(N516):
BCD = [0x399]
MAIN_MEMORY_VOLUME_LABEL = 'The Book Main Memory'
EBOOK_DIR_MAIN = 'My books'
WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE'
class ALEX(N516):

View File

@ -80,3 +80,21 @@ class JETBOOK(USBMS):
return mi
class MIBUK(USBMS):
name = 'MiBuk Wolder Device Interface'
description = _('Communicate with the MiBuk Wolder reader.')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'txt', 'rtf', 'pdf']
VENDOR_ID = [0x0525]
PRODUCT_ID = [0xa4a5]
BCD = [0x314]
SUPPORTS_SUB_DIRS = True
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = 'WOLDERMIBUK'

View File

@ -6,6 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from calibre.devices.usbms.driver import USBMS
class PALMPRE(USBMS):
@ -83,7 +85,14 @@ class PDNOVEL(USBMS):
VENDOR_NAME = 'ANDROID'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
THUMBNAIL_HEIGHT = 144
EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False
def upload_cover(self, path, filename, metadata):
coverdata = getattr(metadata, 'thumbnail', None)
if coverdata and coverdata[2]:
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
coverfile.write(coverdata[2])

View File

@ -10,10 +10,10 @@ from base64 import b64decode
from uuid import uuid4
from lxml import etree
from calibre import prints, guess_type
from calibre import prints, guess_type, isbytestring
from calibre.devices.errors import DeviceError
from calibre.devices.usbms.driver import debug_print
from calibre.constants import DEBUG
from calibre.constants import DEBUG, preferred_encoding
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata import authors_to_string, title_sort
@ -473,6 +473,13 @@ class XMLCache(object):
# if the case of a tie, and hope it is right.
timestamp = os.path.getmtime(path)
rec_date = record.get('date', None)
def clean(x):
if isbytestring(x):
x = x.decode(preferred_encoding, 'replace')
x.replace(u'\0', '')
return x
if not getattr(book, '_new_book', False): # book is not new
if strftime(timestamp, zone=time.gmtime) == rec_date:
gtz_count += 1
@ -486,19 +493,19 @@ class XMLCache(object):
tz = time.gmtime
debug_print("Using GMT TZ for new book", book.lpath)
date = strftime(timestamp, zone=tz)
record.set('date', date)
record.set('date', clean(date))
record.set('size', str(os.stat(path).st_size))
record.set('size', clean(str(os.stat(path).st_size)))
title = book.title if book.title else _('Unknown')
record.set('title', title)
record.set('title', clean(title))
ts = book.title_sort
if not ts:
ts = title_sort(title)
record.set('titleSorter', ts)
record.set('titleSorter', clean(ts))
if self.use_author_sort and book.author_sort is not None:
record.set('author', book.author_sort)
record.set('author', clean(book.author_sort))
else:
record.set('author', authors_to_string(book.authors))
record.set('author', clean(authors_to_string(book.authors)))
ext = os.path.splitext(path)[1]
if ext:
ext = ext[1:].lower()
@ -506,7 +513,7 @@ class XMLCache(object):
if mime is None:
mime = guess_type('a.'+ext)[0]
if mime is not None:
record.set('mime', mime)
record.set('mime', clean(mime))
if 'sourceid' not in record.attrib:
record.set('sourceid', '1')
if 'id' not in record.attrib:

View File

@ -72,13 +72,13 @@ class Book(MetaInformation):
def thumbnail(self):
return None
def smart_update(self, other):
def smart_update(self, other, replace_metadata=False):
'''
Merge the information in C{other} into self. In case of conflicts, the information
in C{other} takes precedence, unless the information in C{other} is NULL.
'''
MetaInformation.smart_update(self, other, replace_tags=True)
MetaInformation.smart_update(self, other, replace_metadata)
for attr in self.BOOK_ATTRS:
if hasattr(other, attr):
@ -116,7 +116,7 @@ class BookList(_BookList):
self.append(book)
return True
if replace_metadata:
self[b].smart_update(book)
self[b].smart_update(book, replace_metadata=True)
return True
return False

View File

@ -177,6 +177,7 @@ class CHMInput(InputFormatPlugin):
chapter_path = None
if match_string(node.tag, 'object') and match_string(node.attrib['type'], 'text/sitemap'):
chapter_title = None
for child in node:
if match_string(child.tag,'param') and match_string(child.attrib['name'], 'name'):
chapter_title = child.attrib['value']

View File

@ -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)

View File

@ -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)
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')

View File

@ -268,10 +268,12 @@ class MetaInformation(object):
):
prints(x, getattr(self, x, 'None'))
def smart_update(self, mi, replace_tags=False):
def smart_update(self, mi, replace_metadata=False):
'''
Merge the information in C{mi} into self. In case of conflicts, the information
in C{mi} takes precedence, unless the information in mi is NULL.
Merge the information in C{mi} into self. In case of conflicts, the
information in C{mi} takes precedence, unless the information in mi is
NULL. If replace_metadata is True, then the information in mi always
takes precedence.
'''
if mi.title and mi.title != _('Unknown'):
self.title = mi.title
@ -285,15 +287,17 @@ class MetaInformation(object):
'cover', 'guide', 'book_producer',
'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights',
'publication_type', 'uuid'):
if hasattr(mi, attr):
if replace_metadata:
setattr(self, attr, getattr(mi, attr, 1.0 if \
attr == 'series_index' else None))
elif hasattr(mi, attr):
val = getattr(mi, attr)
if val is not None:
setattr(self, attr, val)
if mi.tags:
if replace_tags:
if replace_metadata:
self.tags = mi.tags
else:
elif mi.tags:
self.tags += mi.tags
self.tags = list(set(self.tags))
@ -308,6 +312,9 @@ class MetaInformation(object):
if len(other_cover) > len(self_cover):
self.cover_data = mi.cover_data
if replace_metadata:
self.comments = getattr(mi, 'comments', '')
else:
my_comments = getattr(self, 'comments', '')
other_comments = getattr(mi, 'comments', '')
if not my_comments:

View File

@ -12,11 +12,15 @@ import re
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.chardet import xml_to_unicode
from calibre import entity_to_unicode
from calibre.utils.date import parse_date
def get_metadata(stream):
src = stream.read()
return get_metadata_(src)
def get_meta_regexp_(name):
return re.compile('<meta name=[\'"]' + name + '[\'"] content=[\'"](.+?)[\'"]\s*/?>', re.IGNORECASE)
def get_metadata_(src, encoding=None):
if not isinstance(src, unicode):
if not encoding:
@ -24,6 +28,9 @@ def get_metadata_(src, encoding=None):
else:
src = src.decode(encoding, 'replace')
# Meta data definitions as in
# http://www.mobileread.com/forums/showpost.php?p=712544&postcount=9
# Title
title = None
pat = re.compile(r'<!--.*?TITLE=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
@ -35,6 +42,13 @@ def get_metadata_(src, encoding=None):
match = pat.search(src)
if match:
title = match.group(1)
if not title:
for x in ('Title','DC.title','DCTERMS.title'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
title = match.group(1)
break
# Author
author = None
@ -42,7 +56,15 @@ def get_metadata_(src, encoding=None):
match = pat.search(src)
if match:
author = match.group(2).replace(',', ';')
else:
for x in ('Author','DC.creator.aut','DCTERMS.creator.aut'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
author = match.group(1)
break
# Create MetaInformation with Title and Author
ent_pat = re.compile(r'&(\S+)?;')
if title:
title = ent_pat.sub(entity_to_unicode, title)
@ -51,18 +73,158 @@ def get_metadata_(src, encoding=None):
mi = MetaInformation(title, [author] if author else None)
# Publisher
publisher = None
pat = re.compile(r'<!--.*?PUBLISHER=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
match = pat.search(src)
if match:
mi.publisher = match.group(2)
publisher = match.group(2)
else:
for x in ('Publisher','DC.publisher','DCTERMS.publisher'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
publisher = match.group(1)
break
if publisher:
mi.publisher = ent_pat.sub(entity_to_unicode, publisher)
# ISBN
isbn = None
pat = re.compile(r'<!--.*?ISBN=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
isbn = match.group(1)
else:
for x in ('ISBN','DC.identifier.ISBN','DCTERMS.identifier.ISBN'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
isbn = match.group(1)
break
if isbn:
mi.isbn = re.sub(r'[^0-9xX]', '', isbn)
# LANGUAGE
language = None
pat = re.compile(r'<!--.*?LANGUAGE=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
language = match.group(1)
else:
for x in ('DC.language','DCTERMS.language'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
language = match.group(1)
break
if language:
mi.language = language
# PUBDATE
pubdate = None
pat = re.compile(r'<!--.*?PUBDATE=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
pubdate = match.group(1)
else:
for x in ('Pubdate','Date of publication','DC.date.published','DC.date.publication','DC.date.issued','DCTERMS.issued'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
pubdate = match.group(1)
break
if pubdate:
try:
mi.pubdate = parse_date(pubdate)
except:
pass
# TIMESTAMP
timestamp = None
pat = re.compile(r'<!--.*?TIMESTAMP=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
timestamp = match.group(1)
else:
for x in ('Timestamp','Date of creation','DC.date.created','DC.date.creation','DCTERMS.created'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
timestamp = match.group(1)
break
if timestamp:
try:
mi.timestamp = parse_date(timestamp)
except:
pass
# SERIES
series = None
pat = re.compile(r'<!--.*?SERIES=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
series = match.group(1)
else:
pat = get_meta_regexp_("Series")
match = pat.search(src)
if match:
series = match.group(1)
if series:
mi.series = ent_pat.sub(entity_to_unicode, series)
# RATING
rating = None
pat = re.compile(r'<!--.*?RATING=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
rating = match.group(1)
else:
pat = get_meta_regexp_("Rating")
match = pat.search(src)
if match:
rating = match.group(1)
if rating:
try:
mi.rating = float(rating)
if mi.rating < 0:
mi.rating = 0
if mi.rating > 5:
mi.rating /= 2.
if mi.rating > 5:
mi.rating = 0
except:
pass
# COMMENTS
comments = None
pat = re.compile(r'<!--.*?COMMENTS=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
comments = match.group(1)
else:
pat = get_meta_regexp_("Comments")
match = pat.search(src)
if match:
comments = match.group(1)
if comments:
mi.comments = ent_pat.sub(entity_to_unicode, comments)
# TAGS
tags = None
pat = re.compile(r'<!--.*?TAGS=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
tags = match.group(1)
else:
pat = get_meta_regexp_("Tags")
match = pat.search(src)
if match:
tags = match.group(1)
if tags:
mi.tags = [x.strip() for x in ent_pat.sub(entity_to_unicode,
tags).split(",")]
# Ready to return MetaInformation
return mi

View File

@ -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:

View File

@ -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

View File

@ -262,8 +262,11 @@ class CSSFlattener(object):
indent = asfloat(style['text-indent'], 0)
left += margin
if (left + indent) < 0:
try:
percent = (margin - indent) / style['width']
cssdict['margin-left'] = "%d%%" % (percent * 100)
except ZeroDivisionError:
pass
left -= indent
if 'display' in cssdict and cssdict['display'] == 'in-line':
cssdict['display'] = 'inline'

View File

@ -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)

View File

@ -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,

View File

@ -578,9 +578,7 @@ class DeleteAction(object): # {{{
if row is not None:
ci = view.model().index(row, 0)
if ci.isValid():
view.setCurrentIndex(ci)
sm = view.selectionModel()
sm.select(ci, sm.Select)
view.set_current_row(row)
else:
if not confirm('<p>'+_('The selected books will be '
'<b>permanently deleted</b> '
@ -806,11 +804,11 @@ class EditMetadataAction(object): # {{{
for src_id in src_ids:
src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True)
if src_mi.comments and orig_dest_comments != src_mi.comments:
if not dest_mi.comments or len(dest_mi.comments) == 0:
if not dest_mi.comments:
dest_mi.comments = src_mi.comments
else:
dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments)
if src_mi.title and src_mi.title and (not dest_mi.title or
if src_mi.title and (not dest_mi.title or
dest_mi.title == _('Unknown')):
dest_mi.title = src_mi.title
if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] ==
@ -821,8 +819,7 @@ class EditMetadataAction(object): # {{{
if not dest_mi.tags:
dest_mi.tags = src_mi.tags
else:
for tag in src_mi.tags:
dest_mi.tags.append(tag)
dest_mi.tags.extend(src_mi.tags)
if src_mi.cover and not dest_mi.cover:
dest_mi.cover = src_mi.cover
if not dest_mi.publisher:
@ -833,6 +830,44 @@ class EditMetadataAction(object): # {{{
dest_mi.series = src_mi.series
dest_mi.series_index = src_mi.series_index
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
for key in db.field_metadata: #loop thru all defined fields
if db.field_metadata[key]['is_custom']:
colnum = db.field_metadata[key]['colnum']
# Get orig_dest_comments before it gets changed
if db.field_metadata[key]['datatype'] == 'comments':
orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
for src_id in src_ids:
dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
src_value = db.get_custom(src_id, num=colnum, index_is_id=True)
if db.field_metadata[key]['datatype'] == 'comments':
if src_value and src_value != orig_dest_value:
if not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
else:
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
if db.field_metadata[key]['datatype'] in \
('bool', 'int', 'float', 'rating', 'datetime') \
and not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'series' \
and not dest_value:
if src_value:
src_index = db.get_custom_extra(src_id, num=colnum, index_is_id=True)
db.set_custom(dest_id, src_value, num=colnum, extra=src_index)
if db.field_metadata[key]['datatype'] == 'text' \
and not db.field_metadata[key]['is_multiple'] \
and not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'text' \
and db.field_metadata[key]['is_multiple']:
if src_value:
if not dest_value:
dest_value = src_value
else:
dest_value.extend(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
# }}}
def edit_device_collections(self, view, oncard=None):

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.gui2 import gprefs
from calibre.gui2.catalog.catalog_bibtex_ui import Ui_Form
from PyQt4.Qt import QWidget, QListWidgetItem
class PluginWidget(QWidget, Ui_Form):
TITLE = _('BibTeX Options')
HELP = _('Options specific to')+' BibTeX '+_('output')
OPTION_FIELDS = [('bib_cit','{authors}{id}'),
('bib_entry', 0), #mixed
('bibfile_enc', 0), #utf-8
('bibfile_enctag', 0), #strict
('impcit', True) ]
sync_enabled = False
formats = set(['bib'])
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setupUi(self)
from calibre.library.catalog import FIELDS
self.all_fields = []
for x in FIELDS :
if x != 'all':
self.all_fields.append(x)
QListWidgetItem(x, self.db_fields)
def initialize(self, name): #not working properly to update
self.name = name
fields = gprefs.get(name+'_db_fields', self.all_fields)
# Restore the activated db_fields from last use
for x in xrange(self.db_fields.count()):
item = self.db_fields.item(x)
item.setSelected(unicode(item.text()) in fields)
# Update dialog fields from stored options
for opt in self.OPTION_FIELDS:
opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
getattr(self, opt[0]).setCurrentIndex(opt_value)
elif opt[0] == 'impcit' :
getattr(self, opt[0]).setChecked(opt_value)
else:
getattr(self, opt[0]).setText(opt_value)
def options(self):
# Save the currently activated fields
fields = []
for x in xrange(self.db_fields.count()):
item = self.db_fields.item(x)
if item.isSelected():
fields.append(unicode(item.text()))
gprefs.set(self.name+'_db_fields', fields)
# Dictionary currently activated fields
if len(self.db_fields.selectedItems()):
opts_dict = {'fields':[unicode(item.text()) for item in self.db_fields.selectedItems()]}
else:
opts_dict = {'fields':['all']}
# Save/return the current options
# bib_cit stores as text
# 'bibfile_enc','bibfile_enctag' stores as int (Indexes)
for opt in self.OPTION_FIELDS:
if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
opt_value = getattr(self,opt[0]).currentIndex()
elif opt[0] == 'impcit' :
opt_value = getattr(self, opt[0]).isChecked()
else :
opt_value = unicode(getattr(self, opt[0]).text())
gprefs.set(self.name + '_' + opt[0], opt_value)
opts_dict[opt[0]] = opt_value
return opts_dict

View File

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>579</width>
<height>411</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Bib file encoding:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Fields to include in output:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QComboBox" name="bibfile_enc">
<item>
<property name="text">
<string notr="true">utf-8</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">cp1252</string>
</property>
</item>
<item>
<property name="text">
<string>ascii/LaTeX</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1" rowspan="12">
<widget class="QListWidget" name="db_fields">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string extracomment="Select all fields to be exported"/>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Encoding configuration (change if you have errors) :</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QComboBox" name="bibfile_enctag">
<item>
<property name="text">
<string>strict</string>
</property>
</item>
<item>
<property name="text">
<string>replace</string>
</property>
</item>
<item>
<property name="text">
<string>ignore</string>
</property>
</item>
<item>
<property name="text">
<string>backslashreplace</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>60</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>BibTeX entry type:</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QComboBox" name="bib_entry">
<item>
<property name="text">
<string>mixed</string>
</property>
</item>
<item>
<property name="text">
<string>misc</string>
</property>
</item>
<item>
<property name="text">
<string>book</string>
</property>
</item>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="impcit">
<property name="text">
<string>Create a citation tag?</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Expression to form the BibTeX citation tag:</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLineEdit" name="bib_cit"/>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Some explanation about this template:
-The fields availables are 'author_sort', 'authors', 'id',
'isbn', 'pubdate', 'publisher', 'series_index', 'series',
'tags', 'timestamp', 'title', 'uuid'
-For list types ie authors and tags, only the first element
wil be selected.
-For time field, only the date will be used. </string>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -20,6 +20,30 @@
<string>Book Cover</string>
</property>
<layout class="QGridLayout" name="_2">
<item row="0" column="0">
<layout class="QHBoxLayout" name="_3">
<item>
<widget class="ImageView" name="cover" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_prefer_metadata_cover">
<property name="text">
<string>Use cover from &amp;source file</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QVBoxLayout" name="_4">
<property name="spacing">
@ -71,30 +95,6 @@
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_prefer_metadata_cover">
<property name="text">
<string>Use cover from &amp;source file</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="_3">
<item>
<widget class="ImageView" name="cover" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
</layout>
<zorder>opt_prefer_metadata_cover</zorder>
<zorder></zorder>
@ -232,9 +232,6 @@
<property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item row="6" column="1">

View File

@ -28,9 +28,10 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
if not db or not book_id:
self.button_box.addButton(QDialogButtonBox.Open)
else:
self.select_format(db, book_id)
elif not self.select_format(db, book_id):
self.cancelled = True
return
self.cancelled = False
self.connect(self.button_box, SIGNAL('clicked(QAbstractButton*)'), self.button_clicked)
self.connect(self.regex, SIGNAL('textChanged(QString)'), self.regex_valid)
self.connect(self.test, SIGNAL('clicked()'), self.do_test)
@ -79,10 +80,12 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
format = d.format()
if not format:
error_dialog(self, _('No formats available'), _('Cannot build regex using the GUI builder without a book.'))
QDialog.reject()
else:
error_dialog(self, _('No formats available'),
_('Cannot build regex using the GUI builder without a book.'),
show=True)
return False
self.open_book(db.format_abspath(book_id, format, index_is_id=True))
return True
def open_book(self, pathtoebook):
self.iterator = EbookIterator(pathtoebook)
@ -117,6 +120,8 @@ class RegexEdit(QWidget, Ui_Edit):
def builder(self):
bld = RegexBuilder(self.db, self.book_id, self.edit.text(), self)
if bld.cancelled:
return
if bld.exec_() == bld.Accepted:
self.edit.setText(bld.regex.text())

View File

@ -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>

View File

@ -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>

View File

@ -72,7 +72,14 @@ class DeviceJob(BaseJob): # {{{
if self._aborted:
return
self.failed = True
self._details = unicode(err) + '\n\n' + \
try:
ex = unicode(err)
except:
try:
ex = str(err).decode(preferred_encoding, 'replace')
except:
ex = repr(err)
self._details = ex + '\n\n' + \
traceback.format_exc()
self.exception = err
finally:
@ -395,8 +402,6 @@ class DeviceAction(QAction): # {{{
class DeviceMenu(QMenu): # {{{
fetch_annotations = pyqtSignal()
connect_to_folder = pyqtSignal()
connect_to_itunes = pyqtSignal()
disconnect_mounted_device = pyqtSignal()
def __init__(self, parent=None):
@ -408,26 +413,6 @@ class DeviceMenu(QMenu): # {{{
self.set_default_menu = QMenu(_('Set default send to device action'))
self.set_default_menu.setIcon(QIcon(I('config.svg')))
opts = email_config().parse()
default_account = None
if opts.accounts:
self.email_to_menu = self.addMenu(_('Email to')+'...')
keys = sorted(opts.accounts.keys())
for account in keys:
formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats
if default:
default_account = (dest, False, False, I('mail.svg'),
_('Email to')+' '+account)
action1 = DeviceAction(dest, False, False, I('mail.svg'),
_('Email to')+' '+account)
action2 = DeviceAction(dest, True, False, I('mail.svg'),
_('Email to')+' '+account+ _(' and delete from library'))
map(self.email_to_menu.addAction, (action1, action2))
map(self._memory.append, (action1, action2))
self.email_to_menu.addSeparator()
action1.a_s.connect(self.action_triggered)
action2.a_s.connect(self.action_triggered)
basic_actions = [
('main:', False, False, I('reader.svg'),
@ -457,13 +442,6 @@ class DeviceMenu(QMenu): # {{{
]
if default_account is not None:
for x in (basic_actions, delete_actions):
ac = list(default_account)
if x is delete_actions:
ac[1] = True
x.insert(1, tuple(ac))
for menu in (self, self.set_default_menu):
for actions, desc in (
(basic_actions, ''),
@ -502,21 +480,7 @@ class DeviceMenu(QMenu): # {{{
config['default_send_to_device_action'] = repr(action)
self.group.triggered.connect(self.change_default_action)
if opts.accounts:
self.addSeparator()
self.addMenu(self.email_to_menu)
self.addSeparator()
mitem = self.addAction(QIcon(I('document_open.svg')), _('Connect to folder'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
self.connect_to_folder_action = mitem
mitem = self.addAction(QIcon(I('devices/itunes.png')),
_('Connect to iTunes'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
self.connect_to_itunes_action = mitem
mitem = self.addAction(QIcon(I('eject.svg')), _('Eject device'))
mitem.setEnabled(False)
@ -638,7 +602,8 @@ 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.share_conn_menu.connect_to_folder.connect(self.connect_to_folder)
self.share_conn_menu.connect_to_itunes.connect(self.connect_to_itunes)
self.emailer = Emailer()
self.emailer.start()
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
@ -676,21 +641,20 @@ class DeviceMixin(object): # {{{
def create_device_menu(self):
self._sync_menu = DeviceMenu(self)
self.share_conn_menu.build_email_entries(self._sync_menu)
self.action_sync.setMenu(self._sync_menu)
self.connect(self._sync_menu,
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.dispatch_sync_event)
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
self._sync_menu.connect_to_folder.connect(self.connect_to_folder)
self._sync_menu.connect_to_itunes.connect(self.connect_to_itunes)
self._sync_menu.disconnect_mounted_device.connect(self.disconnect_mounted_device)
if self.device_connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
self._sync_menu.connect_to_itunes_action.setEnabled(False)
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.connect_to_itunes_action.setEnabled(True)
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
def device_job_exception(self, job):
@ -727,16 +691,16 @@ class DeviceMixin(object): # {{{
def set_device_menu_items_state(self, connected):
if connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
self._sync_menu.connect_to_itunes_action.setEnabled(False)
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
self._sync_menu.enable_device_actions(True,
self.device_manager.device.card_prefix(),
self.device_manager.device)
self.eject_action.setEnabled(True)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.connect_to_itunes_action.setEnabled(True)
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
self._sync_menu.enable_device_actions(False)
self.eject_action.setEnabled(False)
@ -755,16 +719,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.location_manager.update_devices()
self.refresh_ondevice_info(device_connected=False)
def info_read(self, job):
'''
@ -773,7 +735,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))
@ -985,6 +948,8 @@ class DeviceMixin(object): # {{{
else:
self.status_bar.show_message(_('Sent by email:') + ', '.join(good),
5000)
if remove:
self.library_view.model().delete_books_by_id(remove)
def cover_to_thumbnail(self, data):
p = QPixmap()
@ -1075,9 +1040,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,
@ -1139,9 +1104,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,
@ -1262,7 +1227,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()
@ -1438,7 +1404,8 @@ class DeviceMixin(object): # {{{
for book in booklist:
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
if update_metadata:
book.smart_update(self.db_book_uuid_cache[book.uuid])
book.smart_update(self.db_book_uuid_cache[book.uuid],
replace_metadata=True)
book.in_library = True
# ensure that the correct application_id is set
book.application_id = \
@ -1453,12 +1420,14 @@ class DeviceMixin(object): # {{{
if getattr(book, 'application_id', None) in d['db_ids']:
book.in_library = True
if update_metadata:
book.smart_update(d['db_ids'][book.application_id])
book.smart_update(d['db_ids'][book.application_id],
replace_metadata=True)
continue
if book.db_id in d['db_ids']:
book.in_library = True
if update_metadata:
book.smart_update(d['db_ids'][book.db_id])
book.smart_update(d['db_ids'][book.db_id],
replace_metadata=True)
continue
if book.authors:
# Compare against both author and author sort, because
@ -1468,11 +1437,13 @@ class DeviceMixin(object): # {{{
if book_authors in d['authors']:
book.in_library = True
if update_metadata:
book.smart_update(d['authors'][book_authors])
book.smart_update(d['authors'][book_authors],
replace_metadata=True)
elif book_authors in d['author_sort']:
book.in_library = True
if update_metadata:
book.smart_update(d['author_sort'][book_authors])
book.smart_update(d['author_sort'][book_authors],
replace_metadata=True)
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:

View File

@ -0,0 +1,84 @@
#!/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
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'):
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)

View 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 &amp;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 &amp;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>&amp;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>&amp;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>

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import QDialog
from PyQt4.Qt import Qt, QDialog
from calibre.gui2.dialogs.comments_dialog_ui import Ui_CommentsDialog
class CommentsDialog(QDialog, Ui_CommentsDialog):
@ -12,6 +12,11 @@ class CommentsDialog(QDialog, Ui_CommentsDialog):
QDialog.__init__(self, parent)
Ui_CommentsDialog.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
if text is not None:
self.textbox.setPlainText(text)
self.textbox.setTabChangesFocus(True)

View File

@ -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
@ -195,22 +195,32 @@ class PluginModel(QAbstractItemModel):
class CategoryModel(QStringListModel):
CATEGORIES = [
('general', _('General'), 'dialog_information.svg'),
('interface', _('Interface'), 'lookfeel.svg'),
('conversion', _('Conversion'), 'convert.svg'),
('email', _('Email\nDelivery'), 'mail.svg'),
('add/save', _('Add/Save'), 'save.svg'),
('advanced', _('Advanced'), 'view.svg'),
('server', _('Content\nServer'), 'network-server.svg'),
('plugins', _('Plugins'), 'plugins.svg'),
]
def __init__(self, *args):
QStringListModel.__init__(self, *args)
self.setStringList([_('General'), _('Interface'), _('Conversion'),
_('Email\nDelivery'), _('Add/Save'),
_('Advanced'), _('Content\nServer'), _('Plugins')])
self.icons = list(map(QVariant, map(QIcon,
[I('dialog_information.svg'), I('lookfeel.svg'),
I('convert.svg'),
I('mail.svg'), I('save.svg'), I('view.svg'),
I('network-server.svg'), I('plugins.svg')])))
self.setStringList([x[1] for x in self.CATEGORIES])
def data(self, index, role):
if role == Qt.DecorationRole:
return self.icons[index.row()]
return QVariant(QIcon(I(self.CATEGORIES[index.row()][2])))
return QStringListModel.data(self, index, role)
def index_for_name(self, name):
for i, x in enumerate(self.CATEGORIES):
if x[0] == name:
return self.index(i)
return self.index(0)
class EmailAccounts(QAbstractTableModel):
def __init__(self, accounts):
@ -332,9 +342,9 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
def category_current_changed(self, n, p):
self.stackedWidget.setCurrentIndex(n.row())
def __init__(self, parent, library_view, server=None):
def __init__(self, parent, library_view, server=None,
initial_category='general'):
ResizableDialog.__init__(self, parent)
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
self._category_model = CategoryModel()
self.category_view.currentChanged = self.category_current_changed
@ -344,9 +354,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']
@ -389,10 +396,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.add_custcol_button.clicked.connect(self.add_custcol)
self.edit_custcol_button.clicked.connect(self.edit_custcol)
icons = config['toolbar_icon_size']
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
self.show_toolbar_text.setChecked(config['show_text_in_toolbar'])
output_formats = sorted(available_output_formats())
output_formats.remove('oeb')
for f in output_formats:
@ -469,7 +472,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.button_osx_symlinks.setVisible(isosx)
self.separate_cover_flow.setChecked(config['separate_cover_flow'])
self.setup_email_page()
self.category_view.setCurrentIndex(self.category_view.model().index(0))
self.delete_news.setEnabled(bool(self.sync_news.isChecked()))
self.connect(self.sync_news, SIGNAL('toggled(bool)'),
self.delete_news.setEnabled)
@ -496,6 +498,22 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_gui_layout.setCurrentIndex(li)
self.opt_disable_animations.setChecked(config['disable_animations'])
self.opt_show_donate_button.setChecked(config['show_donate_button'])
idx = 0
for i, x in enumerate([(_('Small'), 'small'), (_('Medium'), 'medium'),
(_('Large'), 'large')]):
if x[1] == gprefs.get('toolbar_icon_size', 'medium'):
idx = i
self.opt_toolbar_icon_size.addItem(x[0], x[1])
self.opt_toolbar_icon_size.setCurrentIndex(idx)
idx = 0
for i, x in enumerate([(_('Automatic'), 'auto'), (_('Always'), 'always'),
(_('Never'), 'never')]):
if x[1] == gprefs.get('toolbar_text', 'auto'):
idx = i
self.opt_toolbar_text.addItem(x[0], x[1])
self.opt_toolbar_text.setCurrentIndex(idx)
self.category_view.setCurrentIndex(self.category_view.model().index_for_name(initial_category))
def check_port_value(self, *args):
port = self.port.value()
@ -813,12 +831,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):
@ -839,14 +851,11 @@ 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
must_restart = self.apply_custom_column_changes()
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
config['disable_tray_notification'] = not self.systray_notifications.isChecked()
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
@ -874,6 +883,10 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['disable_animations'] = bool(self.opt_disable_animations.isChecked())
config['show_donate_button'] = bool(self.opt_show_donate_button.isChecked())
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
for x in ('toolbar_icon_size', 'toolbar_text'):
w = getattr(self, 'opt_'+x)
data = w.itemData(w.currentIndex()).toString()
gprefs[x] = unicode(data)
fmts = []
for i in range(self.viewer.count()):
if self.viewer.item(i).checkState() == Qt.Checked:
@ -882,17 +895,6 @@ 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 '
@ -970,6 +972,5 @@ if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
d=ConfigDialog(None, LibraryDatabase2('/tmp'))
d.category_view.setCurrentIndex(d.category_view.model().index(0))
d.show()
app.exec_()

View File

@ -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>&amp;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">
@ -390,21 +346,21 @@
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news">
<property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news">
<property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_6">
@ -421,54 +377,6 @@
</item>
</layout>
</item>
<item row="10" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="toolbar_button_size">
<item>
<property name="text">
<string>Large</string>
</property>
</item>
<item>
<property name="text">
<string>Medium</string>
</property>
</item>
<item>
<property name="text">
<string>Small</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&amp;Button size in toolbar</string>
</property>
<property name="buddy">
<cstring>toolbar_button_size</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="show_toolbar_text">
<property name="text">
<string>Show &amp;text in toolbar buttons</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="11" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
@ -672,6 +580,41 @@
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Icon size:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_6">

View File

@ -44,6 +44,11 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
QDialog.__init__(self, parent)
Ui_QCreateCustomColumn.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.simple_error = partial(error_dialog, self, show=True,
show_copy_button=False)
self.connect(self.button_box, SIGNAL("accepted()"), self.accept)

View File

@ -21,6 +21,10 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
QDialog.__init__(self, parent)
Ui_EditAuthorsDialog.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.buttonBox.accepted.connect(self.accepted)

View File

@ -60,7 +60,7 @@
<item>
<widget class="QPushButton" name="stop_all_jobs_button">
<property name="text">
<string>Stop &amp;all jobs</string>
<string>Stop &amp;all non device jobs</string>
</property>
</widget>
</item>

View File

@ -27,10 +27,11 @@ from calibre.ebooks.metadata import string_to_authors, \
from calibre.ebooks.metadata.library_thing import cover_from_isbn
from calibre.ebooks.metadata.meta import get_metadata
from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
from calibre.gui2.dialogs.config.social import SocialMetadata
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre import strftime
class CoverFetcher(QThread):
@ -75,13 +76,20 @@ class CoverFetcher(QThread):
class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None):
def __init__(self, parent, ext, size, path=None, timestamp=None):
self.path = path
self.ext = ext
self.size = float(size)/(1024*1024)
text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
text, parent, QListWidgetItem.UserType)
if timestamp is not None:
ts = timestamp.astimezone(local_tz)
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
text = _('Last modified: %s')%t
self.setToolTip(text)
self.setStatusTip(text)
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
@ -151,14 +159,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
nfile = run_plugins_on_import(_file)
if nfile is not None:
_file = nfile
size = os.stat(_file).st_size
stat = os.stat(_file)
size = stat.st_size
ext = os.path.splitext(_file)[1].lower().replace('.', '')
timestamp = utcfromtimestamp(stat.st_mtime)
for row in range(self.formats.count()):
fmt = self.formats.item(row)
if fmt.ext.lower() == ext:
self.formats.takeItem(row)
break
Format(self.formats, ext, size, path=_file)
Format(self.formats, ext, size, path=_file, timestamp=timestamp)
self.formats_changed = True
added = True
if bad_perms:
@ -379,9 +389,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if not ext:
ext = ''
size = self.db.sizeof_format(row, ext)
timestamp = self.db.format_last_modified(self.id, ext)
if size is None:
continue
Format(self.formats, ext, size)
Format(self.formats, ext, size, timestamp=timestamp)
self.initialize_combos()

View File

@ -277,12 +277,6 @@
</property>
<item>
<widget class="EnComboBox" name="series">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>List of known series. You can add new series.</string>
</property>
@ -295,9 +289,6 @@
<property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item>

View File

@ -25,8 +25,8 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.current_search_name = None
self.searches = {}
self.searches_to_delete = []
for name in saved_searches.names():
self.searches[name] = saved_searches.lookup(name)
for name in saved_searches().names():
self.searches[name] = saved_searches().lookup(name)
self.populate_search_list()
if initial_search is not None and initial_search in self.searches:
@ -78,7 +78,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
for name in self.searches_to_delete:
saved_searches.delete(name)
saved_searches().delete(name)
for name in self.searches:
saved_searches.add(name, self.searches[name])
saved_searches().add(name, self.searches[name])
QDialog.accept(self)

View File

@ -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
@ -62,6 +62,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.search_done)
self.disconnect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
self.search.search_done)
self.search.search.disconnect()
self.recipe_model = None
def search_done(self, *args):
@ -203,6 +204,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 +229,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 +289,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()

View File

@ -7,7 +7,6 @@ from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
from calibre.utils.config import prefs
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.constants import islinux
@ -29,6 +28,11 @@ class TagCategories(QDialog, Ui_TagCategories):
Ui_TagCategories.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.db = db
self.applied_items = []
@ -63,7 +67,7 @@ class TagCategories(QDialog, Ui_TagCategories):
self.all_items.append(t)
self.all_items_dict[label+':'+n] = t
self.categories = dict.copy(prefs['user_categories'])
self.categories = dict.copy(db.prefs.get('user_categories', {}))
if self.categories is None:
self.categories = {}
for cat in self.categories:
@ -182,7 +186,7 @@ class TagCategories(QDialog, Ui_TagCategories):
def accept(self):
self.save_category()
prefs['user_categories'] = self.categories
self.db.prefs['user_categories'] = self.categories
QDialog.accept(self)
def save_category(self):

View File

@ -43,6 +43,10 @@ class TagListEditor(QDialog, Ui_TagListEditor):
QDialog.__init__(self, window)
Ui_TagListEditor.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.to_rename = {}
self.to_delete = []

View File

@ -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,163 +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'))
def read_toolbar_settings(self):
self.tool_bar.setIconSize(config['toolbar_icon_size'])
self.tool_bar.setToolButtonStyle(
Qt.ToolButtonTextUnderIcon if \
config['show_text_in_toolbar'] else \
Qt.ToolButtonIconOnly)
# }}}
class LibraryViewMixin(object): # {{{
def __init__(self, db):
@ -217,6 +59,7 @@ class LibraryViewMixin(object): # {{{
self.action_open_containing_folder,
self.action_show_book_details,
self.action_del,
self.action_conn_share,
add_to_library = None,
edit_device_collections=None,
similar_menu=similar_menu)
@ -225,21 +68,24 @@ class LibraryViewMixin(object): # {{{
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard=None))
self.memory_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard='carda'))
self.card_a_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard='cardb'))
self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)

View File

@ -5,137 +5,107 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
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 operator import attrgetter
from functools import partial
from calibre.constants import __appname__, filesystem_encoding
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__, isosx
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2 import NONE
from calibre.gui2 import config, open_url, gprefs
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
from calibre.utils.smtp import config as email_config
class ToolBar(QToolBar): # {{{
def __init__(self, parent=None):
QToolBar.__init__(self, parent)
self.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False)
self.setFloatable(False)
self.setOrientation(Qt.Horizontal)
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setIconSize(QSize(48, 48))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
class SaveMenu(QMenu): # {{{
def add_actions(self, *args):
self.left_space = QWidget(self)
self.left_space.setSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Minimum)
self.addWidget(self.left_space)
for action in args:
if action is None:
self.addSeparator()
else:
self.addAction(action)
self.right_space = QWidget(self)
self.right_space.setSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Minimum)
self.addWidget(self.right_space)
save_fmt = pyqtSignal(object)
def contextMenuEvent(self, *args):
pass
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)
# }}}
# Location View {{{
class LocationManager(QObject): # {{{
class LocationModel(QAbstractListModel): # {{{
locations_changed = pyqtSignal()
unmount_device = pyqtSignal()
location_selected = pyqtSignal(object)
devicesChanged = pyqtSignal()
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')]
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)):
@ -148,129 +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 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):
unmount_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(40, 40))
self.setMovement(self.Static)
self.setFlow(self.LeftToRight)
self.setGridSize(QSize(175, 90))
self.setViewMode(self.ListMode)
self.setWordWrap(True)
self.setObjectName("location_view")
self.setMaximumHeight(74)
def eject_clicked(self, *args):
self.unmount_device.emit()
def count_changed(self, new_count):
self.model().count = new_count
self.model().reset()
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
def enterEvent(self, event):
self.mouse_over = True
def leaveEvent(self, event):
self.mouse_over = False
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 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)
@property
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
# }}}
@ -344,36 +219,170 @@ class SearchBar(QWidget): # {{{
# }}}
class LocationBar(ToolBar): # {{{
class ToolBar(QToolBar): # {{{
def __init__(self, actions, donate, location_view, parent=None):
ToolBar.__init__(self, parent)
def __init__(self, actions, donate, location_manager, parent=None):
QToolBar.__init__(self, parent)
self.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False)
self.setFloatable(False)
self.setOrientation(Qt.Horizontal)
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
self.donate = donate
self.apply_settings()
for ac in actions:
self.addAction(ac)
self.addWidget(location_view)
self.w = QWidget()
self.w.setLayout(QVBoxLayout())
self.w.layout().addWidget(donate)
self.all_actions = actions
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)
donate.setAutoRaise(True)
donate.setCursor(Qt.PointingHandCursor)
self.addWidget(self.w)
self.setIconSize(QSize(50, 50))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.build_bar()
self.preferred_width = self.sizeHint().width()
def button_for_action(self, ac):
b = QToolButton(self)
b.setDefaultAction(ac)
for x in ('ToolTip', 'StatusTip', 'WhatsThis'):
getattr(b, 'set'+x)(b.text())
def apply_settings(self):
sz = gprefs.get('toolbar_icon_size', 'medium')
sz = {'small':24, 'medium':48, 'large':64}[sz]
self.setIconSize(QSize(sz, sz))
style = Qt.ToolButtonTextUnderIcon
if gprefs.get('toolbar_text', 'auto') == 'never':
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
self.donate.set_normal_icon_size(sz, sz)
def contextMenuEvent(self, *args):
pass
def build_bar(self):
showing_device = self.location_manager.has_device
order_field = 'device' if showing_device else 'normal'
o = attrgetter(order_field+'_order')
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()
def setup_tool_button(ac):
ch = self.widgetForAction(ac)
ch.setCursor(Qt.PointingHandCursor)
ch.setAutoRaise(True)
if ac.menu() is not None:
name = getattr(ac, 'action_name', None)
ch.setPopupMode(ch.InstantPopup if name == 'conn_share'
else 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.choose_action.setVisible(not showing_device)
def count_changed(self, new_count):
text = _('%d books')%new_count
a = self.choose_action
a.setText(text)
def resizeEvent(self, ev):
QToolBar.resizeEvent(self, ev)
style = Qt.ToolButtonTextUnderIcon
p = gprefs.get('toolbar_text', 'auto')
if p == 'never':
style = Qt.ToolButtonIconOnly
if p == 'auto' and self.preferred_width > self.width()+35:
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
def database_changed(self, db):
pass
# }}}
class Action(QAction):
pass
class ShareConnMenu(QMenu): # {{{
connect_to_folder = pyqtSignal()
connect_to_itunes = pyqtSignal()
config_email = pyqtSignal()
def __init__(self, parent=None):
QMenu.__init__(self, parent)
mitem = self.addAction(QIcon(I('devices/folder.svg')), _('Connect to folder'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
self.connect_to_folder_action = mitem
mitem = self.addAction(QIcon(I('devices/itunes.png')),
_('Connect to iTunes'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
self.connect_to_itunes_action = mitem
self.addSeparator()
self.email_actions = []
def build_email_entries(self, sync_menu):
from calibre.gui2.device import DeviceAction
for ac in self.email_actions:
self.removeAction(ac)
self.email_actions = []
opts = email_config().parse()
if opts.accounts:
self.email_to_menu = QMenu(_('Email to')+'...', self)
keys = sorted(opts.accounts.keys())
for account in keys:
formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats
action1 = DeviceAction(dest, False, False, I('mail.svg'),
_('Email to')+' '+account)
action2 = DeviceAction(dest, True, False, I('mail.svg'),
_('Email to')+' '+account+ _(' and delete from library'))
map(self.email_to_menu.addAction, (action1, action2))
if default:
map(self.addAction, (action1, action2))
map(self.email_actions.append, (action1, action2))
self.email_to_menu.addSeparator()
action1.a_s.connect(sync_menu.action_triggered)
action2.a_s.connect(sync_menu.action_triggered)
ac = self.addMenu(self.email_to_menu)
self.email_actions.append(ac)
else:
ac = self.addAction(_('Setup email based sharing of books'))
self.email_actions.append(ac)
ac.triggered.connect(self.setup_email)
def setup_email(self, *args):
self.config_email.emit()
return b
# }}}
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__)
@ -385,12 +394,44 @@ class MainWindowMixin(object):
self.centralwidget.setLayout(self._central_widget_layout)
self.resize(1012, 740)
self.donate_button = ThrobbingButton(self.centralwidget)
self.donate_button.set_normal_icon_size(64, 64)
self.location_manager = LocationManager(self)
# Actions {{{
self.init_scheduler(db)
all_actions = self.setup_actions()
def ac(name, text, icon, shortcut=None, tooltip=None):
action = QAction(QIcon(I(icon)), text, self)
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,
name, text, icon, shortcut=None, tooltip=None):
action = Action(QIcon(I(icon)), text, self)
action.normal_order = normal_order
action.device_order = device_order
action.separator_before = separator_before
action.action_name = name
text = tooltip if tooltip else text
action.setToolTip(text)
action.setStatusTip(text)
@ -400,56 +441,174 @@ class MainWindowMixin(object):
if shortcut:
action.setShortcut(shortcut)
setattr(self, 'action_'+name, action)
all_actions.append(action)
ac('add', _('Add books'), 'add_book.svg', _('A'))
ac('del', _('Remove books'), 'trash.svg', _('Del'))
ac('edit', _('Edit metadata'), 'edit_input.svg', _('E'))
ac('merge', _('Merge book records'), 'merge_books.svg', _('M'))
ac('sync', _('Send to device'), 'sync.svg')
ac('save', _('Save to disk'), 'save.svg', _('S'))
ac('news', _('Fetch news'), 'news.svg', _('F'))
ac('convert', _('Convert books'), 'convert.svg', _('C'))
ac('view', _('View'), 'view.svg', _('V'))
ac('open_containing_folder', _('Open containing folder'),
ac(0, 0, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
ac(1, 1, 0, 'edit', _('Edit metadata'), 'edit_input.svg', _('E'))
ac(2, 2, 3, 'convert', _('Convert books'), 'convert.svg', _('C'))
ac(3, 3, 0, 'view', _('View'), 'view.svg', _('V'))
ac(-1, 4, 0, 'sync', _('Send to device'), 'sync.svg')
ac(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png',
tooltip=_('Choose calibre library to work with'))
ac(6, 6, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
ac(7, 7, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
ac(8, 8, 0, 'conn_share', _('Connect/share'), 'connect_share.svg')
ac(9, 9, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
ac(10, 10, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
ac(11, 11, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
ac(-1, -1, 0, 'merge', _('Merge book records'), 'merge_books.svg', _('M'))
ac(-1, -1, 0, 'open_containing_folder', _('Open containing folder'),
'document_open.svg')
ac('show_book_details', _('Show book details'),
ac(-1, -1, 0, 'show_book_details', _('Show book details'),
'dialog_information.svg')
ac('books_by_same_author', _('Books by same author'),
ac(-1, -1, 0, 'books_by_same_author', _('Books by same author'),
'user_profile.svg')
ac('books_in_this_series', _('Books in this series'),
ac(-1, -1, 0, 'books_in_this_series', _('Books in this series'),
'books_in_series.svg')
ac('books_by_this_publisher', _('Books by this publisher'),
ac(-1, -1, 0, 'books_by_this_publisher', _('Books by this publisher'),
'publisher.png')
ac('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')
ac('preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
ac('help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
self.action_news.setMenu(self.scheduler.news_menu)
self.action_news.triggered.connect(
self.scheduler.show_dialog)
self.share_conn_menu = ShareConnMenu(self)
self.share_conn_menu.config_email.connect(partial(self.do_config,
initial_category='email'))
self.action_conn_share.setMenu(self.share_conn_menu)
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)
return all_actions
# }}}
self.tool_bar = ToolBar(self)
self.addToolBar(Qt.BottomToolBarArea, self.tool_bar)
self.tool_bar.add_actions(self.action_convert, self.action_view,
None, self.action_edit, None,
self.action_save, self.action_del,
None,
self.action_help, None, self.action_preferences)
self.location_view = LocationView(self.centralwidget)
self.search_bar = SearchBar(self)
self.location_bar = LocationBar([self.action_add, self.action_sync,
self.action_news], self.donate_button, self.location_view, self)
self.addToolBar(Qt.TopToolBarArea, self.location_bar)
l = self.centralwidget.layout()
l.addWidget(self.search_bar)
for ch in list(self.tool_bar.children()) + list(self.location_bar.children()):
if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor)
ch.setAutoRaise(True)
if ch is not self.donate_button:
ch.setPopupMode(ch.MenuButtonPopup)
def show_help(self, *args):
open_url(QUrl('http://calibre-ebook.com/user_manual'))

View File

@ -214,13 +214,17 @@ class BooksView(QTableView): # {{{
state['column_sizes'][name] = h.sectionSize(i)
return state
def write_state(self, state):
db = getattr(self.model(), 'db', None)
name = unicode(self.objectName())
if name and db is not None:
db.prefs.set(name + ' books view state', state)
def save_state(self):
# Only save if we have been initialized (set_database called)
if len(self.column_map) > 0 and self.was_restored:
state = self.get_state()
name = unicode(self.objectName())
if name:
gprefs.set(name + ' books view state', state)
self.write_state(state)
def cleanup_sort_history(self, sort_history):
history = []
@ -298,11 +302,27 @@ class BooksView(QTableView): # {{{
old_state['column_sizes'][name] += 12
return old_state
def restore_state(self):
def get_old_state(self):
ans = None
name = unicode(self.objectName())
old_state = None
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:
old_state = self.get_default_state()
@ -370,7 +390,7 @@ class BooksView(QTableView): # {{{
# Context Menu {{{
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, delete,
save, open_folder, book_details, delete, conn_share,
similar_menu=None, add_to_library=None,
edit_device_collections=None):
self.setContextMenuPolicy(Qt.DefaultContextMenu)
@ -381,6 +401,8 @@ class BooksView(QTableView): # {{{
self.context_menu.addAction(send_to_device)
if convert is not None:
self.context_menu.addAction(convert)
if conn_share is not None:
self.context_menu.addAction(conn_share)
self.context_menu.addAction(view)
self.context_menu.addAction(save)
if open_folder is not None:
@ -456,14 +478,20 @@ class BooksView(QTableView): # {{{
def set_current_row(self, row, select=True):
if row > -1:
h = self.horizontalHeader()
for i in range(h.count()):
if not h.isSectionHidden(i):
logical_indices = list(range(h.count()))
logical_indices = [x for x in logical_indices if not
h.isSectionHidden(x)]
pairs = [(x, h.visualIndex(x)) for x in logical_indices if
h.visualIndex(x) > -1]
if not pairs:
pairs = [(0, 0)]
pairs.sort(cmp=lambda x,y:cmp(x[1], y[1]))
i = pairs[0][0]
index = self.model().index(row, i)
self.setCurrentIndex(index)
if select:
sm = self.selectionModel()
sm.select(index, sm.ClearAndSelect|sm.Rows)
break
def close(self):
self._model.close()
@ -507,6 +535,19 @@ class DeviceBooksView(BooksView): # {{{
self.context_menu.popup(event.globalPos())
event.accept()
def get_old_state(self):
ans = None
name = unicode(self.objectName())
if name:
name += ' books view state'
ans = gprefs.get(name, None)
return ans
def write_state(self, state):
name = unicode(self.objectName())
if name:
gprefs.set(name + ' books view state', state)
def set_database(self, db):
self._model.set_database(db)
self.restore_state()

View File

@ -10,13 +10,12 @@ import re
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \
QAction, QKeySequence
QAction, QKeySequence, QTimer
from calibre.gui2 import config
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.dialogs.search import SearchDialog
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
class SearchLineEdit(QLineEdit):
@ -83,7 +82,9 @@ class SearchBox2(QComboBox):
self.help_state = False
self.as_you_type = True
self.prev_search = ''
self.timer = None
self.timer = QTimer()
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection)
self.setInsertPolicy(self.NoInsert)
self.setMaxCount(self.MAX_COUNT)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
@ -117,9 +118,6 @@ class SearchBox2(QComboBox):
self.search.emit('')
self._in_a_search = False
self.setEditText(self.help_text)
if self.timer is not None: # Turn off any timers that got started in setEditText
self.killTimer(self.timer)
self.timer = None
self.line_edit.home(False)
self.line_edit.setStyleSheet(
'QLineEdit { color: gray; background-color: %s; }' %
@ -148,17 +146,14 @@ class SearchBox2(QComboBox):
self._in_a_search = False
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.do_search()
self.timer = self.startTimer(self.__class__.INTERVAL)
self.timer.start(1500)
def mouse_released(self, event):
self.normalize_state()
if self.as_you_type:
self.timer = self.startTimer(self.__class__.INTERVAL)
self.timer.start(1500)
def timerEvent(self, event):
self.killTimer(event.timerId())
if event.timerId() == self.timer:
self.timer = None
def timer_event(self):
self.do_search()
def history_selected(self, text):
@ -213,9 +208,6 @@ class SearchBox2(QComboBox):
return
self.normalize_state()
self.setEditText(txt)
if self.timer is not None: # Turn off any timers that got started in setEditText
self.killTimer(self.timer)
self.timer = None
self.search.emit(txt)
self.line_edit.end(False)
self.initial_state = False
@ -259,8 +251,7 @@ class SavedSearchBox(QComboBox):
self.setMinimumContentsLength(10)
self.tool_tip_text = self.toolTip()
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
self.saved_searches = _saved_searches
def initialize(self, _search_box, colorize=False, help_text=_('Search')):
self.search_box = _search_box
self.help_text = help_text
self.colorize = colorize
@ -302,11 +293,11 @@ class SavedSearchBox(QComboBox):
self.normalize_state()
self.search_box.set_search_string(u'search:"%s"' % qname)
self.setEditText(qname)
self.setToolTip(self.saved_searches.lookup(qname))
self.setToolTip(saved_searches().lookup(qname))
def initialize_saved_search_names(self):
self.clear()
qnames = self.saved_searches.names()
qnames = saved_searches().names()
self.addItems(qnames)
self.setCurrentIndex(-1)
@ -319,10 +310,10 @@ class SavedSearchBox(QComboBox):
idx = self.currentIndex
if idx < 0:
return
ss = self.saved_searches.lookup(unicode(self.currentText()))
ss = saved_searches().lookup(unicode(self.currentText()))
if ss is None:
return
self.saved_searches.delete(unicode(self.currentText()))
saved_searches().delete(unicode(self.currentText()))
self.clear_to_help()
self.search_box.clear_to_help()
self.emit(SIGNAL('changed()'))
@ -332,8 +323,8 @@ class SavedSearchBox(QComboBox):
name = unicode(self.currentText())
if self.help_state or not name.strip():
name = unicode(self.search_box.text()).replace('"', '')
self.saved_searches.delete(name)
self.saved_searches.add(name, unicode(self.search_box.text()))
saved_searches().delete(name)
saved_searches().add(name, unicode(self.search_box.text()))
# now go through an initialization cycle to ensure that the combobox has
# the new search in it, that it is selected, and that the search box
# references the new search instead of the text in the search.
@ -348,7 +339,7 @@ class SavedSearchBox(QComboBox):
idx = self.currentIndex();
if idx < 0:
return
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText())))
class SearchBoxMixin(object):
@ -390,11 +381,12 @@ class SearchBoxMixin(object):
class SavedSearchBoxMixin(object):
def __init__(self):
def __init__(self, db):
self.db = db
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
self.saved_searches_changed()
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
self.saved_search.initialize(saved_searches, self.search, colorize=True,
self.saved_search.initialize(self.search, colorize=True,
help_text=_('Saved Searches'))
self.connect(self.save_search_button, SIGNAL('clicked()'),
self.saved_search.save_search_button_clicked)
@ -409,9 +401,12 @@ class SavedSearchBoxMixin(object):
b = getattr(self, x+'_search_button')
b.setStatusTip(b.toolTip())
def set_database(self, db):
self.db = db
self.saved_searches_changed()
def saved_searches_changed(self):
p = prefs['saved_searches'].keys()
p = saved_searches().names()
p.sort()
t = unicode(self.search_restriction.currentText())
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches

View File

@ -13,6 +13,7 @@ class SearchRestrictionMixin(object):
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
self.search_restriction.setStatusTip(self.search_restriction.toolTip())
self.search_count.setText(_("(all books)"))
'''
Adding and deleting books while restricted creates a complexity. When added,

View File

@ -17,7 +17,6 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
@ -224,7 +223,7 @@ class TagsView(QTreeView): # {{{
# Always show the user categories editor
self.context_menu.addSeparator()
if category in prefs['user_categories'].keys():
if category in self.db.prefs.get('user_categories', {}).keys():
self.context_menu.addAction(_('Manage User Categories'),
partial(self.context_menu_handler, action='manage_categories',
category=category))
@ -426,10 +425,10 @@ class TagsModel(QAbstractItemModel): # {{{
for k in tb_cats.keys():
if tb_cats[k]['kind'] in ['user', 'search']:
del tb_cats[k]
for user_cat in sorted(prefs['user_categories'].keys()):
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys()):
cat_name = user_cat+':' # add the ':' to avoid name collision
tb_cats.add_user_category(label=cat_name, name=user_cat)
if len(saved_searches.names()):
if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
# Now get the categories
@ -507,11 +506,11 @@ class TagsModel(QAbstractItemModel): # {{{
if key not in self.db.field_metadata:
return
if key == 'search':
if val in saved_searches.names():
if val in saved_searches().names():
error_dialog(self.tags_view, _('Duplicate search name'),
_('The saved search name %s is already used.')%val).exec_()
return False
saved_searches.rename(unicode(item.data(role).toString()), val)
saved_searches().rename(unicode(item.data(role).toString()), val)
self.tags_view.search_item_renamed.emit()
else:
if key == 'series':

View File

@ -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()
@ -167,8 +165,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.eject_action = self.system_tray_menu.addAction(
QIcon(I('eject.svg')), _('&Eject connected device'))
self.eject_action.setEnabled(False)
if not config['show_donate_button']:
self.donate_button.setVisible(False)
self.addAction(self.quit_action)
self.action_restart = QAction(_('&Restart'), self)
self.addAction(self.action_restart)
@ -194,23 +190,16 @@ 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)
SavedSearchBoxMixin.__init__(self, db)
SearchBoxMixin.__init__(self)
####################### Library view ########################
@ -220,8 +209,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows()
for t in (self.tool_bar, ):
self.library_view.model().count_changed_signal.connect \
(self.location_view.count_changed)
(t.count_changed)
if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -236,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 ##############################
@ -262,20 +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.location_view.setCurrentIndex(self.location_view.model().index(0))
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
AddAction.__init__(self)
@ -283,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)
@ -370,7 +351,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
def do_config(self, *args):
def do_config(self, checked=False, initial_category='general'):
if self.job_manager.has_jobs():
d = error_dialog(self, _('Cannot configure'),
_('Cannot configure while there are running jobs.'))
@ -382,7 +363,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
d.exec_()
return
d = ConfigDialog(self, self.library_view,
server=self.content_server)
server=self.content_server, initial_category=initial_category)
d.exec_()
self.content_server = d.server
@ -399,10 +380,7 @@ 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)
self.tool_bar.apply_settings()
def library_moved(self, newloc):
if newloc is None: return
@ -415,8 +393,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clear_message()
self.search.clear_to_help()
self.saved_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):

View File

@ -448,7 +448,7 @@ class DocumentView(QWebView):
self.unimplemented_actions = list(map(self.pageAction,
[d.DownloadImageToDisk, d.OpenLinkInNewWindow, d.DownloadLinkToDisk,
d.OpenImageInNewWindow, d.OpenLink]))
self.dictionary_action = QAction(QIcon(I('dictionary.png')),
self.dictionary_action = QAction(QIcon(I('dictionary.svg')),
_('&Lookup in dictionary'), self)
self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L)
self.dictionary_action.triggered.connect(self.lookup)

View File

@ -490,6 +490,7 @@ class EnComboBox(QComboBox):
QComboBox.__init__(self, *args)
self.setLineEdit(EnLineEdit(self))
self.setAutoCompletionCaseSensitivity(Qt.CaseSensitive)
self.setMinimumContentsLength(20)
def text(self):
return unicode(self.currentText())

View File

@ -615,7 +615,7 @@ class ResultCache(SearchQueryParser):
q = self.search_restriction
else:
q = query
if not ignore_search_restriction:
if not ignore_search_restriction and self.search_restriction:
q = u'%s (%s)' % (self.search_restriction, query)
if not q:
if return_matches:

View File

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2010, Greg Riker <griker at hotmail.com>'
import datetime, htmlentitydefs, os, re, shutil
import datetime, htmlentitydefs, os, re, shutil, codecs
from collections import namedtuple
from copy import deepcopy
@ -9,6 +11,7 @@ from copy import deepcopy
from xml.sax.saxutils import escape
from calibre import filesystem_encoding, prints, prepare_string_for_xml, strftime
from calibre.constants import preferred_encoding
from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
@ -21,6 +24,10 @@ FIELDS = ['all', 'author_sort', 'authors', 'comments',
'series_index', 'series', 'size', 'tags', 'timestamp', 'title',
'uuid']
#Allowed fields for template
TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate',
'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid' ]
class CSV_XML(CatalogPlugin):
'CSV/XML catalog generator'
@ -89,17 +96,20 @@ class CSV_XML(CatalogPlugin):
fields = self.get_output_fields(opts)
if self.fmt == 'csv':
outfile = open(path_to_output, 'w')
outfile = codecs.open(path_to_output, 'w', 'utf8')
# Output the field headers
outfile.write(u'%s\n' % u','.join(fields))
# Output the entry fields
for entry in data:
outstr = ''
for (x, field) in enumerate(fields):
outstr = []
for field in fields:
item = entry[field]
if field == 'formats':
if item is None:
outstr.append('""')
continue
elif field == 'formats':
fmt_list = []
for format in item:
fmt_list.append(format.rpartition('.')[2].lower())
@ -111,18 +121,13 @@ class CSV_XML(CatalogPlugin):
item = u'%s' % re.sub(r'[\D]', '', item)
elif field in ['pubdate', 'timestamp']:
item = isoformat(item)
elif field == 'comments':
item = item.replace(u'\r\n',u' ')
item = item.replace(u'\n',u' ')
if x < len(fields) - 1:
if item is not None:
outstr += u'"%s",' % unicode(item).replace('"','""')
else:
outstr += '"",'
else:
if item is not None:
outstr += u'"%s"\n' % unicode(item).replace('"','""')
else:
outstr += '""\n'
outfile.write(outstr.encode('utf-8'))
outstr.append(u'"%s"' % unicode(item).replace('"','""'))
outfile.write(u','.join(outstr) + u'\n')
outfile.close()
elif self.fmt == 'xml':
@ -181,6 +186,329 @@ class CSV_XML(CatalogPlugin):
f.write(etree.tostring(root, encoding='utf-8',
xml_declaration=True, pretty_print=True))
class BIBTEX(CatalogPlugin):
'BIBTEX catalog generator'
Option = namedtuple('Option', 'option, default, dest, action, help')
name = 'Catalog_BIBTEX'
description = 'BIBTEX catalog generator'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Sengian'
version = (1, 0, 0)
file_types = set(['bib'])
cli_options = [
Option('--fields',
default = 'all',
dest = 'fields',
action = None,
help = _('The fields to output when cataloging books in the '
'database. Should be a comma-separated list of fields.\n'
'Available fields: %s.\n'
"Default: '%%default'\n"
"Applies to: BIBTEX output format")%', '.join(FIELDS)),
Option('--sort-by',
default = 'id',
dest = 'sort_by',
action = None,
help = _('Output field to sort on.\n'
'Available fields: author_sort, id, rating, size, timestamp, title.\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--create-citation',
default = 'True',
dest = 'impcit',
action = None,
help = _('Create a citation for BibTeX entries.\n'
'Boolean value: True, False\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--citation-template',
default = '{authors}{id}',
dest = 'bib_cit',
action = None,
help = _('The template for citation creation from database fields.\n'
' Should be a template with {} enclosed fields.\n'
'Available fields: %s.\n'
"Default: '%%default'\n"
"Applies to: BIBTEX output format")%', '.join(TEMPLATE_ALLOWED_FIELDS)),
Option('--choose-encoding',
default = 'utf8',
dest = 'bibfile_enc',
action = None,
help = _('BibTeX file encoding output.\n'
'Available types: utf8, cp1252, ascii.\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--choose-encoding-configuration',
default = 'strict',
dest = 'bibfile_enctag',
action = None,
help = _('BibTeX file encoding flag.\n'
'Available types: strict, replace, ignore, backslashreplace.\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--entry-type',
default = 'book',
dest = 'bib_entry',
action = None,
help = _('Entry type for BibTeX catalog.\n'
'Available types: book, misc, mixed.\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format"))]
def run(self, path_to_output, opts, db, notification=DummyReporter()):
from types import StringType, UnicodeType
from calibre.library.save_to_disk import preprocess_template
#Bibtex functions
from calibre.utils.bibtex import bibtex_author_format, utf8ToBibtex, ValidateCitationKey
def create_bibtex_entry(entry, fields, mode, template_citation,
asccii_bibtex = True, citation_bibtex = True):
#Bibtex doesn't like UTF-8 but keep unicode until writing
#Define starting chain or if book valid strict and not book return a Fail string
bibtex_entry = []
if mode != "misc" and check_entry_book_valid(entry) :
bibtex_entry.append(u'@book{')
elif mode != "book" :
bibtex_entry.append(u'@misc{')
else :
#case strict book
return ''
if citation_bibtex :
# Citation tag
bibtex_entry.append(make_bibtex_citation(entry, template_citation, asccii_bibtex))
bibtex_entry = [u' '.join(bibtex_entry)]
for field in fields:
item = entry[field]
#check if the field should be included (none or empty)
if item is None:
continue
try:
if len(item) == 0 :
continue
except TypeError:
pass
if field == 'authors' :
bibtex_entry.append(u'author = "%s"' % bibtex_author_format(item))
elif field in ['title', 'publisher', 'cover', 'uuid',
'author_sort', 'series'] :
bibtex_entry.append(u'%s = "%s"' % (field, utf8ToBibtex(item, asccii_bibtex)))
elif field == 'id' :
bibtex_entry.append(u'calibreid = "%s"' % int(item))
elif field == 'rating' :
bibtex_entry.append(u'rating = "%s"' % int(item))
elif field == 'size' :
bibtex_entry.append(u'%s = "%s octets"' % (field, int(item)))
elif field == 'tags' :
#A list to flatten
bibtex_entry.append(u'tags = "%s"' % utf8ToBibtex(u', '.join(item), asccii_bibtex))
elif field == 'comments' :
#\n removal
item = item.replace(u'\r\n',u' ')
item = item.replace(u'\n',u' ')
bibtex_entry.append(u'note = "%s"' % utf8ToBibtex(item, asccii_bibtex))
elif field == 'isbn' :
# Could be 9, 10 or 13 digits
bibtex_entry.append(u'isbn = "%s"' % re.sub(u'[\D]', u'', item))
elif field == 'formats' :
item = u', '.join([format.rpartition('.')[2].lower() for format in item])
bibtex_entry.append(u'formats = "%s"' % item)
elif field == 'series_index' :
bibtex_entry.append(u'volume = "%s"' % int(item))
elif field == 'timestamp' :
bibtex_entry.append(u'timestamp = "%s"' % isoformat(item).partition('T')[0])
elif field == 'pubdate' :
bibtex_entry.append(u'year = "%s"' % item.year)
bibtex_entry.append(u'month = "%s"' % utf8ToBibtex(strftime("%b", item),
asccii_bibtex))
bibtex_entry = u',\n '.join(bibtex_entry)
bibtex_entry += u' }\n\n'
return bibtex_entry
def check_entry_book_valid(entry):
#Check that the required fields are ok for a book entry
for field in ['title', 'authors', 'publisher'] :
if entry[field] is None or len(entry[field]) == 0 :
return False
if entry['pubdate'] is None :
return False
else :
return True
def make_bibtex_citation(entry, template_citation, asccii_bibtex):
#define a function to replace the template entry by its value
def tpl_replace(objtplname) :
tpl_field = re.sub(u'[\{\}]', u'', objtplname.group())
if tpl_field in TEMPLATE_ALLOWED_FIELDS :
if tpl_field in ['pubdate', 'timestamp'] :
tpl_field = isoformat(entry[tpl_field]).partition('T')[0]
elif tpl_field in ['tags', 'authors'] :
tpl_field =entry[tpl_field][0]
elif tpl_field in ['id', 'series_index'] :
tpl_field = str(entry[tpl_field])
else :
tpl_field = entry[tpl_field]
return tpl_field
else:
return u''
if len(template_citation) >0 :
tpl_citation = utf8ToBibtex(ValidateCitationKey(re.sub(u'\{[^{}]*\}',
tpl_replace, template_citation)), asccii_bibtex)
if len(tpl_citation) >0 :
return tpl_citation
if len(entry["isbn"]) > 0 :
template_citation = u'%s' % re.sub(u'[\D]',u'', entry["isbn"])
else :
template_citation = u'%s' % str(entry["id"])
if asccii_bibtex :
return ValidateCitationKey(template_citation.encode('ascii', 'replace'))
else :
return ValidateCitationKey(template_citation)
self.fmt = path_to_output.rpartition('.')[2]
self.notification = notification
# Combobox options
bibfile_enc = ['utf8', 'cp1252', 'ascii']
bibfile_enctag = ['strict', 'replace', 'ignore', 'backslashreplace']
bib_entry = ['mixed', 'misc', 'book']
# Needed beacause CLI return str vs int by widget
try:
bibfile_enc = bibfile_enc[opts.bibfile_enc]
bibfile_enctag = bibfile_enctag[opts.bibfile_enctag]
bib_entry = bib_entry[opts.bib_entry]
except:
if opts.bibfile_enc in bibfile_enc :
bibfile_enc = opts.bibfile_enc
else :
log(" WARNING: incorrect --choose-encoding flag, revert to default")
bibfile_enc = bibfile_enc[0]
if opts.bibfile_enctag in bibfile_enctag :
bibfile_enctag = opts.bibfile_enctag
else :
log(" WARNING: incorrect --choose-encoding-configuration flag, revert to default")
bibfile_enctag = bibfile_enctag[0]
if opts.bib_entry in bib_entry :
bib_entry = opts.bib_entry
else :
log(" WARNING: incorrect --entry-type flag, revert to default")
bib_entry = bib_entry[0]
if opts.verbose:
opts_dict = vars(opts)
log("%s(): Generating %s" % (self.name,self.fmt))
if opts_dict['search_text']:
log(" --search='%s'" % opts_dict['search_text'])
if opts_dict['ids']:
log(" Book count: %d" % len(opts_dict['ids']))
if opts_dict['search_text']:
log(" (--search ignored when a subset of the database is specified)")
if opts_dict['fields']:
if opts_dict['fields'] == 'all':
log(" Fields: %s" % ', '.join(FIELDS[1:]))
else:
log(" Fields: %s" % opts_dict['fields'])
log(" Output file will be encoded in %s with %s flag" % (bibfile_enc, bibfile_enctag))
log(" BibTeX entry type is %s with a citation like '%s' flag" % (bib_entry, opts_dict['bib_cit']))
# If a list of ids are provided, don't use search_text
if opts.ids:
opts.search_text = None
data = self.search_sort_db(db, opts)
if not len(data):
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
# Get the requested output fields as a list
fields = self.get_output_fields(opts)
if not len(data):
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
#Entries writing after Bibtex formating (or not)
if bibfile_enc != 'ascii' :
asccii_bibtex = False
else :
asccii_bibtex = True
#Check and go to default in case of bad CLI
if isinstance(opts.impcit, (StringType, UnicodeType)) :
if opts.impcit == 'False' :
citation_bibtex= False
elif opts.impcit == 'True' :
citation_bibtex= True
else :
log(" WARNING: incorrect --create-citation, revert to default")
citation_bibtex= True
else :
citation_bibtex= opts.impcit
template_citation = preprocess_template(opts.bib_cit)
#Open output and write entries
outfile = codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag)
#File header
nb_entries = len(data)
#check in book strict if all is ok else throw a warning into log
if bib_entry == 'book' :
nb_books = len(filter(check_entry_book_valid, data))
if nb_books < nb_entries :
log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries))
nb_entries = nb_books
outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries))
outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n'
% (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding)))
for entry in data:
outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation,
asccii_bibtex, citation_bibtex))
outfile.close()
class EPUB_MOBI(CatalogPlugin):
'ePub catalog generator'

View File

@ -12,6 +12,7 @@ from math import floor
from calibre import prints
from calibre.constants import preferred_encoding
from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import parse_date
class CustomColumns(object):
@ -30,6 +31,10 @@ class CustomColumns(object):
def __init__(self):
# Verify that CUSTOM_DATA_TYPES is a (possibly improper) subset of
# VALID_DATA_TYPES
if len(self.CUSTOM_DATA_TYPES - FieldMetadata.VALID_DATA_TYPES) > 0:
raise ValueError('Unknown custom column type in set')
# Delete marked custom columns
for record in self.conn.get(
'SELECT id FROM custom_columns WHERE mark_for_delete=1'):

View File

@ -19,6 +19,7 @@ from calibre.library.schema_upgrades import SchemaUpgrade
from calibre.library.caches import ResultCache
from calibre.library.custom_columns import CustomColumns
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, \
MetaInformation
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
@ -29,7 +30,7 @@ from calibre.customize.ui import run_plugins_on_import
from calibre.utils.filenames import ascii_filename
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.config import prefs, tweaks
from calibre.utils.search_query_parser import saved_searches
from calibre.utils.search_query_parser import saved_searches, set_saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick_draw import save_cover_data_to
@ -116,6 +117,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):
@ -136,6 +141,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.initialize_dynamic()
def initialize_dynamic(self):
self.prefs = DBPrefs(self)
# Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default):
oldval = prefs[key]
if oldval != default:
self.prefs[key] = oldval
prefs[key] = default
if key not in self.prefs:
self.prefs[key] = default
migrate_preference('user_categories', {})
migrate_preference('saved_searches', {})
set_saved_searches(self, 'saved_searches')
self.conn.executescript('''
DROP TRIGGER IF EXISTS author_insert_trg;
CREATE TEMP TRIGGER author_insert_trg
@ -264,10 +284,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for k in tb_cats.keys():
if tb_cats[k]['kind'] in ['user', 'search']:
del tb_cats[k]
for user_cat in sorted(prefs['user_categories'].keys()):
for user_cat in sorted(self.prefs.get('user_categories', {}).keys()):
cat_name = user_cat+':' # add the ':' to avoid name collision
tb_cats.add_user_category(label=cat_name, name=user_cat)
if len(saved_searches.names()):
if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
self.book_on_device_func = None
@ -307,6 +327,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
''' Return last modified time as a UTC datetime object'''
return utcfromtimestamp(os.stat(self.dbpath).st_mtime)
def check_if_modified(self):
if self.last_modified() > self.last_update_check:
self.refresh()
@ -577,6 +598,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def has_format(self, index, format, index_is_id=False):
return self.format_abspath(index, format, index_is_id) is not None
def format_last_modified(self, id_, fmt):
path = self.format_abspath(id_, fmt, index_is_id=True)
if path is not None:
return utcfromtimestamp(os.stat(path).st_mtime)
def format_abspath(self, index, format, index_is_id=False):
'Return absolute path to the ebook file of format `format`'
id = index if index_is_id else self.id(index)
@ -839,7 +865,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
categories['formats'].sort(key = lambda x:x.name)
#### Now do the user-defined categories. ####
user_categories = prefs['user_categories']
user_categories = self.prefs['user_categories']
# We want to use same node in the user category as in the source
# category. To do that, we need to find the original Tag node. There is
@ -876,8 +902,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
icon = None
if icon_map and 'search' in icon_map:
icon = icon_map['search']
for srch in saved_searches.names():
items.append(Tag(srch, tooltip=saved_searches.lookup(srch), icon=icon))
for srch in saved_searches().names():
items.append(Tag(srch, tooltip=saved_searches().lookup(srch), icon=icon))
if len(items):
if icon_map is not None:
icon_map['search'] = icon_map['search']

View File

@ -30,8 +30,8 @@ class FieldMetadata(dict):
label: the actual column label. No prefixing.
datatype: the type of the information in the field. Valid values are float,
int, rating, bool, comments, datetime, text.
datatype: the type of information in the field. Valid values are listed in
VALID_DATA_TYPES below.
is_multiple: valid for the text datatype. If None, the field is to be
treated as a single term. If not None, it contains a string, and the field
is assumed to contain a list of terms separated by that string
@ -65,6 +65,10 @@ class FieldMetadata(dict):
rec_index: the index of the field in the db metadata record.
'''
VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
'int', 'float', 'bool', 'series'])
_field_metadata = [
('authors', {'table':'authors',
'column':'name',
@ -296,6 +300,8 @@ class FieldMetadata(dict):
self._search_term_map = {}
self.custom_label_to_key_map = {}
for k,v in self._field_metadata:
if v['kind'] == 'field' and v['datatype'] not in self.VALID_DATA_TYPES:
raise ValueError('Unknown datatype %s for field %s'%(v['datatype'], k))
self._tb_cats[k] = v
self._tb_cats[k]['label'] = k
self._tb_cats[k]['display'] = {}
@ -377,6 +383,8 @@ class FieldMetadata(dict):
key = self.custom_field_prefix + label
if key in self._tb_cats:
raise ValueError('Duplicate custom field [%s]'%(label))
if datatype not in self.VALID_DATA_TYPES:
raise ValueError('Unknown datatype %s for field %s'%(datatype, key))
self._tb_cats[key] = {'table':table, 'column':column,
'datatype':datatype, 'is_multiple':is_multiple,
'kind':'field', 'name':name,

View 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)

View File

@ -387,3 +387,13 @@ class SchemaUpgrade(object):
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)

View File

@ -349,7 +349,7 @@ table of contents, check the :guilabel:`Do not add detected chapters` option.
If less than the :guilabel:`Chapter threshold` number of chapters were detected, |app| will then add any hyperlinks
it finds in the input document to the Table of Contents. This often works well many input documents include a
hyperlinked Table of Contents right at the start. The :guilabel:`Number fo links` option can be used to control
hyperlinked Table of Contents right at the start. The :guilabel:`Number of links` option can be used to control
this behavior. If set to zero, no links are added. If set to a number greater than zero, at most that number of links
is added.

View File

@ -9,7 +9,8 @@ Customizing |app|
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
first, how to use environment variables and *tweaks* to customize |app|'s behavior and then how to
first, how to use environment variables and *tweaks* to customize |app|'s behavior, and then how to
specify your own static resources like icons and templates to override the defaults and finally how to
use *plugins* to add funtionality to |app|.
.. contents::
@ -35,6 +36,21 @@ The default tweaks.py file is reproduced below
.. literalinclude:: ../../../resources/default_tweaks.py
Overriding icons, templates, etcetera
----------------------------------------
|app| allows you to override the static resources, like icons, templates, javascript, etc. with customized versions that you like.
All static resources are stored in the resources sub-folder of the calibre install location. On Windows, this is usually
:file:`C:\Program Files\Calibre2\resources`. On OS X, :file:`/Applications/calibre.app/Contents/Resources/resources/`. On linux, if you are using the binary installer
from the calibre website it will be :file:`/opt/calibre/resources`. These paths can change depending on where you choose to install |app|.
You should not change the files in this resources folder, as your changes will get overwritten the next time you update |app|. Instead, go to
:guilabel:`Preferences->Advanced` and click :guilabel:`Open calibre configuration directory`. In this configuration directory, create a sub-folder called resources and place the files you want to override in it. Place the files in the appropriate sub folders, for example place images in :file:`resources/images`, etc.
|app| will automatically use your custom file in preference to the builtin one the next time it is started.
For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is
:file:`resources/images/trash.svg`. Assuming you have an alternate icon in svg format called :file:`mytrash.svg` you would save it in the configuration directory as :file:`resources/images/trash.svg`. All the icons used by the calibre user interface are in :file:`resources/images` and its sub-folders.
A Hello World plugin
------------------------

View File

@ -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?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -265,7 +281,7 @@ Why doesn't |app| have a column for foo?
How do I move my |app| library from one computer to another?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking Preferences. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder.
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking the calibre icon in the toolbar. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder.
Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also go to Preferences->Advanced and click the Check database integrity button. It will warn you about missing files, if any, which you should then transfer by hand.

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

Some files were not shown because too many files have changed in this diff Show More