Sync to trunk.
124
Changelog.yaml
@ -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
|
||||
|
||||
|
5123
resources/images/connect_share.svg
Normal file
After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 5.3 KiB |
1009
resources/images/dictionary.svg
Normal file
After Width: | Height: | Size: 48 KiB |
@ -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
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 21 KiB |
38
resources/recipes/alternet.recipe
Normal file
@ -0,0 +1,38 @@
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Alternet(BasicNewsRecipe):
|
||||
title = u'Alternet'
|
||||
__author__= 'rty'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
publisher = 'alternet.org'
|
||||
category = 'News, Magazine'
|
||||
description = 'News magazine and online community'
|
||||
feeds = [
|
||||
(u'Front Page', u'http://feeds.feedblitz.com/alternet'),
|
||||
(u'Breaking News', u'http://feeds.feedblitz.com/alternet_breaking_news'),
|
||||
(u'Top Ten Campaigns', u'http://feeds.feedblitz.com/alternet_top_10_campaigns'),
|
||||
(u'Special Coverage Areas', u'http://feeds.feedblitz.com/alternet_coverage')
|
||||
]
|
||||
remove_attributes = ['width', 'align','cellspacing']
|
||||
remove_javascript = True
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
language = 'en'
|
||||
encoding = 'UTF-8'
|
||||
temp_files = []
|
||||
articles_are_obfuscated = True
|
||||
|
||||
def get_article_url(self, article):
|
||||
return article.get('link', None)
|
||||
|
||||
def get_obfuscated_article(self, url):
|
||||
br = self.get_browser()
|
||||
br.open(url)
|
||||
response = br.follow_link(url_regex = r'/printversion/[0-9]+', nr = 0)
|
||||
html = response.read()
|
||||
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
|
||||
self.temp_files[-1].write(html)
|
||||
self.temp_files[-1].close()
|
||||
return self.temp_files[-1].name
|
@ -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 = [
|
||||
|
@ -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 = [
|
||||
|
112
resources/recipes/daum_net.recipe
Normal 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: '< '),
|
||||
(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
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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'),
|
||||
|
@ -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' })
|
||||
|
38
resources/recipes/orlando_sentinel.recipe
Normal 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'}),
|
||||
]
|
44
resources/recipes/technology_review.recipe
Normal file
@ -0,0 +1,44 @@
|
||||
import string
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class TechnologyReview(BasicNewsRecipe):
|
||||
title = u'Technology Review'
|
||||
__author__ = 'rty'
|
||||
description = 'MIT Technology Magazine'
|
||||
publisher = 'Technology Review Inc.'
|
||||
category = 'Technology, Innovation, R&D'
|
||||
oldest_article = 14
|
||||
max_articles_per_feed = 100
|
||||
No_stylesheets = True
|
||||
extra_css = """
|
||||
.ArticleBody {font: normal; text-align: justify}
|
||||
.headline {font: bold x-large}
|
||||
.subheadline {font: italic large}
|
||||
"""
|
||||
feeds = [
|
||||
(u'Computing', u'http://feeds.technologyreview.com/technology_review_Computing'),
|
||||
(u'Web', u'http://feeds.technologyreview.com/technology_review_Web'),
|
||||
(u'Communications', u'http://feeds.technologyreview.com/technology_review_Communications'),
|
||||
(u'Energy', u'http://feeds.technologyreview.com/technology_review_Energy'),
|
||||
(u'Materials', u'http://feeds.technologyreview.com/technology_review_Materials'),
|
||||
(u'Biomedicine', u'http://feeds.technologyreview.com/technology_review_Biotech'),
|
||||
(u'Business', u'http://feeds.technologyreview.com/technology_review_Biztech')
|
||||
]
|
||||
remove_attributes = ['width', 'align','cellspacing']
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'id':['CloseLink','footerAdDiv','copyright']}),
|
||||
]
|
||||
remove_tags_after = [dict(name='div', attrs={'id':'copyright'})]
|
||||
|
||||
def get_article_url(self, article):
|
||||
return article.get('guid', article.get('id', None))
|
||||
|
||||
|
||||
def print_version(self, url):
|
||||
baseurl='http://www.technologyreview.com/printer_friendly_article.aspx?id='
|
||||
split1 = string.split(url,"/")
|
||||
xxx=split1 [4]
|
||||
split2= string.split(xxx,"/")
|
||||
s = baseurl + split2[0]
|
||||
return s
|
34
resources/recipes/waco_tribune.recipe
Normal 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'}),
|
||||
]
|
@ -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">
|
||||
|
@ -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 = {
|
||||
'>' : '>',
|
||||
'&' : '&'})
|
||||
|
||||
def replace_entities(raw):
|
||||
return _ent_pat.sub(entity_to_unicode, raw)
|
||||
|
||||
def prepare_string_for_xml(raw, attribute=False):
|
||||
raw = _ent_pat.sub(entity_to_unicode, raw)
|
||||
raw = raw.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
@ -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):
|
||||
|
@ -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')
|
||||
# }}}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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]},
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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']
|
||||
|
@ -60,6 +60,9 @@ class FB2Input(InputFormatPlugin):
|
||||
|
||||
transform = etree.XSLT(styledoc)
|
||||
result = transform(doc)
|
||||
for img in result.xpath('//img[@src]'):
|
||||
src = img.get('src')
|
||||
img.set('src', self.binary_map.get(src, src))
|
||||
open('index.xhtml', 'wb').write(transform.tostring(result))
|
||||
stream.seek(0)
|
||||
mi = get_metadata(stream, 'fb2')
|
||||
@ -83,9 +86,15 @@ class FB2Input(InputFormatPlugin):
|
||||
return os.path.join(os.getcwd(), 'metadata.opf')
|
||||
|
||||
def extract_embedded_content(self, doc):
|
||||
self.binary_map = {}
|
||||
for elem in doc.xpath('./*'):
|
||||
if 'binary' in elem.tag and elem.attrib.has_key('id'):
|
||||
ct = elem.get('content-type', '')
|
||||
fname = elem.attrib['id']
|
||||
ext = ct.rpartition('/')[-1].lower()
|
||||
if ext in ('png', 'jpeg', 'jpg'):
|
||||
fname += '.' + ext
|
||||
self.binary_map[elem.get('id')] = fname
|
||||
data = b64decode(elem.text.strip())
|
||||
open(fname, 'wb').write(data)
|
||||
|
||||
|
@ -368,7 +368,15 @@ class LRFInput(InputFormatPlugin):
|
||||
if options.verbose > 2:
|
||||
open('lrs.xml', 'wb').write(xml.encode('utf-8'))
|
||||
parser = etree.XMLParser(no_network=True, huge_tree=True)
|
||||
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')
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -14,7 +14,8 @@ except ImportError:
|
||||
|
||||
from lxml import html, etree
|
||||
|
||||
from calibre import xml_entity_to_unicode, CurrentDir, entity_to_unicode
|
||||
from calibre import xml_entity_to_unicode, CurrentDir, entity_to_unicode, \
|
||||
replace_entities
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from calibre.utils.date import parse_date
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
@ -70,7 +71,7 @@ class EXTHHeader(object):
|
||||
#else:
|
||||
# print 'unknown record', id, repr(content)
|
||||
if title:
|
||||
self.mi.title = title
|
||||
self.mi.title = replace_entities(title)
|
||||
|
||||
def process_metadata(self, id, content, codec):
|
||||
if id == 100:
|
||||
|
@ -475,7 +475,8 @@ class Style(object):
|
||||
value = float(m.group(1))
|
||||
unit = m.group(2)
|
||||
if unit == '%':
|
||||
base = base or self.width
|
||||
if base is None:
|
||||
base = self.width
|
||||
result = (value / 100.0) * base
|
||||
elif unit == 'px':
|
||||
result = value * 72.0 / self._profile.dpi
|
||||
|
@ -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'
|
||||
|
@ -192,12 +192,18 @@ class RTFInput(InputFormatPlugin):
|
||||
from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException
|
||||
self.log = log
|
||||
self.log('Converting RTF to XML...')
|
||||
#Name of the preprocesssed RTF file
|
||||
fname = self.preprocess(stream.name)
|
||||
try:
|
||||
xml = self.generate_xml(fname)
|
||||
except RtfInvalidCodeException, e:
|
||||
raise ValueError(_('This RTF file has a feature calibre does not '
|
||||
'support. Convert it to HTML first and then try it.\n%s')%e)
|
||||
|
||||
'''dataxml = open('dataxml.xml', 'w')
|
||||
dataxml.write(xml)
|
||||
dataxml.close'''
|
||||
|
||||
d = glob.glob(os.path.join('*_rtf_pict_dir', 'picts.rtf'))
|
||||
if d:
|
||||
imap = {}
|
||||
@ -205,6 +211,7 @@ class RTFInput(InputFormatPlugin):
|
||||
imap = self.extract_images(d[0])
|
||||
except:
|
||||
self.log.exception('Failed to extract images...')
|
||||
|
||||
self.log('Parsing XML...')
|
||||
parser = etree.XMLParser(recover=True, no_network=True)
|
||||
doc = etree.fromstring(xml, parser=parser)
|
||||
@ -214,10 +221,10 @@ class RTFInput(InputFormatPlugin):
|
||||
name = imap.get(num, None)
|
||||
if name is not None:
|
||||
pict.set('num', name)
|
||||
|
||||
self.log('Converting XML to HTML...')
|
||||
inline_class = InlineClass(self.log)
|
||||
styledoc = etree.fromstring(P('templates/rtf.xsl', data=True))
|
||||
|
||||
extensions = { ('calibre', 'inline-class') : inline_class }
|
||||
transform = etree.XSLT(styledoc, extensions=extensions)
|
||||
result = transform(doc)
|
||||
|
@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import os, sys
|
||||
from threading import RLock
|
||||
|
||||
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
||||
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
|
||||
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
|
||||
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||
@ -33,10 +33,6 @@ def _config():
|
||||
help=_('Send file to storage card instead of main memory by default'))
|
||||
c.add_opt('confirm_delete', default=False,
|
||||
help=_('Confirm before deleting'))
|
||||
c.add_opt('toolbar_icon_size', default=QSize(48, 48),
|
||||
help=_('Toolbar icon size')) # value QVariant.toSize
|
||||
c.add_opt('show_text_in_toolbar', default=True,
|
||||
help=_('Show button labels in the toolbar'))
|
||||
c.add_opt('main_window_geometry', default=None,
|
||||
help=_('Main window geometry')) # value QVariant.toByteArray
|
||||
c.add_opt('new_version_notification', default=True,
|
||||
|
@ -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):
|
||||
|
84
src/calibre/gui2/catalog/catalog_bibtex.py
Normal 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
|
173
src/calibre/gui2/catalog/catalog_bibtex.ui
Normal 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>
|
@ -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 &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 &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">
|
||||
|
@ -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())
|
||||
|
||||
|
@ -58,7 +58,7 @@
|
||||
<item row="9" column="0" colspan="2">
|
||||
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<item row="10" column="0" colspan="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
@ -43,12 +43,6 @@
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimumContentsLength">
|
||||
<number>30</number>
|
||||
</property>
|
||||
|
@ -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:
|
||||
|
84
src/calibre/gui2/dialogs/choose_library.py
Normal 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)
|
174
src/calibre/gui2/dialogs/choose_library.ui
Normal file
@ -0,0 +1,174 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>602</width>
|
||||
<height>245</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Choose your calibre library</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="4">
|
||||
<widget class="QLabel" name="old_location">
|
||||
<property name="text">
|
||||
<string>Your calibre library is currently located at {0}</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>New &Location:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>location</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="4">
|
||||
<widget class="QRadioButton" name="existing_library">
|
||||
<property name="text">
|
||||
<string>Use &existing library at the new location</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="3">
|
||||
<widget class="QRadioButton" name="empty_library">
|
||||
<property name="text">
|
||||
<string>&Create an empty library at the new location</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="3">
|
||||
<widget class="QRadioButton" name="move_library">
|
||||
<property name="text">
|
||||
<string>&Move current library to new location</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="2">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QToolButton" name="browse_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="HistoryLineEdit" name="location"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>HistoryLineEdit</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>calibre/gui2/widgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -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)
|
||||
|
@ -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_()
|
||||
|
@ -113,50 +113,6 @@
|
||||
</property>
|
||||
<widget class="QWidget" name="page_3">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>70</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Location of ebooks (The ebooks are stored in folders sorted by author and metadata is stored in the file metadata.db)</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>location</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="_3">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="location"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="browse_button">
|
||||
<property name="toolTip">
|
||||
<string>Browse for the new database location</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../../resources/images.qrc">
|
||||
<normaloff>:/images/mimetypes/dir.svg</normaloff>:/images/mimetypes/dir.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="new_version_notification">
|
||||
<property name="text">
|
||||
@ -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 &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>&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>&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 &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>&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>&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 &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">
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -60,7 +60,7 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="stop_all_jobs_button">
|
||||
<property name="text">
|
||||
<string>Stop &all jobs</string>
|
||||
<string>Stop &all non device jobs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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 = []
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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'))
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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':
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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:
|
||||
|
@ -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'
|
||||
|
@ -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'):
|
||||
|
@ -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']
|
||||
|
@ -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,
|
||||
|
49
src/calibre/library/prefs.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import json
|
||||
|
||||
from calibre.constants import preferred_encoding
|
||||
from calibre.utils.config import to_json, from_json
|
||||
|
||||
class DBPrefs(dict):
|
||||
|
||||
def __init__(self, db):
|
||||
dict.__init__(self)
|
||||
self.db = db
|
||||
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
|
||||
val = self.raw_to_object(val)
|
||||
dict.__setitem__(self, key, val)
|
||||
|
||||
def raw_to_object(self, raw):
|
||||
if not isinstance(raw, unicode):
|
||||
raw = raw.decode(preferred_encoding)
|
||||
return json.loads(raw, object_hook=from_json)
|
||||
|
||||
def to_raw(self, val):
|
||||
return json.dumps(val, indent=2, default=to_json)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||
self.db.conn.commit()
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
raw = self.to_raw(val)
|
||||
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||
self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key,
|
||||
raw))
|
||||
self.db.conn.commit()
|
||||
dict.__setitem__(self, key, val)
|
||||
|
||||
def set(self, key, val):
|
||||
self.__setitem__(key, val)
|
||||
|
||||
|
@ -387,3 +387,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)
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
------------------------
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|