Pull from trunk

This commit is contained in:
Timothy Legge 2010-06-14 21:19:53 -03:00
commit 4414ed01f6
96 changed files with 29816 additions and 15269 deletions

View File

@ -4,6 +4,65 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.2
date: 2010-06-11
new features:
- title: "The Cover Browser can now be freely resized."
description: >
"You can now resize the Cover Browser just like the other areas of the user interface by dragging the edge. The Cover Browser now
also emphasizes the cetral book cover, making it larger than the others. Also on widescreen monitors the cover browser is now automatically placed
to the side of the book list instead of below it."
- title: "Added tweak to control how titles and series names are sorted"
- title: "Clicking on row numbers no longer open the viewer. Instead you have to double click"
bug fixes:
- title: "SONY driver: The regression causing slow perfomance has been corrected. Various bug fixes to deal with corner cases"
- title: "iPad driver: Various bugfixes, should now work much more seamlessly."
- title: "Fix regression causing calibre to not start if the library path is invalid, because say a drive has been removed"
tickets: [5787]
- title: "Fix regression in 0.7.1 that broke searching in the e-book viewers and for news sources"
- title: "Fix regressions that caused the Publisher to change when updating Series and floating point custom columns to be rounded to integers."
tickets: [5788]
- title: "Fix regression that broke check database integrity in the presence of custom coulmns or user categories"
tickets: [5779]
- title: "Fix regresison that broke the Email to submenu in the send to device menu"
- title: "Fix Tag browser re-opening closed tree after editing metadata"
tickets: [5744]
- title: "Conversion pipeline: Handle missing/obsolete input/output profiles gracefully"
- title: "Fix Adding/Deleting Search does not refresh Left Pane Correctly"
tickets: [5751]
- title: "Content server: Fix serving of CBZ/CBR files to stanza"
new recipes:
- title: Repantes, Haaretz
author: Darko Miletic
- title: CBC Canada
author: rty
improved recipes:
- The Sun
- The Economist
- Boston Globe
- Honolulu Star Advertiser
- SMH
- Sueddeutsche
- Our Daily Bread
- version: 0.7.1
date: 2010-06-04

View File

@ -61,3 +61,13 @@ sort_columns_at_startup = None
# default if not set: MMM yyyy
gui_pubdate_display_format = 'MMM yyyy'
# Control title and series sorting in the library view.
# If set to 'library_order', Leading articles such as The and A will be ignored.
# If set to 'strictly_alphabetic', the titles will be sorted without processing
# For example, with library_order, The Client will sort under 'C'. With
# strictly_alphabetic, the book will sort under 'T'.
# This flag affects Calibre's library display. It has no effect on devices. In
# addition, titles for books added before changing the flag will retain their
# order until the title is edited. Double-clicking on a title and hitting return
# without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

After

Width:  |  Height:  |  Size: 983 B

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'GabrieleMarini, based on Darko Miletic'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>, Gabriele Marini'
__version__ = 'v1.02 Marini Gabriele '
__date__ = '14062010'
__description__ = 'Italian daily newspaper'
'''
http://www.corrieredellosport.it/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Auto(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'Auto and Formula 1'
cover_url = 'http://www.auto.it/res/imgs/logo_Auto.png'
title = u'Auto'
publisher = 'CONTE Editore'
category = 'Sport'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 60
max_articles_per_feed = 30
use_embedded_content = False
recursion = 10
remove_javascript = True
no_stylesheets = True
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
keep_only_tags = [
dict(name='h2', attrs={'class':['tit_Article y_Txt']}),
dict(name='h2', attrs={'class':['tit_Article']}),
dict(name='div', attrs={'class':['box_Img newsdet_new ']}),
dict(name='div', attrs={'class':['box_Img newsdet_as ']}),
dict(name='table', attrs={'class':['table_A']}),
dict(name='div', attrs={'class':['txt_Article txtBox_cms']}),
dict(name='testoscheda')]
feeds = [
(u'Tutte le News' , u'http://www.auto.it/rss/articoli.xml' ),
(u'Prove su Strada' , u'http://www.auto.it/rss/prove+6.xml'),
(u'Novit\xe0' , u'http://www.auto.it/rss/novita+3.xml')
]

View File

@ -1,5 +1,5 @@
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.boston.com
'''
@ -7,10 +7,10 @@ www.boston.com
from calibre.web.feeds.recipes import BasicNewsRecipe
class BusinessStandard(BasicNewsRecipe):
title = 'Boston'
title = 'The Boston Globe'
__author__ = 'Darko Miletic'
description = 'News from Boston'
oldest_article = 7
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
delay = 1
@ -19,6 +19,9 @@ class BusinessStandard(BasicNewsRecipe):
publisher = 'Boston'
category = 'news, boston, usa, world'
language = 'en'
publication_type = 'newspaper'
masthead_url = 'http://cache.boston.com/images/globe/grslider/the_boston_globe.gif'
extra_css = ' body{font-family: Georgia, serif} div#articleBodyTop{display:block} '
conversion_options = {
'comments' : description
@ -27,8 +30,11 @@ class BusinessStandard(BasicNewsRecipe):
,'publisher' : publisher
}
keep_only_tags = [dict(name='div', attrs={'class':'story'})]
remove_tags = [dict(name=['object','link','script','iframe'])]
keep_only_tags = [dict(attrs={'id':['INDblogEntry','blogEntry','articleHeader','articleGraphs','galleryShell']})]
remove_tags = [
dict(name=['object','link','script','iframe'])
,dict(attrs={'id':['blogheadTools','bdc_emailWidget','tools','relatedContent']})
]
feeds = [
(u'Top Stories' , u'http://feeds.boston.com/boston/topstories' )
@ -38,12 +44,9 @@ class BusinessStandard(BasicNewsRecipe):
]
def print_version(self, url):
return url + '?mode=PF'
return url + '?page=full'
def get_article_url(self, article):
rawarticle = article.get('pheedo_origlink', None)
artls, sep, rsep = rawarticle.rpartition('/?')
if artls == '':
artls = rawarticle.rpartition('?')[0]
return artls
rawarticle = article.get('guid', None)
return rawarticle.rpartition('?')[0]

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'GabrieleMarini, based on Darko Miletic'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>, Gabriele Marini'
__version__ = ' '
__date__ = '14-06-2010'
__description__ = 'Italian daily newspaper'
'''
http://www.corrieredellosport.it/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class ilCorrieredelloSport(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'Italian daily newspaper'
cover_url = 'http://edicola.corrieredellosport.it/newsmem/corsport/prima/nazionale_prima.jpg'
title = u'Il Corriere dello Sport'
publisher = 'CORRIERE DELLO SPORT s.r.l. '
category = 'Sport'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 10
max_articles_per_feed = 100
use_embedded_content = False
recursion = 10
remove_javascript = True
no_stylesheets = True
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
keep_only_tags = [
dict(name='h1', attrs={'class':['tit_Article']}),
dict(name='h1', attrs={'class':['tit_Article_mondiali']}),
dict(name='div', attrs={'class':['box_Img']}),
dict(name='p', attrs={'class':['summary','text']})]
feeds = [
(u'Primo Piano' , u'http://www.corrieredellosport.it/rss/primo_piano.xml' ),
(u'Calcio' , u'http://www.corrieredellosport.it/rss/Calcio-3.xml'),
(u'Formula 1' , u'http://www.corrieredellosport.it/rss/Formula-1-7.xml'),
(u'Moto' , u'http://www.corrieredellosport.it/rss/Moto-8.xml'),
(u'Piu visti' , u'http://www.corrieredellosport.it/rss/piu_visti.xml')
]

View File

@ -24,9 +24,10 @@ class Economist(BasicNewsRecipe):
oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
dict(attrs={'class':['dblClkTrk']})]
remove_tags_before = dict(name=lambda tag: tag.name=='title' and tag.parent.name=='body')
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
keep_only_tags = [dict(id='ec-article-body')]
needs_subscription = True
no_stylesheets = True
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
lambda x:'</html>')]
@ -87,7 +88,9 @@ class Economist(BasicNewsRecipe):
continue
a = tag.find('a', href=True)
if a is not None:
url=a['href'].replace('displaystory', 'PrinterFriendly').strip()
url=a['href']
id_ = re.search(r'story_id=(\d+)', url).group(1)
url = 'http://www.economist.com/node/%s/print'%id_
if url.startswith('Printer'):
url = '/'+url
if url.startswith('/'):

View File

@ -17,8 +17,9 @@ class Economist(BasicNewsRecipe):
oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
dict(attrs={'class':['dblClkTrk']})]
remove_tags_before = dict(name=lambda tag: tag.name=='title' and tag.parent.name=='body')
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
keep_only_tags = [dict(id='ec-article-body')]
no_stylesheets = True
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
lambda x:'</html>')]
@ -88,19 +89,20 @@ class Economist(BasicNewsRecipe):
br = browser()
ret = br.open(url)
raw = ret.read()
url = br.geturl().replace('displaystory', 'PrinterFriendly').strip()
url = br.geturl().split('?')[0]+'/print'
root = html.fromstring(raw)
matches = root.xpath('//*[@class = "article-section"]')
matches = root.xpath('//*[@class = "ec-article-info"]')
feedtitle = 'Miscellaneous'
if matches:
feedtitle = string.capwords(html.tostring(matches[0], method='text',
encoding=unicode))
feedtitle = string.capwords(html.tostring(matches[-1], method='text',
encoding=unicode).split('|')[-1].strip())
return (i, feedtitle, url, title, description, author, published)
def eco_article_found(self, req, result):
from calibre.web.feeds import Article
i, feedtitle, link, title, description, author, published = result
self.log('Found print version for article:', title)
self.log('Found print version for article:', title, 'in', feedtitle,
'at', link)
a = Article(i, title, link, author, description, published, '')

View File

@ -1,96 +0,0 @@
#!/usr/bin/env python
# -*- coding: cp1252 -*-
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
honoluluadvertiser.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Honoluluadvertiser(BasicNewsRecipe):
title = 'Honolulu Advertiser'
__author__ = 'Darko Miletic and Sujata Raman'
description = "Latest national and local Hawaii sports news from The Honolulu Advertiser."
publisher = 'Honolulu Advertiser'
category = 'news, Honolulu, Hawaii'
oldest_article = 2
language = 'en'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'cp1252'
remove_javascript = True
cover_url = 'http://www.honoluluadvertiser.com/graphics/frontpage/frontpage.jpg'
html2lrf_options = [
'--comment' , description
, '--category' , category
, '--publisher' , publisher
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
keep_only_tags = [dict(name='div', attrs={'class':["hon_article_top","article-bodytext","hon_article_photo","storyphoto","article"]}),
dict(name='div', attrs={'id':["storycontentleft","article"]})
]
remove_tags = [dict(name=['object','link','embed']),
dict(name='div', attrs={'class':["article-tools","titleBar","invisiblespacer","articleflex-container","hon_newslist","categoryheader","columnframe","subHeadline","poster-container"]}),
dict(name='div', attrs={'align':["right"]}),
dict(name='div', attrs={'id':["pluckcomments"]}),
dict(name='td', attrs={'class':["prepsfacts"]}),
dict(name='img', attrs={'height':["1"]}),
dict(name='img', attrs={'alt':["Advertisement"]}),
dict(name='img', attrs={'src':["/gcicommonfiles/sr/graphics/common/adlabel_horz.gif","/gcicommonfiles/sr/graphics/common/icon_whatsthis.gif",]}),
]
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-size:large; color:#000000; }
.hon_article_timestamp{font-family:Arial,Helvetica,sans-serif; font-size:70%; }
.postedStoryDate{font-family:Arial,Helvetica,sans-serif; font-size:30%; }
.postedDate{font-family:Arial,Helvetica,sans-serif; font-size:30%; }
.credit{font-family:Arial,Helvetica,sans-serif; font-size:30%; }
.hon_article_top{font-family:Arial,Helvetica,sans-serif; color:#666666; font-size:30%; font-weight:bold;}
.grayBackground{font-family:Arial,Helvetica,sans-serif; color:#666666; font-size:30%;}
.hon_photocaption{font-family:Arial,Helvetica,sans-serif; font-size:30%; }
.photoCaption{font-family:Arial,Helvetica,sans-serif; font-size:30%; }
.hon_photocredit{font-family:Arial,Helvetica,sans-serif; font-size:30%; color:#666666;}
.storyphoto{font-family:Arial,Helvetica,sans-serif; font-size:30%; color:#666666;}
.article-bodytext{font-family:Arial,Helvetica,sans-serif; font-size:xx-small; }
.storycontentleft{font-family:Arial,Helvetica,sans-serif; font-size:xx-small; }
#article{font-family:Arial,Helvetica,sans-serif; font-size:xx-small; }
.contentarea{font-family:Arial,Helvetica,sans-serif; font-size:xx-small; }
.storytext{font-family:Verdana,Arial,Helvetica,sans-serif; font-size:xx-small;}
.storyHeadline{font-family:Arial,Helvetica,sans-serif; font-size:large; color:#000000; font-weight:bold;}
.source{font-family:Arial,Helvetica,sans-serif; color:#333333; font-style: italic; font-weight:bold; }
'''
feeds = [
(u'Breaking news', u'http://www.honoluluadvertiser.com/apps/pbcs.dll/section?Category=RSS01&MIME=XML' )
,(u'Local news', u'http://www.honoluluadvertiser.com/apps/pbcs.dll/section?Category=RSS02&MIME=XML' )
,(u'Sports', u'http://www.honoluluadvertiser.com/apps/pbcs.dll/section?Category=RSS03&MIME=XML' )
,(u'Island life', u'http://www.honoluluadvertiser.com/apps/pbcs.dll/section?Category=RSS05&MIME=XML' )
,(u'Entertainment', u'http://www.honoluluadvertiser.com/apps/pbcs.dll/section?Category=RSS06&MIME=XML' )
,(u'Business', u'http://www.honoluluadvertiser.com/apps/pbcs.dll/section?Category=RSS04&MIME=XML' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
mtag = '\n<meta http-equiv="Content-Language" content="en"/>\n'
soup.head.insert(0,mtag)
for tag in soup.findAll(name=['span','table','font']):
tag.name = 'div'
return soup
# def print_version(self, url):
# ubody, sep, rest = url.rpartition('/-1/')
# root, sep2, article_id = ubody.partition('/article/')
# return u'http://www.honoluluadvertiser.com/apps/pbcs.dll/article?AID=/' + article_id + '&template=printart'

View File

@ -51,6 +51,7 @@ class LeMondeDiplomatiqueEn(BasicNewsRecipe):
, dict(name='div',attrs={'class':'notes surlignable'})
]
remove_tags = [dict(name=['object','link','script','iframe','base'])]
remove_attributes = ['height','width']
def parse_index(self):
articles = []
@ -72,5 +73,5 @@ class LeMondeDiplomatiqueEn(BasicNewsRecipe):
,'url' :url
,'description':description
})
return [(soup.head.title.string, articles)]
return [(self.title, articles)]

View File

@ -11,11 +11,11 @@ http://www.libero-news.it/
from calibre.web.feeds.news import BasicNewsRecipe
class LiberoNews(BasicNewsRecipe):
__author__ = 'Marini Gabriele'
description = 'Italian daily newspaper'
__author__ = 'Marini Gabriele'
description = 'Italian daily newspaper'
cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif'
title = u'Libero'
cover_url = 'http://www.libero-news.it/images/logo.png'
title = u'Libero '
publisher = 'EDITORIALE LIBERO s.r.l 2006'
category = 'News, politics, culture, economy, general interest'

View File

@ -0,0 +1,43 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.reptantes.com.ar
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Reptantes(BasicNewsRecipe):
title = 'Reptantes'
__author__ = 'Darko Miletic'
description = u"cada vez que te haces acupuntura, tu muñeco vudú sufre en algún lado"
oldest_article = 130
max_articles_per_feed = 100
language = 'es'
encoding = 'utf-8'
no_stylesheets = True
use_embedded_content = False
publication_type = 'blog'
extra_css = ' body{font-family: "Palatino Linotype",serif} h2{text-align: center; color:#BE7F8D} img{margin-bottom: 2em} '
conversion_options = {
'comment' : description
, 'tags' : 'literatura'
, 'publisher': 'Hernan Racnati'
, 'language' : language
}
feeds = [(u'Posts', u'http://www.reptantes.com.ar/?feed=rss2')]
keep_only_tags = [dict(attrs={'id':'content'})]
remove_tags = [dict(attrs={'class':'iLikeThis'})]
remove_tags_before = dict(name='h2')
remove_tags_after = dict(attrs={'class':'iLikeThis'})
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

@ -21,7 +21,7 @@ class Smh_au(BasicNewsRecipe):
language = 'en_AU'
remove_empty_feeds = True
masthead_url = 'http://images.smh.com.au/2010/02/02/1087188/smh-620.jpg'
publication_type = 'newspaper'
publication_type = 'newspaper'
extra_css = ' h1{font-family: Georgia,"Times New Roman",Times,serif } body{font-family: Arial,Helvetica,sans-serif} .cT-imageLandscape{font-size: x-small} '
conversion_options = {
@ -47,7 +47,7 @@ class Smh_au(BasicNewsRecipe):
for itimg in soup.findAll('img',src=True):
if itimg['src'].endswith('frontpage.jpg'):
self.cover_url = itimg['src']
for item in soup.findAll(attrs={'class':'cN-storyHeadlineLead cfix'}):
description = ''
title_prefix = ''
@ -65,4 +65,4 @@ class Smh_au(BasicNewsRecipe):
,'url' :url
,'description':description
})
return [(soup.head.title.string, articles)]
return [(self.tag_to_string(soup.find('title')), articles)]

View File

@ -0,0 +1,47 @@
__license__ = 'GPL v3'
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
'''
staradvertiser.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Starbulletin(BasicNewsRecipe):
title = 'Honolulu Star Advertiser'
__author__ = 'Darko Miletic'
description = "Latest national and local Hawaii sports news"
publisher = 'Honolulu Star-Advertiser'
category = 'news, Honolulu, Hawaii'
oldest_article = 2
max_articles_per_feed = 100
language = 'en'
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
publication_type = 'newspaper'
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif} h1,.brown,.postCredit{color: #663300} .storyDeck{font-size: 1.2em; font-weight: bold} '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
, 'linearize_tables' : True
}
remove_tags_before = dict(attrs={'id':'storyTitle'})
remove_tags_after = dict(name='div', attrs={'class':'storytext'})
remove_tags = [
dict(name=['object','link'])
,dict(attrs={'class':'insideStoryImage'})
]
feeds = [
(u'Headlines' , u'http://www.staradvertiser.com/staradvertiser_headlines.rss' )
,(u'News' , u'http://www.staradvertiser.com/news/index.rss' )
,(u'Sports' , u'http://www.staradvertiser.com/sports/index.rss' )
,(u'Features' , u'http://www.staradvertiser.com/features/index.rss' )
,(u'Editorials', u'http://www.staradvertiser.com/editorials/index.rss' )
,(u'Business' , u'http://www.staradvertiser.com/business/index.rss' )
,(u'Travel' , u'http://www.staradvertiser.com/travel/index.rss' )
]

View File

@ -1,60 +0,0 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
starbulletin.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Starbulletin(BasicNewsRecipe):
title = 'Honolulu Star-Bulletin'
__author__ = 'Darko Miletic'
description = "Latest national and local Hawaii sports news"
publisher = 'Honolulu Star-Bulletin'
category = 'news, Honolulu, Hawaii'
oldest_article = 2
max_articles_per_feed = 100
language = 'en'
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
remove_javascript = True
cover_url = 'http://media.starbulletin.com/designimages/spacer.gif'
html2lrf_options = [
'--comment' , description
, '--category' , category
, '--publisher' , publisher
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
keep_only_tags = [ dict(name='div', attrs={'id':'storyColoumn'}) ]
remove_tags = [
dict(name=['object','link'])
,dict(name='span', attrs={'id':'printdesc'})
,dict(name='div' , attrs={'class':'lightGreyBox storyTools clearAll'})
,dict(name='div' , attrs={'id':'breadcrumbs'})
]
feeds = [
(u'Headlines', u'http://www.starbulletin.com/starbulletin_headlines.rss' )
,(u'News', u'http://www.starbulletin.com/news/index.rss' )
,(u'Sports', u'http://www.starbulletin.com/sports/index.rss' )
,(u'Features', u'http://www.starbulletin.com/features/index.rss' )
,(u'Editorials', u'http://www.starbulletin.com/editorials/index.rss' )
,(u'Business', u'http://www.starbulletin.com/business/index.rss' )
,(u'Travel', u'http://www.starbulletin.com/travel/index.rss' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
mtag = '\n<meta http-equiv="Content-Language" content="en"/>\n'
soup.head.insert(0,mtag)
return soup

View File

@ -16,7 +16,7 @@ class DailyTelegraph(BasicNewsRecipe):
language = 'en_AU'
oldest_article = 2
max_articles_per_feed = 10
max_articles_per_feed = 20
remove_javascript = True
no_stylesheets = True
encoding = 'utf8'

View File

@ -1,5 +1,6 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
class AdvancedUserRecipe1268409464(BasicNewsRecipe):
title = u'The Sun'
@ -14,24 +15,27 @@ class AdvancedUserRecipe1268409464(BasicNewsRecipe):
remove_javascript = True
keep_only_tags = [
dict(name='div', attrs={'class':'medium-centered'})
,dict(name='div', attrs={'class':'article'})
,dict(name='div', attrs={'class':'clear-left'})
,dict(name='div', attrs={'class':'text-center'})
dict(id='column-print')
]
remove_tags = [
dict(name='div', attrs={'class':'slideshow'})
,dict(name='div', attrs={'class':'float-left'})
,dict(name='div', attrs={'class':'ltbx-slideshow ltbx-btn-ss'})
,dict(name='a', attrs={'class':'add_a_comment'})
,dict(name='div', attrs={'id':'vxFlashPlayerContent'})
,dict(name='div', attrs={'id':'k1006094r1c1t5w380h529'})
,dict(name='div', attrs={'id':'tum_login_form_container'})
,dict(name='div', attrs={'class':'discHeader'})
,dict(name='div', attrs={'class':'margin-bottom-neg-2'})
dict(name='div', attrs={'class':[
'clear text-center small padding-left-right-5 text-999 padding-top-5 padding-bottom-10 grey-solid-line',
'clear width-625 bg-fff padding-top-10'
]}),
dict(name='video'),
]
def preprocess_html(self, soup):
h1 = soup.find('h1')
if h1 is not None:
text = self.tag_to_string(h1)
nh = Tag(soup, 'h1')
nh.insert(0, text)
h1.replaceWith(nh)
return soup
feeds = [(u'News', u'http://www.thesun.co.uk/sol/homepage/feeds/rss/article312900.ece')
,(u'Sport', u'http://www.thesun.co.uk/sol/homepage/feeds/rss/article247732.ece')

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.1'
__version__ = '0.7.2'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -445,7 +445,7 @@ from calibre.devices.nook.driver import NOOK
from calibre.devices.prs505.driver import PRS505
from calibre.devices.android.driver import ANDROID, S60
from calibre.devices.nokia.driver import N770, N810, E71X
from calibre.devices.eslick.driver import ESLICK
from calibre.devices.eslick.driver import ESLICK, EBK52
from calibre.devices.nuut2.driver import NUUT2
from calibre.devices.iriver.driver import IRIVER_STORY
from calibre.devices.binatone.driver import README
@ -519,6 +519,7 @@ plugins += [
N810,
COOL_ER,
ESLICK,
EBK52,
NUUT2,
IRIVER_STORY,
GER2,

View File

@ -30,7 +30,7 @@ class ANDROID(USBMS):
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
# Samsung
0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222, 0x0224]},
0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]},
# Acer
0x502 : { 0x3203 : [0x0100]},
@ -41,10 +41,12 @@ class ANDROID(USBMS):
'be used')
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700']
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE']
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD',
'PROD_GT-I9000']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'PROD_GT-I9000_CARD']
OSX_MAIN_MEM = 'HTC Android Phone Media'

File diff suppressed because it is too large Load Diff

View File

@ -36,4 +36,29 @@ class ESLICK(USBMS):
SUPPORTS_SUB_DIRS = True
@classmethod
def can_handle(cls, dev, debug=False):
return (dev[3], dev[4]) != ('philips', 'Philips d')
class EBK52(ESLICK):
name = 'EBK-52 Device Interface'
gui_name = 'Sigmatek EBK'
description = _('Communicate with the Sigmatek eBook reader.')
FORMATS = ['epub', 'fb2', 'pdf', 'txt']
VENDOR_NAME = ''
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_READER'
MAIN_MEMORY_VOLUME_LABEL = 'Sigmatek Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'Sigmatek Storage Card'
@classmethod
def can_handle(cls, dev, debug=False):
return (dev[3], dev[4]) == ('philips', 'Philips d')

View File

@ -289,7 +289,10 @@ class DevicePlugin(Plugin):
word "card" if C{on_card} is not None otherwise it must contain the word "memory".
:files: A list of paths and/or file-like objects. If they are paths and
the paths point to temporary files, they may have an additional
attribute, original_file_path pointing to the originals.
attribute, original_file_path pointing to the originals. They may have
another optional attribute, deleted_after_upload which if True means
that the file pointed to by original_file_path will be deleted after
being uploaded to the device.
:names: A list of file names that the books should have
once uploaded to the device. len(names) == len(files)
:return: A list of 3-element tuples. The list is meant to be passed

View File

@ -7,12 +7,33 @@ __docformat__ = 'restructuredtext en'
'''
Device driver for Amazon's Kindle
'''
import datetime, os, re, sys
import datetime, os, re, sys, json, hashlib
from cStringIO import StringIO
from struct import unpack
from calibre.devices.usbms.driver import USBMS
'''
Notes on collections:
A collections cache is stored at system/collections.json
The cache is read only, changes made to it are overwritten (it is regenerated)
on device disconnect
A log of collection creation/manipulation is available at
system/userannotationlog
collections.json refers to books via a SHA1 hash of the absolute path to the
book (prefix is /mnt/us on my Kindle). The SHA1 hash may or may not be prefixed
by some characters, use the last 40 characters.
Changing the metadata and resending the file doesn't seem to affect collections
Adding a book to a collection on the Kindle does not change the book file at all
(i.e. it is binary identical). Therefore collection information is not stored in
file metadata.
'''
class KINDLE(USBMS):
name = 'Kindle Device Interface'
@ -60,6 +81,7 @@ class KINDLE(USBMS):
'replace')
return mi
def get_annotations(self, path_map):
MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt']
mbp_formats = set(MBP_FORMATS)
@ -150,6 +172,37 @@ class KINDLE2(KINDLE):
PRODUCT_ID = [0x0002]
BCD = [0x0100]
def books(self, oncard=None, end_session=True):
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
# Read collections information
collections = os.path.join(self._main_prefix, 'system', 'collections.json')
if os.access(collections, os.R_OK):
try:
self.kindle_update_booklist(bl, collections)
except:
import traceback
traceback.print_exc()
return bl
def kindle_update_booklist(self, bl, collections):
with open(collections, 'rb') as f:
collections = f.read()
collections = json.loads(collections)
path_map = {}
for name, val in collections.items():
col = name.split('@')[0]
items = val.get('items', [])
for x in items:
x = x[-40:]
if x not in path_map:
path_map[x] = set([])
path_map[x].add(col)
if path_map:
for book in bl:
path = '/mnt/us/'+book.lpath
h = hashlib.sha1(path).hexdigest()
if h in path_map:
book.device_collections = list(sorted(path_map[h]))
class KINDLE_DX(KINDLE2):

View File

@ -47,7 +47,7 @@ def strptime(src):
src[2] = str(MONTH_MAP[src[2]])
return time.strptime(' '.join(src), '%w, %d %m %Y %H:%M:%S %Z')
def strftime(epoch, zone=time.gmtime):
def strftime(epoch, zone=time.localtime):
src = time.strftime("%w, %d %m %Y %H:%M:%S GMT", zone(epoch)).split()
src[0] = INVERSE_DAY_MAP[int(src[0][:-1])]+','
src[2] = INVERSE_MONTH_MAP[int(src[2])]
@ -423,7 +423,7 @@ class XMLCache(object):
return ans
def update_text_record(self, record, book, path, bl_index):
timestamp = os.path.getctime(path)
timestamp = os.path.getmtime(path)
date = strftime(timestamp)
if date != record.get('date', None):
record.set('date', date)

View File

@ -47,7 +47,7 @@ class Book(MetaInformation):
def __eq__(self, other):
# use lpath because the prefix can change, changing path
return self.path == getattr(other, 'lpath', None)
return self.lpath == getattr(other, 'lpath', None)
@dynamic_property
def db_id(self):

View File

@ -294,6 +294,18 @@ class USBMS(CLI, Device):
self.report_progress(1.0, _('Sending metadata to device...'))
debug_print('USBMS: finished sync_booklists')
@classmethod
def build_template_regexp(cls):
def replfunc(match):
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
return '(?P<' + match.group(1) + '>.+?)'
elif match.group(1) == 'authors':
return '(?P<author>.+?)'
else:
return '(.+?)'
template = cls.save_template().rpartition('/')[2]
return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)')
@classmethod
def path_to_unicode(cls, path):
if isbytestring(path):
@ -355,22 +367,22 @@ class USBMS(CLI, Device):
from calibre.ebooks.metadata.meta import metadata_from_formats
from calibre.customize.ui import quick_metadata
with quick_metadata:
return metadata_from_formats(fmts)
return metadata_from_formats(fmts, force_read_metadata=True,
pattern=cls.build_template_regexp())
@classmethod
def book_from_path(cls, prefix, path):
def book_from_path(cls, prefix, lpath):
from calibre.ebooks.metadata import MetaInformation
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, path)))
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
else:
from calibre.ebooks.metadata.meta import metadata_from_filename
mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)),
re.compile(r'^(?P<title>[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+'))
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
cls.build_template_regexp())
if mi is None:
mi = MetaInformation(os.path.splitext(os.path.basename(path))[0],
mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0],
[_('Unknown')])
size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size
book = cls.book_class(prefix, path, other=mi, size=size)
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
book = cls.book_class(prefix, lpath, other=mi, size=size)
return book

View File

@ -706,15 +706,22 @@ OptionRecommendation(name='timestamp',
for rec in group:
setattr(self.opts, rec.option.name, rec.recommended_value)
for x in input_profiles():
if x.short_name == self.opts.input_profile:
self.opts.input_profile = x
break
def set_profile(profiles, which):
attr = which + '_profile'
sval = getattr(self.opts, attr)
for x in profiles():
if x.short_name == sval:
setattr(self.opts, attr, x)
return
self.log.warn(
'Profile (%s) %r is no longer available, using default'%(which, sval))
for x in profiles():
if x.short_name == 'default':
setattr(self.opts, attr, x)
break
for x in output_profiles():
if x.short_name == self.opts.output_profile:
self.opts.output_profile = x
break
set_profile(input_profiles, 'input')
set_profile(output_profiles, 'output')
self.read_user_metadata()
self.opts.no_inline_navbars = self.opts.output_profile.supports_mobi_indexing \

View File

@ -10,12 +10,31 @@ import os
from contextlib import closing
from calibre.customize import FileTypePlugin
from calibre.utils.zipfile import ZipFile, stringFileHeader
def is_comic(list_of_names):
extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names])
comic_extensions = set(['jpg', 'jpeg', 'png'])
return len(extensions - comic_extensions) == 0
def archive_type(stream):
try:
pos = stream.tell()
except:
pos = 0
id_ = stream.read(4)
ans = None
if id_ == stringFileHeader:
ans = 'zip'
elif id_.startswith('Rar'):
ans = 'rar'
try:
stream.seek(pos)
except:
pass
return ans
class ArchiveExtract(FileTypePlugin):
name = 'Archive Extract'
author = 'Kovid Goyal'
@ -31,7 +50,6 @@ class ArchiveExtract(FileTypePlugin):
if is_rar:
from calibre.libunrar import extract_member, names
else:
from calibre.utils.zipfile import ZipFile
zf = ZipFile(archive, 'r')
if is_rar:

View File

@ -27,16 +27,16 @@ for i, ext in enumerate(_METADATA_PRIORITIES):
def path_to_ext(path):
return os.path.splitext(path)[1][1:].lower()
def metadata_from_formats(formats):
def metadata_from_formats(formats, force_read_metadata=False, pattern=None):
try:
return _metadata_from_formats(formats)
return _metadata_from_formats(formats, force_read_metadata, pattern)
except:
mi = metadata_from_filename(list(iter(formats))[0])
mi = metadata_from_filename(list(iter(formats), pattern)[0])
if not mi.authors:
mi.authors = [_('Unknown')]
return mi
def _metadata_from_formats(formats):
def _metadata_from_formats(formats, force_read_metadata=False, pattern=None):
mi = MetaInformation(None, None)
formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)],
METADATA_PRIORITIES[path_to_ext(y)]))
@ -51,7 +51,9 @@ def _metadata_from_formats(formats):
with open(path, 'rb') as stream:
try:
newmi = get_metadata(stream, stream_type=ext,
use_libprs_metadata=True)
use_libprs_metadata=True,
force_read_metadata=force_read_metadata,
pattern=pattern)
mi.smart_update(newmi)
except:
continue
@ -69,18 +71,21 @@ def is_recipe(filename):
return filename.startswith('calibre') and \
filename.rpartition('.')[0].endswith('_recipe_out')
def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False,
force_read_metadata=False, pattern=None):
pos = 0
if hasattr(stream, 'tell'):
pos = stream.tell()
try:
return _get_metadata(stream, stream_type, use_libprs_metadata)
return _get_metadata(stream, stream_type, use_libprs_metadata,
force_read_metadata, pattern)
finally:
if hasattr(stream, 'seek'):
stream.seek(pos)
def _get_metadata(stream, stream_type, use_libprs_metadata):
def _get_metadata(stream, stream_type, use_libprs_metadata,
force_read_metadata=False, pattern=None):
if stream_type: stream_type = stream_type.lower()
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
stream_type = 'html'
@ -100,8 +105,8 @@ def _get_metadata(stream, stream_type, use_libprs_metadata):
mi = MetaInformation(None, None)
name = os.path.basename(getattr(stream, 'name', ''))
base = metadata_from_filename(name)
if is_recipe(name) or prefs['read_file_metadata']:
base = metadata_from_filename(name, pat=pattern)
if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']:
mi = get_file_type_metadata(stream, stream_type)
if base.title == os.path.splitext(name)[0] and base.authors is None:
# Assume that there was no metadata in the file and the user set pattern
@ -139,7 +144,7 @@ def metadata_from_filename(name, pat=None):
pat = re.compile(prefs.get('filename_pattern'))
name = name.replace('_', ' ')
match = pat.search(name)
if match:
if match is not None:
try:
mi.title = match.group('title')
except IndexError:

View File

@ -103,8 +103,8 @@ class CoverManager(object):
32)]
img_data = create_cover_page(lines, I('library.png'))
id, href = self.oeb.manifest.generate('cover_image',
'cover_image.png')
item = self.oeb.manifest.add(id, href, guess_type('t.png')[0],
'cover_image.jpg')
item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0],
data=img_data)
m.clear('cover')
m.add('cover', item.id)

View File

@ -99,6 +99,8 @@ def _config():
help=_('Limit max simultaneous jobs to number of CPUs'))
c.add_opt('tag_browser_hidden_categories', default=set(),
help=_('tag browser categories not to display'))
c.add_opt('gui_layout', choices=['wide', 'narrow'],
help=_('The layout of the user interface'), default='wide')
return ConfigProxy(c)
config = _config()
@ -125,6 +127,17 @@ def available_width():
desktop = QCoreApplication.instance().desktop()
return desktop.availableGeometry().width()
_is_widescreen = None
def is_widescreen():
global _is_widescreen
if _is_widescreen is None:
try:
_is_widescreen = float(available_width())/available_height() > 1.4
except:
_is_widescreen = False
return _is_widescreen
def extension(path):
return os.path.splitext(path)[1][1:].lower()

1209
src/calibre/gui2/actions.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,280 @@
#!/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, collections
from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \
QVBoxLayout, QScrollArea, QPropertyAnimation, QEasingCurve, \
QSizePolicy, QPainter, QRect, pyqtProperty, QDesktopServices, QUrl
from calibre import fit_image, prepare_string_for_xml
from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import preferred_encoding
from calibre.library.comments import comments_to_html
# render_rows(data) {{{
WEIGHTS = collections.defaultdict(lambda : 100)
WEIGHTS[_('Path')] = 5
WEIGHTS[_('Formats')] = 1
WEIGHTS[_('Collections')] = 2
WEIGHTS[_('Series')] = 3
WEIGHTS[_('Tags')] = 4
def render_rows(data):
keys = data.keys()
keys.sort(cmp=lambda x, y: cmp(WEIGHTS[x], WEIGHTS[y]))
rows = []
for key in keys:
txt = data[key]
if key in ('id', _('Comments')) or not hasattr(txt, 'strip') or not txt.strip() or \
txt == 'None':
continue
if isinstance(key, str):
key = key.decode(preferred_encoding, 'replace')
if isinstance(txt, str):
txt = txt.decode(preferred_encoding, 'replace')
if '</font>' not in txt:
txt = prepare_string_for_xml(txt)
if 'id' in data:
if key == _('Path'):
txt = u'<a href="path:%s">%s</a>'%(data['id'],
_('Click to open'))
if key == _('Formats') and txt and txt != _('None'):
fmts = [x.strip() for x in txt.split(',')]
fmts = [u'<a href="format:%s:%s">%s</a>' % (data['id'], x, x) for x
in fmts]
txt = ', '.join(fmts)
else:
if key == _('Path'):
txt = u'<a href="devpath:%s">%s</a>'%(txt,
_('Click to open'))
rows.append((key, txt))
return rows
# }}}
class CoverView(QWidget): # {{{
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setMaximumSize(QSize(120, 120))
self.setMinimumSize(QSize(120, 1))
self._current_pixmap_size = self.maximumSize()
self.animation = QPropertyAnimation(self, 'current_pixmap_size', self)
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
self.animation.setDuration(1000)
self.animation.setStartValue(QSize(0, 0))
self.animation.valueChanged.connect(self.value_changed)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.default_pixmap = QPixmap(I('book.svg'))
self.pixmap = self.default_pixmap
self.pwidth = self.pheight = None
self.data = {}
self.do_layout()
def value_changed(self, val):
self.update()
def setCurrentPixmapSize(self, val):
self._current_pixmap_size = val
def do_layout(self):
pixmap = self.pixmap
pwidth, pheight = pixmap.width(), pixmap.height()
self.pwidth, self.pheight = fit_image(pwidth, pheight,
self.rect().width(), self.rect().height())[1:]
self.current_pixmap_size = QSize(self.pwidth, self.pheight)
self.animation.setEndValue(self.current_pixmap_size)
def relayout(self, parent_size):
self.setMaximumSize(parent_size.width(),
min(int(parent_size.height()/2.),int(4/3. * parent_size.width())+1))
self.resize(self.maximumSize())
self.animation.stop()
self.do_layout()
def sizeHint(self):
return self.maximumSize()
def show_data(self, data):
self.animation.stop()
if data.get('id', None) == self.data.get('id', None):
return
self.data = {'id':data.get('id', None)}
if data.has_key('cover'):
self.pixmap = QPixmap.fromImage(data.pop('cover'))
if self.pixmap.isNull():
self.pixmap = self.default_pixmap
else:
self.pixmap = self.default_pixmap
self.do_layout()
self.update()
self.animation.start()
def paintEvent(self, event):
canvas_size = self.rect()
width = self.current_pixmap_size.width()
extrax = canvas_size.width() - width
if extrax < 0: extrax = 0
x = int(extrax/2.)
height = self.current_pixmap_size.height()
extray = canvas_size.height() - height
if extray < 0: extray = 0
y = int(extray/2.)
target = QRect(x, y, width, height)
p = QPainter(self)
p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
p.drawPixmap(target, self.pixmap.scaled(target.size(),
Qt.KeepAspectRatio, Qt.SmoothTransformation))
p.end()
current_pixmap_size = pyqtProperty('QSize',
fget=lambda self: self._current_pixmap_size,
fset=setCurrentPixmapSize
)
# }}}
class Label(QLabel):
mr = pyqtSignal(object)
link_clicked = pyqtSignal(object)
def __init__(self):
QLabel.__init__(self)
self.setTextFormat(Qt.RichText)
self.setText('')
self.setWordWrap(True)
self.linkActivated.connect(self.link_activated)
self._link_clicked = False
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def link_activated(self, link):
self._link_clicked = True
link = unicode(link)
self.link_clicked.emit(link)
def mouseReleaseEvent(self, ev):
QLabel.mouseReleaseEvent(self, ev)
if not self._link_clicked:
self.mr.emit(ev)
self._link_clicked = False
class BookInfo(QScrollArea):
def __init__(self, parent=None):
QScrollArea.__init__(self, parent)
self.setWidgetResizable(True)
self.label = Label()
self.setWidget(self.label)
self.link_clicked = self.label.link_clicked
self.mr = self.label.mr
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def show_data(self, data):
self.label.setText('')
rows = render_rows(data)
rows = u'\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
k, t in rows])
if _('Comments') in data and data[_('Comments')]:
comments = comments_to_html(data[_('Comments')])
rows += u'<tr><td colspan="2">%s</td></tr>'%comments
self.label.setText(u'<table>%s</table>'%rows)
class BookDetails(QWidget):
resized = pyqtSignal(object)
show_book_info = pyqtSignal()
open_containing_folder = pyqtSignal(int)
view_specific_format = pyqtSignal(int, object)
# Drag 'n drop {{{
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
files_dropped = pyqtSignal(object, object)
@classmethod
def paths_from_event(cls, event):
'''
Accept a drop event and return a list of paths that can be read from
and represent files with extensions.
'''
if event.mimeData().hasFormat('text/uri-list'):
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
def dragEnterEvent(self, event):
if int(event.possibleActions() & Qt.CopyAction) + \
int(event.possibleActions() & Qt.MoveAction) == 0:
return
paths = self.paths_from_event(event)
if paths:
event.acceptProposedAction()
def dropEvent(self, event):
paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction)
self.files_dropped.emit(event, paths)
def dragMoveEvent(self, event):
event.acceptProposedAction()
# }}}
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self._layout = QVBoxLayout()
self.setLayout(self._layout)
self.cover_view = CoverView(self)
self.cover_view.relayout(self.size())
self.resized.connect(self.cover_view.relayout, type=Qt.QueuedConnection)
self._layout.addWidget(self.cover_view, alignment=Qt.AlignHCenter)
self.book_info = BookInfo(self)
self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self._link_clicked)
self.book_info.mr.connect(self.mouseReleaseEvent)
self.setMinimumSize(QSize(190, 200))
self.setCursor(Qt.PointingHandCursor)
def _link_clicked(self, link):
typ, _, val = link.partition(':')
if typ == 'path':
self.open_containing_folder.emit(int(val))
elif typ == 'format':
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
path = os.path.dirname(val)
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def mouseReleaseEvent(self, ev):
ev.accept()
self.show_book_info.emit()
def resizeEvent(self, ev):
self.resized.emit(self.size())
def show_data(self, data):
self.cover_view.show_data(data)
self.book_info.show_data(data)
def reset_info(self):
self.show_data({})

View File

@ -7,16 +7,18 @@ __docformat__ = 'restructuredtext en'
Module to implement the Cover Flow feature
'''
import sys, os
import sys, os, time
from PyQt4.QtGui import QImage, QSizePolicy
from PyQt4.QtCore import Qt, QSize, SIGNAL, QObject
from PyQt4.Qt import QImage, QSizePolicy, QTimer, QDialog, Qt, QSize, \
QStackedLayout, QLabel
from calibre import plugins
from calibre.gui2 import config
from calibre.gui2 import config, available_height, available_width
pictureflow, pictureflowerror = plugins['pictureflow']
if pictureflow is not None:
class EmptyImageList(pictureflow.FlowImages):
def __init__(self):
pictureflow.FlowImages.__init__(self)
@ -51,7 +53,7 @@ if pictureflow is not None:
def __init__(self, model, buffer=20):
pictureflow.FlowImages.__init__(self)
self.model = model
QObject.connect(self.model, SIGNAL('modelReset()'), self.reset)
self.model.modelReset.connect(self.reset)
def count(self):
return self.model.count()
@ -66,7 +68,7 @@ if pictureflow is not None:
return ans
def reset(self):
self.emit(SIGNAL('dataChanged()'))
self.dataChanged.emit()
def image(self, index):
return self.model.cover(index)
@ -74,13 +76,16 @@ if pictureflow is not None:
class CoverFlow(pictureflow.PictureFlow):
def __init__(self, height=300, parent=None, text_height=25):
def __init__(self, parent=None):
pictureflow.PictureFlow.__init__(self, parent,
config['cover_flow_queue_length']+1)
self.setSlideSize(QSize(int(2/3. * height), height))
self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+text_height))
self.setMinimumSize(QSize(300, 150))
self.setFocusPolicy(Qt.WheelFocus)
self.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding))
def sizeHint(self):
return self.minimumSize()
def wheelEvent(self, ev):
ev.accept()
@ -95,6 +100,118 @@ else:
DatabaseImages = None
FileSystemImages = None
class CoverFlowMixin(object):
def __init__(self):
self.cover_flow = None
if CoverFlow is not None:
self.cf_last_updated_at = None
self.cover_flow_sync_timer = QTimer(self)
self.cover_flow_sync_timer.timeout.connect(self.cover_flow_do_sync)
self.cover_flow_sync_flag = True
self.cover_flow = CoverFlow(parent=self)
self.cover_flow.currentChanged.connect(self.sync_listview_to_cf)
self.library_view.selectionModel().currentRowChanged.connect(
self.sync_cf_to_listview)
self.db_images = DatabaseImages(self.library_view.model())
self.cover_flow.setImages(self.db_images)
else:
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
+'<br>'+pictureflowerror)
self.cover_flow.setWordWrap(True)
if config['separate_cover_flow']:
self.cb_splitter.button.clicked.connect(self.toggle_cover_browser)
if CoverFlow is not None:
self.cover_flow.stop.connect(self.hide_cover_browser)
else:
self.cb_splitter.insertWidget(self.cb_splitter.side_index, self.cover_flow)
if CoverFlow is not None:
self.cover_flow.stop.connect(self.cb_splitter.hide_side_pane)
self.cb_splitter.button.toggled.connect(self.cover_browser_toggled)
def toggle_cover_browser(self):
cbd = getattr(self, 'cb_dialog', None)
if cbd is not None:
self.hide_cover_browser()
else:
self.show_cover_browser()
def cover_browser_toggled(self, *args):
if self.cb_splitter.button.isChecked():
self.cover_browser_shown()
else:
self.cover_browser_hidden()
def cover_browser_shown(self):
self.cover_flow.setFocus(Qt.OtherFocusReason)
if CoverFlow is not None:
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
self.cover_flow_sync_timer.start(500)
self.library_view.setCurrentIndex(
self.library_view.currentIndex())
self.library_view.scroll_to_row(self.library_view.currentIndex().row())
def cover_browser_hidden(self):
if CoverFlow is not None:
self.cover_flow_sync_timer.stop()
idx = self.library_view.model().index(self.cover_flow.currentSlide(), 0)
if idx.isValid():
sm = self.library_view.selectionModel()
sm.select(idx, sm.ClearAndSelect|sm.Rows)
self.library_view.setCurrentIndex(idx)
self.library_view.scroll_to_row(idx.row())
def show_cover_browser(self):
d = QDialog(self)
ah, aw = available_height(), available_width()
d.resize(int(aw/1.5), ah-60)
d._layout = QStackedLayout()
d.setLayout(d._layout)
d.setWindowTitle(_('Browse by covers'))
d.layout().addWidget(self.cover_flow)
self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason)
d.show()
self.cb_splitter.button.set_state_to_hide()
d.finished.connect(self.cb_splitter.button.set_state_to_show)
self.cb_dialog = d
def hide_cover_browser(self):
cbd = getattr(self, 'cb_dialog', None)
if cbd is not None:
cbd.accept()
self.cb_dialog = None
def sync_cf_to_listview(self, current, previous):
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \
self.cover_flow.currentSlide() != current.row():
self.cover_flow.setCurrentSlide(current.row())
self.cover_flow_sync_flag = True
def cover_flow_do_sync(self):
self.cover_flow_sync_flag = True
try:
if self.cover_flow.isVisible() and self.cf_last_updated_at is not None and \
time.time() - self.cf_last_updated_at > 0.5:
self.cf_last_updated_at = None
row = self.cover_flow.currentSlide()
m = self.library_view.model()
index = m.index(row, 0)
if self.library_view.currentIndex().row() != row and index.isValid():
self.cover_flow_sync_flag = False
self.library_view.scroll_to_row(index.row())
sm = self.library_view.selectionModel()
sm.select(index, sm.ClearAndSelect|sm.Rows)
self.library_view.setCurrentIndex(index)
except:
pass
def sync_listview_to_cf(self, row):
self.cf_last_updated_at = time.time()
def main(args=sys.argv):
return 0
@ -103,12 +220,12 @@ if __name__ == '__main__':
app = QApplication([])
w = QMainWindow()
cf = CoverFlow()
cf.resize(cf.minimumSize())
w.resize(cf.minimumSize()+QSize(30, 20))
cf.resize(int(available_width()/1.5), available_height()-60)
w.resize(cf.size()+QSize(30, 20))
path = sys.argv[1]
model = FileSystemImages(sys.argv[1])
cf.currentChanged[int].connect(model.currentChanged)
cf.setImages(model)
cf.connect(cf, SIGNAL('currentChanged(int)'), model.currentChanged)
w.setCentralWidget(cf)
w.show()

View File

@ -101,6 +101,11 @@ class Float(Int):
w.setSpecialValueText(_('Undefined'))
w.setSingleStep(1)
def setter(self, val):
if val is None:
val = self.widgets[1].minimum()
self.widgets[1].setValue(val)
class Rating(Int):
def setup_ui(self, parent):

View File

@ -3,24 +3,26 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
# Imports {{{
import os, traceback, Queue, time, socket, cStringIO, re
import os, traceback, Queue, time, socket, cStringIO, re, sys
from threading import Thread, RLock
from itertools import repeat
from functools import partial
from binascii import unhexlify
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
Qt, pyqtSignal
Qt, pyqtSignal, QColor, QPainter
from PyQt4.QtSvg import QSvgRenderer
from calibre.customize.ui import available_input_formats, available_output_formats, \
device_plugins
from calibre.devices.interface import DevicePlugin
from calibre.devices.errors import UserFeedback
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.utils.ipc.job import BaseJob
from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
pixmap_to_data, warning_dialog, \
question_dialog
pixmap_to_data, warning_dialog, \
question_dialog, info_dialog, choose_dir
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
from calibre import preferred_encoding, prints
from calibre.utils.filenames import ascii_filename
@ -391,16 +393,14 @@ class DeviceMenu(QMenu): # {{{
default_account = (dest, False, False, I('mail.svg'),
_('Email to')+' '+account)
action1 = DeviceAction(dest, False, False, I('mail.svg'),
_('Email to')+' '+account, self)
_('Email to')+' '+account)
action2 = DeviceAction(dest, True, False, I('mail.svg'),
_('Email to')+' '+account, self)
_('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()
self.connect(action1, SIGNAL('a_s(QAction)'),
self.action_triggered)
self.connect(action2, SIGNAL('a_s(QAction)'),
self.action_triggered)
action1.a_s.connect(self.action_triggered)
action2.a_s.connect(self.action_triggered)
basic_actions = [
('main:', False, False, I('reader.svg'),
@ -599,7 +599,204 @@ class Emailer(Thread): # {{{
# }}}
class DeviceGUI(object):
class DeviceMixin(object): # {{{
def __init__(self):
self.db_book_uuid_cache = set()
self.device_error_dialog = error_dialog(self, _('Error'),
_('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal)
self.device_connected = None
self.emailer = Emailer()
self.emailer.start()
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
self.job_manager, Dispatcher(self.status_bar.show_message))
self.device_manager.start()
def set_default_thumbnail(self, height):
r = QSvgRenderer(I('book.svg'))
pixmap = QPixmap(height, height)
pixmap.fill(QColor(255,255,255))
p = QPainter(pixmap)
r.render(p)
p.end()
self.default_thumbnail = (pixmap.width(), pixmap.height(),
pixmap_to_data(pixmap))
def connect_to_folder(self):
dir = choose_dir(self, 'Select Device Folder',
_('Select folder to open as device'))
if dir is not None:
self.device_manager.connect_to_folder(dir)
def disconnect_from_folder(self):
self.device_manager.disconnect_folder()
def _sync_action_triggered(self, *args):
m = getattr(self, '_sync_menu', None)
if m is not None:
m.trigger_default()
def create_device_menu(self):
self._sync_menu = DeviceMenu(self)
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.disconnect_from_folder.connect(self.disconnect_from_folder)
if self.device_connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
if self.device_connected == 'folder':
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
else:
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
def device_job_exception(self, job):
'''
Handle exceptions in threaded device jobs.
'''
if isinstance(getattr(job, 'exception', None), UserFeedback):
ex = job.exception
func = {UserFeedback.ERROR:error_dialog,
UserFeedback.WARNING:warning_dialog,
UserFeedback.INFO:info_dialog}[ex.level]
return func(self, _('Failed'), ex.msg, det_msg=ex.details if
ex.details else '', show=True)
try:
if 'Could not read 32 bytes on the control bus.' in \
unicode(job.details):
error_dialog(self, _('Error talking to device'),
_('There was a temporary error talking to the '
'device. Please unplug and reconnect the device '
'and or reboot.')).show()
return
except:
pass
try:
prints(job.details, file=sys.stderr)
except:
pass
if not self.device_error_dialog.isVisible():
self.device_error_dialog.setDetailedText(job.details)
self.device_error_dialog.show()
# Device connected {{{
def device_detected(self, connected, is_folder_device):
'''
Called when a device is connected to the computer.
'''
if connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
if is_folder_device:
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
self.device_manager.get_device_information(\
Dispatcher(self.info_read))
self.set_default_thumbnail(\
self.device_manager.device.THUMBNAIL_HEIGHT)
self.status_bar.show_message(_('Device: ')+\
self.device_manager.device.__class__.get_gui_name()+\
_(' detected.'), 3000)
self.device_connected = 'device' if not is_folder_device else 'folder'
self._sync_menu.enable_device_actions(True,
self.device_manager.device.card_prefix(),
self.device_manager.device)
self.location_view.model().device_connected(self.device_manager.device)
self.eject_action.setEnabled(True)
self.refresh_ondevice_info (device_connected = True, reset_only = True)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
self.device_connected = None
self._sync_menu.enable_device_actions(False)
self.location_view.model().update_devices()
self.vanity.setText(self.vanity_template%\
dict(version=self.latest_version, device=' '))
self.device_info = ' '
if self.current_view() != self.library_view:
self.book_details.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0))
self.eject_action.setEnabled(False)
self.refresh_ondevice_info (device_connected = False)
def info_read(self, job):
'''
Called once device information has been read.
'''
if job.failed:
return self.device_job_exception(job)
info, cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
self.device_info = _('Connected ')+info[0]
self.vanity.setText(self.vanity_template%\
dict(version=self.latest_version, device=self.device_info))
self.device_manager.books(Dispatcher(self.metadata_downloaded))
def metadata_downloaded(self, job):
'''
Called once metadata has been read for all books on the device.
'''
if job.failed:
self.device_job_exception(job)
return
self.set_books_in_library(job.result, reset=True)
mainlist, cardalist, cardblist = job.result
self.memory_view.set_database(mainlist)
self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
self.card_a_view.set_database(cardalist)
self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
self.card_b_view.set_database(cardblist)
self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
self.sync_news()
self.sync_catalogs()
self.refresh_ondevice_info(device_connected = True)
def refresh_ondevice_info(self, device_connected, reset_only = False):
'''
Force the library view to refresh, taking into consideration
books information
'''
self.book_on_device(None, reset=True)
if reset_only:
return
self.library_view.set_device_connected(device_connected)
# }}}
def remove_paths(self, paths):
return self.device_manager.delete_books(
Dispatcher(self.books_deleted), paths)
def books_deleted(self, job):
'''
Called once deletion is done on the device
'''
for view in (self.memory_view, self.card_a_view, self.card_b_view):
view.model().deletion_done(job, job.failed)
if job.failed:
self.device_job_exception(job)
return
if self.delete_memory.has_key(job):
paths, model = self.delete_memory.pop(job)
self.device_manager.remove_books_from_metadata(paths,
self.booklists())
model.paths_deleted(paths)
self.upload_booklists()
# Clear the ondevice info so it will be recomputed
self.book_on_device(None, None, reset=True)
# We want to reset all the ondevice flags in the library. Use a big
# hammer, so we don't need to worry about whether some succeeded or not
self.library_view.model().refresh()
def dispatch_sync_event(self, dest, delete, specific):
rows = self.library_view.selectionModel().selectedRows()
@ -686,7 +883,7 @@ class DeviceGUI(object):
self.emailer.send_mails(jobnames,
Dispatcher(partial(self.emails_sent, remove=remove)),
attachments, to_s, subjects, texts, attachment_names)
self.status_bar.showMessage(_('Sending email to')+' '+to, 3000)
self.status_bar.show_message(_('Sending email to')+' '+to, 3000)
auto = []
if _auto_ids != []:
@ -747,7 +944,7 @@ class DeviceGUI(object):
'%s'%errors, show=True
)
else:
self.status_bar.showMessage(_('Sent by email:') + ', '.join(good),
self.status_bar.show_message(_('Sent by email:') + ', '.join(good),
5000)
def cover_to_thumbnail(self, data):
@ -786,7 +983,7 @@ class DeviceGUI(object):
attachments, to_s, subjects, texts, attachment_names)
sent_mails.append(to_s[0])
if sent_mails:
self.status_bar.showMessage(_('Sent news to')+' '+\
self.status_bar.show_message(_('Sent news to')+' '+\
', '.join(sent_mails), 3000)
def sync_catalogs(self, send_ids=None, do_auto_convert=True):
@ -845,12 +1042,13 @@ class DeviceGUI(object):
self.upload_books(files, names, metadata,
on_card=on_card,
memory=[files, remove])
self.status_bar.showMessage(_('Sending catalogs to device.'), 5000)
self.status_bar.show_message(_('Sending catalogs to device.'), 5000)
def sync_news(self, send_ids=None, do_auto_convert=True):
if self.device_connected:
del_on_upload = config['delete_news_from_library_on_upload']
settings = self.device_manager.device.settings()
ids = list(dynamic.get('news_to_be_synced', set([]))) if send_ids is None else send_ids
ids = [id for id in ids if self.library_view.model().db.has_id(id)]
@ -880,6 +1078,8 @@ class DeviceGUI(object):
'the device?'), det_msg=autos):
self.auto_convert_news(auto, format)
files = [f for f in files if f is not None]
for f in files:
f.deleted_after_upload = del_on_upload
if not files:
dynamic.set('news_to_be_synced', set([]))
return
@ -897,8 +1097,7 @@ class DeviceGUI(object):
'rb').read())
dynamic.set('news_to_be_synced', set([]))
if config['upload_news_to_device'] and files:
remove = ids if \
config['delete_news_from_library_on_upload'] else []
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' }
@ -906,7 +1105,7 @@ class DeviceGUI(object):
self.upload_books(files, names, metadata,
on_card=on_card,
memory=[files, remove])
self.status_bar.showMessage(_('Sending news to device.'), 5000)
self.status_bar.show_message(_('Sending news to device.'), 5000)
def sync_to_device(self, on_card, delete_from_library,
@ -960,7 +1159,7 @@ class DeviceGUI(object):
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
remove = remove_ids if delete_from_library else []
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
self.status_bar.showMessage(_('Sending books to device.'), 5000)
self.status_bar.show_message(_('Sending books to device.'), 5000)
auto = []
if _auto_ids != []:
@ -1135,6 +1334,13 @@ class DeviceGUI(object):
in cache['authors']:
loc[i] = True
continue
# Also check author sort, because it can be used as author in
# some formats
if mi.author_sort and \
re.sub('(?u)\W|[_]', '', mi.author_sort.lower()) \
in cache['authors']:
loc[i] = True
continue
return loc
def set_books_in_library(self, booklists, reset=False):
@ -1147,10 +1353,16 @@ class DeviceGUI(object):
mi = db.get_metadata(id, index_is_id=True)
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
if title not in self.db_book_title_cache:
self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}}
authors = authors_to_string(mi.authors).lower() if mi.authors else ''
authors = re.sub('(?u)\W|[_]', '', authors)
self.db_book_title_cache[title]['authors'][authors] = mi
self.db_book_title_cache[title] = \
{'authors':{}, 'author_sort':{}, 'db_ids':{}}
if mi.authors:
authors = authors_to_string(mi.authors).lower()
authors = re.sub('(?u)\W|[_]', '', authors)
self.db_book_title_cache[title]['authors'][authors] = mi
if mi.author_sort:
aus = mi.author_sort.lower()
aus = re.sub('(?u)\W|[_]', '', aus)
self.db_book_title_cache[title]['author_sort'][aus] = mi
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
self.db_book_uuid_cache.add(mi.uuid)
@ -1181,12 +1393,19 @@ class DeviceGUI(object):
book.smart_update(d['db_ids'][book.db_id])
resend_metadata = True
continue
book_authors = authors_to_string(book.authors).lower() if book.authors else ''
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
if book_authors in d['authors']:
book.in_library = True
book.smart_update(d['authors'][book_authors])
resend_metadata = True
if book.authors:
# Compare against both author and author sort, because
# either can appear as the author
book_authors = authors_to_string(book.authors).lower()
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
if book_authors in d['authors']:
book.in_library = True
book.smart_update(d['authors'][book_authors])
resend_metadata = True
elif book_authors in d['author_sort']:
book.in_library = True
book.smart_update(d['author_sort'][book_authors])
resend_metadata = True
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
@ -1197,3 +1416,6 @@ class DeviceGUI(object):
# Correct the metadata cache on device.
if self.device_manager.is_device_connected:
self.device_manager.sync_booklists(None, booklists)
# }}}

View File

@ -49,12 +49,12 @@ class BookInfo(QDialog, Ui_BookInfo):
def open_book_path(self, path):
if os.sep in unicode(path):
QDesktopServices.openUrl(QUrl('file:'+path))
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
else:
format = unicode(path)
path = self.view.model().db.format_abspath(self.current_row, format)
if path is not None:
QDesktopServices.openUrl(QUrl('file:'+path))
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def next(self):
@ -121,6 +121,7 @@ class BookInfo(QDialog, Ui_BookInfo):
f = f.strip()
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
for key in info.keys():
if key == 'id': continue
txt = info[key]
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)

View File

@ -483,6 +483,14 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.port.editingFinished.connect(self.check_port_value)
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
True))
li = None
for i, z in enumerate([('wide', _('Wide')),
('narrow', _('Narrow'))]):
x, y = z
self.opt_gui_layout.addItem(y, QVariant(x))
if x == config['gui_layout']:
li = i
self.opt_gui_layout.setCurrentIndex(li)
def check_port_value(self, *args):
port = self.port.value()
@ -863,6 +871,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
if self.viewer.item(i).checkState() == Qt.Checked:
fmts.append(str(self.viewer.item(i).text()))
config['internally_viewed_formats'] = fmts
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'),

View File

@ -89,8 +89,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>608</width>
<height>683</height>
<width>604</width>
<height>679</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_7">
@ -332,7 +332,7 @@
</widget>
<widget class="QWidget" name="page">
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<item row="1" column="0">
<widget class="QCheckBox" name="roman_numerals">
<property name="text">
<string>Use &amp;Roman numerals for series number</string>
@ -342,35 +342,35 @@
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QCheckBox" name="systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="2" column="1">
<widget class="QCheckBox" name="systray_notifications">
<property name="text">
<string>Show &amp;notifications in system tray</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="show_splash_screen">
<property name="text">
<string>Show &amp;splash screen at startup</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QCheckBox" name="search_as_you_type">
<property name="text">
<string>Search as you type</string>
@ -380,21 +380,21 @@
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news">
<property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news">
<property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<item row="8" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_6">
@ -411,7 +411,7 @@
</item>
</layout>
</item>
<item row="8" column="0" colspan="2">
<item row="9" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Toolbar</string>
@ -459,7 +459,7 @@
</layout>
</widget>
</item>
<item row="9" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QGroupBox" name="groupBox">
@ -625,6 +625,26 @@
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>User Interface &amp;layout (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_gui_layout</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_gui_layout">
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_6">

View File

@ -357,7 +357,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
aus = self.db.author_sort(row)
self.author_sort.setText(aus if aus else '')
tags = self.db.tags(row)
self.tags.setText(', '.join(tags.split(',')) if tags else '')
self.original_tags = ', '.join(tags.split(',')) if tags else ''
self.tags.setText(self.original_tags)
self.tags.update_tags_cache(self.db.all_tags())
rating = self.db.rating(row)
if rating > 0:
@ -527,6 +528,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.publisher.setCurrentIndex(idx)
def edit_tags(self):
if self.tags.text() != self.original_tags:
error_dialog(self, _('Cannot use tag editor'),
_('The tags editor cannot be used if you have modified the tags')).exec_()
return
d = TagEditor(self, self.db, self.row)
d.exec_()
if d.result() == QDialog.Accepted:

424
src/calibre/gui2/init.py Normal file
View File

@ -0,0 +1,424 @@
#!/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 functools
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
QWidget, QHBoxLayout, QToolBar, QSize, QSizePolicy
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import isosx, __appname__
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
from calibre.gui2.status import StatusBar, HStatusBar
from calibre.gui2.book_details import BookDetails
_keep_refs = []
def partial(*args, **kwargs):
ans = functools.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):
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))
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.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()
ap = self.action_preferences
pm.addAction(ap)
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)
for x in ('news', 'edit', 'sync', 'convert', 'save', 'add', 'view',
'del', 'preferences'):
w = self.tool_bar.widgetForAction(getattr(self, 'action_'+x))
w.setPopupMode(w.MenuButtonPopup)
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
for ch in self.tool_bar.children():
if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor)
self.tool_bar.contextMenuEvent = self.no_op
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):
similar_menu = QMenu(_('Similar books...'))
similar_menu.addAction(self.action_books_by_same_author)
similar_menu.addAction(self.action_books_in_this_series)
similar_menu.addAction(self.action_books_with_the_same_tags)
similar_menu.addAction(self.action_books_by_this_publisher)
self.action_books_by_same_author.setShortcut(Qt.ALT + Qt.Key_A)
self.action_books_in_this_series.setShortcut(Qt.ALT + Qt.Key_S)
self.action_books_by_this_publisher.setShortcut(Qt.ALT + Qt.Key_P)
self.action_books_with_the_same_tags.setShortcut(Qt.ALT+Qt.Key_T)
self.addAction(self.action_books_by_same_author)
self.addAction(self.action_books_by_this_publisher)
self.addAction(self.action_books_in_this_series)
self.addAction(self.action_books_with_the_same_tags)
self.similar_menu = similar_menu
self.action_books_by_same_author.triggered.connect(
partial(self.show_similar_books, 'authors'))
self.action_books_in_this_series.triggered.connect(
partial(self.show_similar_books, 'series'))
self.action_books_with_the_same_tags.triggered.connect(
partial(self.show_similar_books, 'tag'))
self.action_books_by_this_publisher.triggered.connect(
partial(self.show_similar_books, 'publisher'))
self.library_view.set_context_menu(self.action_edit, self.action_sync,
self.action_convert, self.action_view,
self.action_save,
self.action_open_containing_folder,
self.action_show_book_details,
self.action_del,
similar_menu=similar_menu)
self.memory_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.card_a_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
for func, args in [
('connect_to_search_box', (self.search,
self.search_done)),
('connect_to_book_display',
(self.book_details.show_data,)),
]:
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
getattr(view, func)(*args)
self.memory_view.connect_dirtied_signal(self.upload_booklists)
self.card_a_view.connect_dirtied_signal(self.upload_booklists)
self.card_b_view.connect_dirtied_signal(self.upload_booklists)
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
self.library_view.model().set_book_on_device_func(self.book_on_device)
prefs['library_path'] = self.library_path
for view in ('library', 'memory', 'card_a', 'card_b'):
view = getattr(self, view+'_view')
view.verticalHeader().sectionDoubleClicked.connect(self.view_specific_book)
def show_similar_books(self, type, *args):
search, join = [], ' '
idx = self.library_view.currentIndex()
if not idx.isValid():
return
row = idx.row()
if type == 'series':
series = idx.model().db.series(row)
if series:
search = ['series:"'+series+'"']
elif type == 'publisher':
publisher = idx.model().db.publisher(row)
if publisher:
search = ['publisher:"'+publisher+'"']
elif type == 'tag':
tags = idx.model().db.tags(row)
if tags:
search = ['tag:"='+t+'"' for t in tags.split(',')]
elif type in ('author', 'authors'):
authors = idx.model().db.authors(row)
if authors:
search = ['author:"='+a.strip().replace('|', ',')+'"' \
for a in authors.split(',')]
join = ' or '
if search:
self.search.set_search_string(join.join(search))
def search_done(self, view, ok):
if view is self.current_view():
self.search.search_done(ok)
self.set_number_of_books_shown()
# }}}
class LibraryWidget(Splitter): # {{{
def __init__(self, parent):
orientation = Qt.Vertical
if config['gui_layout'] == 'narrow':
orientation = Qt.Horizontal if is_widescreen() else Qt.Vertical
idx = 0 if orientation == Qt.Vertical else 1
size = 300 if orientation == Qt.Vertical else 550
Splitter.__init__(self, 'cover_browser_splitter', _('Cover Browser'),
I('cover_flow.svg'),
orientation=orientation, parent=parent,
connect_button=not config['separate_cover_flow'],
side_index=idx, initial_side_size=size, initial_show=False)
parent.library_view = BooksView(parent)
parent.library_view.setObjectName('library_view')
self.addWidget(parent.library_view)
# }}}
class Stack(QStackedWidget): # {{{
def __init__(self, parent):
QStackedWidget.__init__(self, parent)
parent.cb_splitter = LibraryWidget(parent)
self.tb_widget = TagBrowserWidget(parent)
parent.tb_splitter = Splitter('tag_browser_splitter',
_('Tag Browser'), I('tags.svg'),
parent=parent, side_index=0, initial_side_size=200)
parent.tb_splitter.addWidget(self.tb_widget)
parent.tb_splitter.addWidget(parent.cb_splitter)
parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False)
self.addWidget(parent.tb_splitter)
for x in ('memory', 'card_a', 'card_b'):
name = x+'_view'
w = DeviceBooksView(parent)
setattr(parent, name, w)
self.addWidget(w)
w.setObjectName(name)
# }}}
class SideBar(QToolBar): # {{{
def __init__(self, splitters, jobs_button, parent=None):
QToolBar.__init__(self, _('Side bar'), parent)
self.setOrientation(Qt.Vertical)
self.setMovable(False)
self.setFloatable(False)
self.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.setIconSize(QSize(48, 48))
self.spacer = QWidget(self)
self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
for s in splitters:
self.addWidget(s.button)
self.addWidget(self.spacer)
self.addWidget(jobs_button)
for ch in self.children():
if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor)
# }}}
class LayoutMixin(object): # {{{
def __init__(self):
self.setupUi(self)
self.setWindowTitle(__appname__)
if config['gui_layout'] == 'narrow':
self.status_bar = self.book_details = StatusBar(self)
self.stack = Stack(self)
self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.svg'),
orientation=Qt.Vertical, parent=self, side_index=1)
self._layout_mem = [QWidget(self), QHBoxLayout()]
self._layout_mem[0].setLayout(self._layout_mem[1])
l = self._layout_mem[1]
l.addWidget(self.stack)
self.sidebar = SideBar([getattr(self, x+'_splitter')
for x in ('bd', 'tb', 'cb')], self.jobs_button, parent=self)
l.addWidget(self.sidebar)
self.bd_splitter.addWidget(self._layout_mem[0])
self.bd_splitter.addWidget(self.status_bar)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
self.centralwidget.layout().addWidget(self.bd_splitter)
else:
self.status_bar = HStatusBar(self)
self.setStatusBar(self.status_bar)
self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.svg'), initial_side_size=200,
orientation=Qt.Horizontal, parent=self, side_index=1)
self.stack = Stack(self)
self.bd_splitter.addWidget(self.stack)
self.book_details = BookDetails(self)
self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
self.bd_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding))
self.centralwidget.layout().addWidget(self.bd_splitter)
for x in ('cb', 'tb', 'bd'):
button = getattr(self, x+'_splitter').button
button.setIconSize(QSize(22, 22))
self.status_bar.addPermanentWidget(button)
self.status_bar.addPermanentWidget(self.jobs_button)
def finalize_layout(self):
m = self.library_view.model()
if m.rowCount(None) > 0:
self.library_view.set_current_row(0)
m.current_changed(self.library_view.currentIndex(),
self.library_view.currentIndex())
def save_layout_state(self):
for x in ('library', 'memory', 'card_a', 'card_b'):
getattr(self, x+'_view').save_state()
for x in ('cb', 'tb', 'bd'):
getattr(self, x+'_splitter').save_state()
def read_layout_settings(self):
# View states are restored automatically when set_database is called
for x in ('cb', 'tb', 'bd'):
getattr(self, x+'_splitter').restore_state()
# }}}

View File

@ -7,11 +7,14 @@ __docformat__ = 'restructuredtext en'
Job management.
'''
import re
from Queue import Empty, Queue
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
QTimer, SIGNAL, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip
QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \
QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication
from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob
@ -20,9 +23,13 @@ from calibre.gui2.device import DeviceJob
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
from calibre import __appname__
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
from calibre.gui2.progress_indicator import ProgressIndicator
class JobManager(QAbstractTableModel):
job_added = pyqtSignal(int)
job_done = pyqtSignal(int)
def __init__(self):
QAbstractTableModel.__init__(self)
self.wait_icon = QVariant(QIcon(I('jobs.svg')))
@ -37,8 +44,7 @@ class JobManager(QAbstractTableModel):
self.changed_queue = Queue()
self.timer = QTimer(self)
self.connect(self.timer, SIGNAL('timeout()'), self.update,
Qt.QueuedConnection)
self.timer.timeout.connect(self.update, type=Qt.QueuedConnection)
self.timer.start(1000)
def columnCount(self, parent=QModelIndex()):
@ -130,8 +136,7 @@ class JobManager(QAbstractTableModel):
for i, j in enumerate(self.jobs):
if j.run_state == j.RUNNING:
idx = self.index(i, 3)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
idx, idx)
self.dataChanged.emit(idx, idx)
# Update parallel jobs
jobs = set([])
@ -157,20 +162,19 @@ class JobManager(QAbstractTableModel):
self.jobs.sort()
self.reset()
if job.is_finished:
self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs()))
self.job_done.emit(len(self.unfinished_jobs()))
else:
for job in jobs:
idx = self.jobs.index(job)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
self.dataChanged.emit(
self.index(idx, 0), self.index(idx, 3))
def _add_job(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()'))
self.layoutAboutToBeChanged.emit()
self.jobs.append(job)
self.jobs.sort()
self.emit(SIGNAL('job_added(int)'), len(self.unfinished_jobs()))
self.emit(SIGNAL('layoutChanged()'))
self.job_added.emit(len(self.unfinished_jobs()))
def done_jobs(self):
return [j for j in self.jobs if j.is_finished]
@ -266,6 +270,80 @@ class DetailView(QDialog, Ui_Dialog):
if more:
self.log.appendPlainText(more.decode('utf-8', 'replace'))
class JobsButton(QFrame):
def __init__(self, horizontal=False, size=48, parent=None):
QFrame.__init__(self, parent)
if horizontal:
size = 24
self.pi = ProgressIndicator(self, size)
self._jobs = QLabel('<b>'+_('Jobs:')+' 0')
self._jobs.mouseReleaseEvent = self.mouseReleaseEvent
if horizontal:
self.setLayout(QHBoxLayout())
self.layout().setDirection(self.layout().RightToLeft)
else:
self.setLayout(QVBoxLayout())
self._jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom)
self.layout().addWidget(self.pi)
self.layout().addWidget(self._jobs)
if not horizontal:
self.layout().setAlignment(self._jobs, Qt.AlignHCenter)
self._jobs.setMargin(0)
self.layout().setMargin(0)
self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.setCursor(Qt.PointingHandCursor)
self.setToolTip(_('Click to see list of active jobs.'))
def initialize(self, jobs_dialog, job_manager):
self.jobs_dialog = jobs_dialog
job_manager.job_added.connect(self.job_added)
job_manager.job_done.connect(self.job_done)
def mouseReleaseEvent(self, event):
if self.jobs_dialog.isVisible():
self.jobs_dialog.hide()
else:
self.jobs_dialog.show()
@property
def is_running(self):
return self.pi.isAnimated()
def start(self):
self.pi.startAnimation()
def stop(self):
self.pi.stopAnimation()
def jobs(self):
src = unicode(self._jobs.text())
return int(re.search(r'\d+', src).group())
def job_added(self, nnum):
jobs = self._jobs
src = unicode(jobs.text())
num = self.jobs()
text = src.replace(str(num), str(nnum))
jobs.setText(text)
self.start()
def job_done(self, nnum):
jobs = self._jobs
src = unicode(jobs.text())
num = self.jobs()
text = src.replace(str(num), str(nnum))
jobs.setText(text)
if nnum == 0:
self.no_more_jobs()
def no_more_jobs(self):
if self.is_running:
self.stop()
QCoreApplication.instance().alert(self, 5000)
class JobsDialog(QDialog, Ui_JobsDialog):
@ -278,14 +356,9 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.model = model
self.setWindowModality(Qt.NonModal)
self.setWindowTitle(__appname__ + _(' - Jobs'))
self.connect(self.kill_button, SIGNAL('clicked()'),
self.kill_job)
self.connect(self.details_button, SIGNAL('clicked()'),
self.show_details)
self.connect(self.stop_all_jobs_button, SIGNAL('clicked()'),
self.kill_all_jobs)
self.connect(self, SIGNAL('kill_job(int, PyQt_PyObject)'),
self.jobs_view.model().kill_job)
self.kill_button.clicked.connect(self.kill_job)
self.details_button.clicked.connect(self.show_details)
self.stop_all_jobs_button.clicked.connect(self.kill_all_jobs)
self.pb_delegate = ProgressBarDelegate(self)
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
self.jobs_view.doubleClicked.connect(self.show_job_details)
@ -304,18 +377,18 @@ class JobsDialog(QDialog, Ui_JobsDialog):
d.exec_()
d.timer.stop()
def kill_job(self):
def kill_job(self, *args):
for index in self.jobs_view.selectedIndexes():
row = index.row()
self.model.kill_job(row, self)
return
def show_details(self):
def show_details(self, *args):
for index in self.jobs_view.selectedIndexes():
self.show_job_details(index)
return
def kill_all_jobs(self):
def kill_all_jobs(self, *args):
self.model.kill_all_jobs()
def closeEvent(self, e):

View File

@ -21,7 +21,7 @@ from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
from calibre import strftime, isbytestring
from calibre import strftime, isbytestring, prepare_string_for_xml
from calibre.constants import filesystem_encoding
from calibre.gui2.library import DEFAULT_SORT
@ -39,6 +39,7 @@ class FormatPath(unicode):
def __new__(cls, path, orig_file_path):
ans = unicode.__new__(cls, path)
ans.orig_file_path = orig_file_path
ans.deleted_after_upload = False
return ans
class BooksModel(QAbstractTableModel): # {{{
@ -199,7 +200,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.count_changed()
self.clear_caches()
self.reset()
return ids
def delete_books_by_id(self, ids):
for id in ids:
@ -299,6 +300,7 @@ class BooksModel(QAbstractTableModel): # {{{
formats = _('None')
data[_('Formats')] = formats
data[_('Path')] = self.db.abspath(idx)
data['id'] = self.id(idx)
comments = self.db.comments(idx)
if not comments:
comments = _('None')
@ -307,7 +309,9 @@ class BooksModel(QAbstractTableModel): # {{{
if series:
sidx = self.db.series_index(idx)
sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series)
data[_('Series')] = \
_('Book <font face="serif">%s</font> of %s.')%\
(sidx, prepare_string_for_xml(series))
return data
@ -881,6 +885,15 @@ class DeviceBooksModel(BooksModel): # {{{
ans.extend(v)
return ans
def clear_ondevice(self, db_ids):
for data in self.db:
if data is None:
continue
app_id = getattr(data, 'application_id', None)
if app_id is not None and app_id in db_ids:
data.in_library = False
self.reset()
def flags(self, index):
if self.map[index.row()] in self.indices_to_be_deleted():
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python

View File

@ -26,6 +26,15 @@ class BooksView(QTableView): # {{{
def __init__(self, parent, modelcls=BooksModel):
QTableView.__init__(self, parent)
self.setDragEnabled(True)
self.setDragDropOverwriteMode(False)
self.setDragDropMode(self.DragDrop)
self.setAlternatingRowColors(True)
self.setSelectionBehavior(self.SelectRows)
self.setShowGrid(False)
self.setWordWrap(False)
self.rating_delegate = RatingDelegate(self)
self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self)
@ -283,7 +292,8 @@ class BooksView(QTableView): # {{{
old_state['column_positions'][name] = i
if name != 'ondevice':
old_state['column_sizes'][name] = \
max(self.sizeHintForColumn(i), h.sectionSizeHint(i))
min(350, max(self.sizeHintForColumn(i),
h.sectionSizeHint(i)))
if name == 'timestamp':
old_state['column_sizes'][name] += 12
return old_state
@ -434,6 +444,18 @@ class BooksView(QTableView): # {{{
self.scrollTo(self.model().index(row, i))
break
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):
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()

View File

@ -53,7 +53,24 @@ def init_qt(args):
app.setWindowIcon(QIcon(I('library.png')))
return app, opts, args, actions
def get_library_path():
def get_default_library_path():
fname = _('Calibre Library')
if isinstance(fname, unicode):
try:
fname = fname.encode(filesystem_encoding)
except:
fname = 'Calibre Library'
x = os.path.expanduser('~'+os.sep+fname)
if not os.path.exists(x):
try:
os.makedirs(x)
except:
x = os.path.expanduser('~')
return x
def get_library_path(parent=None):
library_path = prefs['library_path']
if library_path is None: # Need to migrate to new database layout
base = os.path.expanduser('~')
@ -73,10 +90,12 @@ def get_library_path():
try:
os.makedirs(library_path)
except:
error_dialog(None, _('Failed to create library'),
_('Failed to create calibre library at: %r. Aborting.')%library_path,
error_dialog(parent, _('Failed to create library'),
_('Failed to create calibre library at: %r.')%library_path,
det_msg=traceback.format_exc(), show=True)
library_path = None
library_path = choose_dir(parent, 'choose calibre library',
_('Choose a location for your new calibre e-book library'),
default_dir=get_default_library_path())
return library_path
class DBRepair(QThread):
@ -159,22 +178,9 @@ class GuiRunner(QObject):
'a new empty library.'),
det_msg=tb, show=True)
if db is None:
fname = _('Calibre Library')
if isinstance(fname, unicode):
try:
fname = fname.encode(filesystem_encoding)
except:
fname = 'Calibre Library'
x = os.path.expanduser('~'+os.sep+fname)
if not os.path.exists(x):
try:
os.makedirs(x)
except:
x = os.path.expanduser('~')
candidate = choose_dir(self.splash_screen, 'choose calibre library',
_('Choose a location for your new calibre e-book library'),
default_dir=x)
default_dir=get_default_library_path())
if not candidate:
self.initialization_failed()
@ -236,8 +242,8 @@ class GuiRunner(QObject):
if gprefs.get('show_splash_screen', True):
self.show_splash_screen()
self.library_path = get_library_path()
if self.library_path is None:
self.library_path = get_library_path(parent=self.splash_screen)
if not self.library_path:
self.initialization_failed()
self.initialize_db()

View File

@ -169,6 +169,12 @@
</item>
<item>
<widget class="QComboBox" name="search_restriction">
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Books display will be restricted to those matching the selected saved search</string>
</property>
@ -304,270 +310,6 @@
</item>
</layout>
</item>
<item>
<widget class="Splitter" name="vertical_splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QWidget" name="layoutWidget">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QStackedWidget" name="stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="library">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="Splitter" name="horizontal_splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="TagsView" name="tags_view">
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="animated">
<bool>true</bool>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="popularity">
<property name="text">
<string>Sort by &amp;popularity</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="tag_match">
<property name="currentIndex">
<number>0</number>
</property>
<item>
<property name="text">
<string>Match any</string>
</property>
</item>
<item>
<property name="text">
<string>Match all</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QPushButton" name="edit_categories">
<property name="toolTip">
<string>Create, edit, and delete user categories</string>
</property>
<property name="text">
<string>Manage &amp;user categories</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="cb_layout">
<item>
<widget class="BooksView" name="library_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="main_memory">
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="DeviceBooksView" name="memory_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="card_a_memory">
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="DeviceBooksView" name="card_a_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>10</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="card_b_memory">
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="DeviceBooksView" name="card_b_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>10</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="SideBar" name="sidebar" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="StatusBar" name="status_bar" native="true"/>
</widget>
</item>
</layout>
</widget>
<widget class="QToolBar" name="tool_bar">
@ -804,26 +546,11 @@
</action>
</widget>
<customwidgets>
<customwidget>
<class>BooksView</class>
<extends>QTableView</extends>
<header>calibre/gui2/library/views.h</header>
</customwidget>
<customwidget>
<class>LocationView</class>
<extends>QListView</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>DeviceBooksView</class>
<extends>QTableView</extends>
<header>calibre/gui2/library/views.h</header>
</customwidget>
<customwidget>
<class>TagsView</class>
<extends>QTreeView</extends>
<header>calibre/gui2/tag_view.h</header>
</customwidget>
<customwidget>
<class>SearchBox2</class>
<extends>QComboBox</extends>
@ -834,24 +561,6 @@
<extends>QComboBox</extends>
<header>calibre.gui2.search_box</header>
</customwidget>
<customwidget>
<class>StatusBar</class>
<extends>QWidget</extends>
<header>calibre/gui2/status.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>Splitter</class>
<extends>QSplitter</extends>
<header>calibre/gui2/widgets.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>SideBar</class>
<extends>QWidget</extends>
<header>calibre/gui2/sidebar.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../../resources/images.qrc"/>

View File

@ -85,6 +85,10 @@ typedef long PFreal;
typedef unsigned short QRgb565;
#define REFLECTION_FACTOR 1.5
#define MAX(x, y) ((x > y) ? x : y)
#define RGB565_RED_MASK 0xF800
#define RGB565_GREEN_MASK 0x07E0
#define RGB565_BLUE_MASK 0x001F
@ -122,6 +126,7 @@ inline PFreal floatToFixed(float val)
return (PFreal)(val*PFREAL_ONE);
}
// sinTable {{{
#define IANGLE_MAX 1024
#define IANGLE_MASK 1023
@ -291,6 +296,7 @@ int main(int, char**)
return 0;
}
#endif
// }}}
inline PFreal fsin(int iangle)
{
@ -313,6 +319,8 @@ struct SlideInfo
PFreal cy;
};
// PicturePlowPrivate {{{
class PictureFlowPrivate
{
public:
@ -367,6 +375,7 @@ private:
int slideWidth;
int slideHeight;
int fontSize;
int zoom;
int queueLength;
@ -404,6 +413,7 @@ PictureFlowPrivate::PictureFlowPrivate(PictureFlow* w, int queueLength_)
slideWidth = 200;
slideHeight = 200;
fontSize = 10;
zoom = 100;
centerIndex = 0;
@ -540,6 +550,11 @@ void PictureFlowPrivate::showSlide(int index)
void PictureFlowPrivate::resize(int w, int h)
{
if (w < 10) w = 10;
if (h < 10) h = 10;
slideHeight = int(float(h)/REFLECTION_FACTOR);
slideWidth = int(float(slideHeight) * 2/3.);
fontSize = MAX(int(h/15.), 12);
recalc(w, h);
resetSlides();
triggerRender();
@ -588,8 +603,8 @@ static QImage prepareSurface(QImage img, int w, int h)
img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
// slightly larger, to accomodate for the reflection
int hs = h * 2;
int hofs = h / 3;
int hs = int(h * REFLECTION_FACTOR);
int hofs = 0;
// offscreen buffer: black is sweet
QImage result(hs, w, QImage::Format_RGB16);
@ -709,14 +724,15 @@ void PictureFlowPrivate::render()
QPainter painter;
painter.begin(&buffer);
QFont font("Arial", 14);
QFont font = QFont();
font.setBold(true);
font.setPixelSize(fontSize);
painter.setFont(font);
painter.setPen(Qt::white);
//painter.setPen(QColor(255,255,255,127));
if (centerIndex < slideCount() && centerIndex > -1)
painter.drawText( QRect(0,0, buffer.width(), (buffer.height() - slideSize().height())/2),
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*3),
Qt::AlignCenter, slideImages->caption(centerIndex));
painter.end();
@ -759,8 +775,9 @@ void PictureFlowPrivate::render()
QPainter painter;
painter.begin(&buffer);
QFont font("Arial", 14);
QFont font = QFont();
font.setBold(true);
font.setPixelSize(fontSize);
painter.setFont(font);
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
@ -768,12 +785,12 @@ void PictureFlowPrivate::render()
painter.setPen(QColor(255,255,255, (255-fade) ));
if (leftTextIndex < sc && leftTextIndex > -1)
painter.drawText( QRect(0,0, buffer.width(), (buffer.height() - slideSize().height())/2),
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
Qt::AlignCenter, slideImages->caption(leftTextIndex));
painter.setPen(QColor(255,255,255, fade));
if (leftTextIndex+1 < sc && leftTextIndex > -2)
painter.drawText( QRect(0,0, buffer.width(), (buffer.height() - slideSize().height())/2),
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
@ -887,7 +904,7 @@ int col1, int col2)
int center = (sh*BILINEAR_STRETCH_VER/2);
int dy = dist*BILINEAR_STRETCH_VER / h;
#else
int center = (sh/2);
int center = sh/2;
int dy = dist / h;
#endif
int p1 = center*PFREAL_ONE - dy/2;
@ -1104,8 +1121,9 @@ void PictureFlowPrivate::clearSurfaceCache()
surfaceCache.clear();
}
// -----------------------------------------
// }}}
// PictureFlow {{{
PictureFlow::PictureFlow(QWidget* parent, int queueLength): QWidget(parent)
{
d = new PictureFlowPrivate(this, queueLength);
@ -1381,3 +1399,5 @@ void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); }
int FlowImages::count() { return 0; }
QImage FlowImages::image(int index) { index=0; return QImage(); }
QString FlowImages::caption(int index) {index=0; return QString(); }
// }}}

View File

@ -115,7 +115,8 @@ public:
QSize slideSize() const;
/*!
Sets the dimension of each slide (in pixels).
Sets the dimension of each slide (in pixels). Do not use this method directly
instead use resize which automatically sets an appropriate slide size.
*/
void setSlideSize(QSize size);

View File

@ -16,6 +16,10 @@ public:
virtual int count();
virtual QImage image(int index);
virtual QString caption(int index);
signals:
void dataChanged();
};

View File

@ -7,11 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
pyqtSignal, SIGNAL
from PyQt4.QtGui import QCompleter
pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \
QAction, QKeySequence
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):
@ -69,7 +73,7 @@ class SearchBox2(QComboBox):
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
self.mouse_released, Qt.DirectConnection)
self.setEditable(True)
self.help_state = True
self.help_state = False
self.as_you_type = True
self.prev_search = ''
self.timer = None
@ -79,8 +83,7 @@ class SearchBox2(QComboBox):
self.setMinimumContentsLength(25)
self._in_a_search = False
def initialize(self, opt_name, colorize=False,
help_text=_('Search')):
def initialize(self, opt_name, colorize=False, help_text=_('Search')):
self.as_you_type = config['search_as_you_type']
self.opt_name = opt_name
self.addItems(QStringList(list(set(config[opt_name]))))
@ -97,6 +100,9 @@ class SearchBox2(QComboBox):
self.help_state = False
def clear_to_help(self):
if self.help_state:
return
self.help_state = True
self.search.emit('')
self._in_a_search = False
self.setEditText(self.help_text)
@ -104,7 +110,6 @@ class SearchBox2(QComboBox):
self.killTimer(self.timer)
self.timer = None
self.line_edit.home(False)
self.help_state = True
self.line_edit.setStyleSheet(
'QLineEdit { color: gray; background-color: %s; }' %
self.normal_background)
@ -239,9 +244,9 @@ class SavedSearchBox(QComboBox):
self.setInsertPolicy(self.NoInsert)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.setMinimumContentsLength(10)
self.tool_tip_text = self.toolTip()
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
self.tool_tip_text = self.toolTip()
self.saved_searches = _saved_searches
self.search_box = _search_box
self.help_text = help_text
@ -331,3 +336,75 @@ class SavedSearchBox(QComboBox):
if idx < 0:
return
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
class SearchBoxMixin(object):
def __init__(self):
self.search.initialize('main_search_history', colorize=True,
help_text=_('Search (For Advanced Search click the button to the left)'))
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
self.do_advanced_search)
self.search.clear()
self.search.setMaximumWidth(self.width()-150)
self.action_focus_search = QAction(self)
shortcuts = QKeySequence.keyBindings(QKeySequence.Find)
shortcuts = list(shortcuts) + [QKeySequence('/')]
self.action_focus_search.setShortcuts(shortcuts)
self.action_focus_search.triggered.connect(lambda x:
self.search.setFocus(Qt.OtherFocusReason))
self.addAction(self.action_focus_search)
def search_box_cleared(self):
self.tags_view.clear()
self.saved_search.clear_to_help()
self.set_number_of_books_shown()
def do_advanced_search(self, *args):
d = SearchDialog(self)
if d.exec_() == QDialog.Accepted:
self.search.set_search_string(d.search_string())
class SavedSearchBoxMixin(object):
def __init__(self):
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,
help_text=_('Saved Searches'))
self.connect(self.save_search_button, SIGNAL('clicked()'),
self.saved_search.save_search_button_clicked)
self.connect(self.delete_search_button, SIGNAL('clicked()'),
self.saved_search.delete_search_button_clicked)
self.connect(self.copy_search_button, SIGNAL('clicked()'),
self.saved_search.copy_search_button_clicked)
def saved_searches_changed(self):
p = prefs['saved_searches'].keys()
p.sort()
t = unicode(self.search_restriction.currentText())
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
self.search_restriction.addItem('')
self.tags_view.recount()
for s in p:
self.search_restriction.addItem(s)
if t:
if t in p: # redo the current restriction, if there was one
self.search_restriction.setCurrentIndex(self.search_restriction.findText(t))
# self.tags_view.set_search_restriction(t)
else:
self.search_restriction.setCurrentIndex(0)
self.apply_search_restriction('')
def do_saved_search_edit(self, search):
d = SavedSearchEditor(self, search)
d.exec_()
if d.result() == d.Accepted:
self.saved_searches_changed()
self.saved_search.clear_to_help()

View File

@ -0,0 +1,57 @@
'''
Created on 10 Jun 2010
@author: charles
'''
class SearchRestrictionMixin(object):
def __init__(self):
self.search_restriction.activated[str].connect(self.apply_search_restriction)
self.library_view.model().count_changed_signal.connect(self.restriction_count_changed)
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
'''
Adding and deleting books while restricted creates a complexity. When added,
they are displayed regardless of whether they match a search restriction.
However, if they do not, they are removed at the next search. The counts
must take this behavior into effect.
'''
def restriction_count_changed(self, c):
self.restriction_count_of_books_in_view += \
c - self.restriction_count_of_books_in_library
self.restriction_count_of_books_in_library = c
if self.restriction_in_effect:
self.set_number_of_books_shown()
def apply_search_restriction(self, r):
r = unicode(r)
if r is not None and r != '':
self.restriction_in_effect = True
restriction = 'search:"%s"'%(r)
else:
self.restriction_in_effect = False
restriction = ''
self.restriction_count_of_books_in_view = \
self.library_view.model().set_search_restriction(restriction)
self.search.clear_to_help()
self.saved_search.clear_to_help()
self.tags_view.set_search_restriction(restriction)
self.set_number_of_books_shown()
def set_number_of_books_shown(self):
if self.current_view() == self.library_view and self.restriction_in_effect:
t = _("({0} of {1})").format(self.current_view().row_count(),
self.restriction_count_of_books_in_view)
self.search_count.setStyleSheet \
('QLabel { border-radius: 8px; background-color: yellow; }')
else: # No restriction or not library view
if not self.search.in_a_search():
t = _("(all books)")
else:
t = _("({0} of all)").format(self.current_view().row_count())
self.search_count.setStyleSheet(
'QLabel { background-color: transparent; }')
self.search_count.setText(t)

View File

@ -1,240 +0,0 @@
#!/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 re
from functools import partial
from PyQt4.Qt import QToolBar, Qt, QIcon, QSizePolicy, QWidget, \
QFrame, QVBoxLayout, QLabel, QSize, QCoreApplication, QToolButton
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2 import dynamic
class JobsButton(QFrame):
def __init__(self, parent):
QFrame.__init__(self, parent)
self.setLayout(QVBoxLayout())
self.pi = ProgressIndicator(self)
self.layout().addWidget(self.pi)
self.jobs = QLabel('<b>'+_('Jobs:')+' 0')
self.jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom)
self.layout().addWidget(self.jobs)
self.layout().setAlignment(self.jobs, Qt.AlignHCenter)
self.jobs.setMargin(0)
self.layout().setMargin(0)
self.jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.setCursor(Qt.PointingHandCursor)
self.setToolTip(_('Click to see list of active jobs.'))
def initialize(self, jobs_dialog):
self.jobs_dialog = jobs_dialog
def mouseReleaseEvent(self, event):
if self.jobs_dialog.isVisible():
self.jobs_dialog.hide()
else:
self.jobs_dialog.show()
@property
def is_running(self):
return self.pi.isAnimated()
def start(self):
self.pi.startAnimation()
def stop(self):
self.pi.stopAnimation()
class Jobs(ProgressIndicator):
def initialize(self, jobs_dialog):
self.jobs_dialog = jobs_dialog
def mouseClickEvent(self, event):
if self.jobs_dialog.isVisible():
self.jobs_dialog.jobs_view.write_settings()
self.jobs_dialog.hide()
else:
self.jobs_dialog.jobs_view.read_settings()
self.jobs_dialog.show()
self.jobs_dialog.jobs_view.restore_column_widths()
@property
def is_running(self):
return self.isAnimated()
def start(self):
self.startAnimation()
def stop(self):
self.stopAnimation()
class SideBar(QToolBar):
toggle_texts = {
'book_info' : (_('Show Book Details'), _('Hide Book Details')),
'tag_browser' : (_('Show Tag Browser'), _('Hide Tag Browser')),
'cover_browser': (_('Show Cover Browser'), _('Hide Cover Browser')),
}
toggle_icons = {
'book_info' : 'book.svg',
'tag_browser' : 'tags.svg',
'cover_browser': 'cover_flow.svg',
}
def __init__(self, parent=None):
QToolBar.__init__(self, _('Side bar'), parent)
self.setOrientation(Qt.Vertical)
self.setMovable(False)
self.setFloatable(False)
self.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.setIconSize(QSize(48, 48))
for ac in ('book_info', 'tag_browser', 'cover_browser'):
action = self.addAction(QIcon(I(self.toggle_icons[ac])),
self.toggle_texts[ac][1], getattr(self, '_toggle_'+ac))
setattr(self, 'action_toggle_'+ac, action)
w = self.widgetForAction(action)
w.setCheckable(True)
setattr(self, 'show_'+ac, partial(getattr(self, '_toggle_'+ac),
show=True))
setattr(self, 'hide_'+ac, partial(getattr(self, '_toggle_'+ac),
show=False))
self.spacer = QWidget(self)
self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
self.addWidget(self.spacer)
self.jobs_button = JobsButton(self)
self.addWidget(self.jobs_button)
self.show_cover_browser = partial(self._toggle_cover_browser, show=True)
self.hide_cover_browser = partial(self._toggle_cover_browser,
show=False)
for ch in self.children():
if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor)
def initialize(self, jobs_dialog, cover_browser, toggle_cover_browser,
cover_browser_error, vertical_splitter, horizontal_splitter):
self.jobs_button.initialize(jobs_dialog)
self.cover_browser, self.do_toggle_cover_browser = cover_browser, \
toggle_cover_browser
if self.cover_browser is None:
self.action_toggle_cover_browser.setEnabled(False)
self.action_toggle_cover_browser.setText(
_('Cover browser could not be loaded: ') + cover_browser_error)
else:
self.cover_browser.stop.connect(self.hide_cover_browser)
self._toggle_cover_browser(dynamic.get('cover_flow_visible', False))
self.horizontal_splitter = horizontal_splitter
self.vertical_splitter = vertical_splitter
tb_state = dynamic.get('tag_browser_state', None)
if tb_state is not None:
self.horizontal_splitter.restoreState(tb_state)
tb_last_open_state = dynamic.get('tag_browser_last_open_state', None)
if tb_last_open_state is not None and \
not self.horizontal_splitter.is_side_index_hidden:
self.horizontal_splitter.restoreState(tb_last_open_state)
bi_state = dynamic.get('book_info_state', None)
if bi_state is not None:
self.vertical_splitter.restoreState(bi_state)
bi_last_open_state = dynamic.get('book_info_last_open_state', None)
if bi_last_open_state is not None and \
not self.vertical_splitter.is_side_index_hidden:
self.vertical_splitter.restoreState(bi_last_open_state)
self.horizontal_splitter.initialize(name='tag_browser')
self.vertical_splitter.initialize(name='book_info')
self.view_status_changed('book_info', not
self.vertical_splitter.is_side_index_hidden)
self.view_status_changed('tag_browser', not
self.horizontal_splitter.is_side_index_hidden)
self.vertical_splitter.state_changed.connect(partial(self.view_status_changed,
'book_info'), type=Qt.QueuedConnection)
self.horizontal_splitter.state_changed.connect(partial(self.view_status_changed,
'tag_browser'), type=Qt.QueuedConnection)
def view_status_changed(self, name, visible):
action = getattr(self, 'action_toggle_'+name)
texts = self.toggle_texts[name]
action.setText(texts[int(visible)])
w = self.widgetForAction(action)
w.setCheckable(True)
w.setChecked(visible)
def location_changed(self, location):
is_lib = location == 'library'
for ac in ('cover_browser', 'tag_browser'):
ac = getattr(self, 'action_toggle_'+ac)
ac.setEnabled(is_lib)
self.widgetForAction(ac).setVisible(is_lib)
def save_state(self):
dynamic.set('cover_flow_visible', self.is_cover_browser_visible)
dynamic.set('tag_browser_state',
str(self.horizontal_splitter.saveState()))
dynamic.set('book_info_state',
str(self.vertical_splitter.saveState()))
@property
def is_cover_browser_visible(self):
return self.cover_browser is not None and self.cover_browser.isVisible()
def _toggle_cover_browser(self, show=None):
if show is None:
show = not self.is_cover_browser_visible
self.do_toggle_cover_browser(show)
self.view_status_changed('cover_browser', show)
def external_cover_flow_finished(self, *args):
self.view_status_changed('cover_browser', False)
def _toggle_tag_browser(self, show=None):
self.horizontal_splitter.toggle_side_index()
def _toggle_book_info(self, show=None):
self.vertical_splitter.toggle_side_index()
def jobs(self):
src = unicode(self.jobs_button.jobs.text())
return int(re.search(r'\d+', src).group())
def job_added(self, nnum):
jobs = self.jobs_button.jobs
src = unicode(jobs.text())
num = self.jobs()
text = src.replace(str(num), str(nnum))
jobs.setText(text)
self.jobs_button.start()
def job_done(self, nnum):
jobs = self.jobs_button.jobs
src = unicode(jobs.text())
num = self.jobs()
text = src.replace(str(num), str(nnum))
jobs.setText(text)
if nnum == 0:
self.no_more_jobs()
def no_more_jobs(self):
if self.jobs_button.is_running:
self.jobs_button.stop()
QCoreApplication.instance().alert(self, 5000)

View File

@ -1,10 +1,12 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, collections
from PyQt4.QtGui import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
QSizePolicy, QScrollArea
from PyQt4.QtCore import Qt, QSize, pyqtSignal
import os
from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \
QPropertyAnimation, QEasingCurve, QDesktopServices, QUrl
from calibre import fit_image, preferred_encoding, isosx
from calibre.gui2 import config
@ -12,6 +14,7 @@ from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.gui2.notify import get_notifier
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.library.comments import comments_to_html
from calibre.gui2.book_details import render_rows
class BookInfoDisplay(QWidget):
@ -46,10 +49,14 @@ class BookInfoDisplay(QWidget):
event.acceptProposedAction()
class BookCoverDisplay(QLabel):
class BookCoverDisplay(QLabel): # {{{
def __init__(self, coverpath=I('book.svg')):
QLabel.__init__(self)
self.animation = QPropertyAnimation(self, 'size', self)
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
self.animation.setDuration(1000)
self.animation.setStartValue(QSize(0, 0))
self.setMaximumWidth(81)
self.setMaximumHeight(108)
self.default_pixmap = QPixmap(coverpath)
@ -58,21 +65,23 @@ class BookInfoDisplay(QWidget):
self.setPixmap(self.default_pixmap)
def do_layout(self):
self.animation.stop()
pixmap = self.pixmap()
pwidth, pheight = pixmap.width(), pixmap.height()
width, height = fit_image(pwidth, pheight,
pwidth, self.statusbar_height-12)[1:]
pwidth, self.statusbar_height-20)[1:]
self.setMaximumHeight(height)
try:
aspect_ratio = pwidth/float(pheight)
except ZeroDivisionError:
aspect_ratio = 1
self.setMaximumWidth(int(aspect_ratio*self.maximumHeight()))
self.animation.setEndValue(self.maximumSize())
def setPixmap(self, pixmap):
QLabel.setPixmap(self, pixmap)
self.do_layout()
self.animation.start()
def sizeHint(self):
return QSize(self.maximumWidth(), self.maximumHeight())
@ -81,27 +90,31 @@ class BookInfoDisplay(QWidget):
self.statusbar_height = statusbar_size.height()
self.do_layout()
# }}}
class BookDataDisplay(QLabel):
mr = pyqtSignal(int)
mr = pyqtSignal(object)
link_clicked = pyqtSignal(object)
def __init__(self):
QLabel.__init__(self)
self.setText('')
self.setWordWrap(True)
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
self.linkActivated.connect(self.link_activated)
self._link_clicked = False
def mouseReleaseEvent(self, ev):
self.mr.emit(1)
QLabel.mouseReleaseEvent(self, ev)
if not self._link_clicked:
self.mr.emit(ev)
self._link_clicked = False
WEIGHTS = collections.defaultdict(lambda : 100)
WEIGHTS[_('Path')] = 0
WEIGHTS[_('Formats')] = 1
WEIGHTS[_('Collections')] = 2
WEIGHTS[_('Series')] = 3
WEIGHTS[_('Tags')] = 4
WEIGHTS[_('Comments')] = 5
def link_activated(self, link):
self._link_clicked = True
link = unicode(link)
self.link_clicked.emit(link)
show_book_info = pyqtSignal()
@ -122,6 +135,7 @@ class BookInfoDisplay(QWidget):
self._layout.setAlignment(self.cover_display, Qt.AlignTop|Qt.AlignLeft)
def mouseReleaseEvent(self, ev):
ev.accept()
self.show_book_info.emit()
def show_data(self, data):
@ -133,24 +147,12 @@ class BookInfoDisplay(QWidget):
rows, comments = [], ''
self.book_data.setText('')
self.data = data.copy()
keys = data.keys()
keys.sort(cmp=lambda x, y: cmp(self.WEIGHTS[x], self.WEIGHTS[y]))
for key in keys:
txt = data[key]
if not txt or not txt.strip() or txt == 'None':
continue
if isinstance(key, str):
key = key.decode(preferred_encoding, 'replace')
if isinstance(txt, str):
txt = txt.decode(preferred_encoding, 'replace')
if key == _('Comments'):
comments = comments_to_html(txt)
else:
rows.append((key, txt))
rows = render_rows(self.data)
rows = '\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
k, t in rows])
if comments:
comments = '<b>Comments:</b>'+comments
if _('Comments') in self.data:
comments = comments_to_html(self.data[_('Comments')])
comments = ('<b>%s:</b>'%_('Comments'))+comments
left_pane = u'<table>%s</table>'%rows
right_pane = u'<div>%s</div>'%comments
self.book_data.setText(u'<table><tr><td valign="top" '
@ -162,17 +164,54 @@ class BookInfoDisplay(QWidget):
self.updateGeometry()
self.setVisible(True)
class StatusBar(QStatusBar):
resized = pyqtSignal(object)
files_dropped = pyqtSignal(object, object)
show_book_info = pyqtSignal()
class StatusBarInterface(object):
def initialize(self, systray=None):
self.systray = systray
self.notifier = get_notifier(systray)
self.book_info = BookInfoDisplay(self.clearMessage)
def show_message(self, msg, timeout=0):
QStatusBar.showMessage(self, msg, timeout)
if self.notifier is not None and not config['disable_tray_notification']:
if isosx and isinstance(msg, unicode):
try:
msg = msg.encode(preferred_encoding)
except UnicodeEncodeError:
msg = msg.encode('utf-8')
self.notifier(msg)
def clear_message(self):
QStatusBar.clearMessage(self)
class BookDetailsInterface(object):
# These signals must be defined in the class implementing this interface
files_dropped = None
show_book_info = None
open_containing_folder = None
view_specific_format = None
def reset_info(self):
raise NotImplementedError()
def show_data(self, data):
raise NotImplementedError()
class HStatusBar(QStatusBar, StatusBarInterface):
pass
class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
files_dropped = pyqtSignal(object, object)
show_book_info = pyqtSignal()
open_containing_folder = pyqtSignal(int)
view_specific_format = pyqtSignal(int, object)
resized = pyqtSignal(object)
def initialize(self, systray=None):
StatusBarInterface.initialize(self, systray=systray)
self.book_info = BookInfoDisplay(self.clear_message)
self.book_info.setAcceptDrops(True)
self.scroll_area = QScrollArea()
self.scroll_area.setWidget(self.book_info)
@ -181,26 +220,31 @@ class StatusBar(QStatusBar):
type=Qt.QueuedConnection)
self.book_info.files_dropped.connect(self.files_dropped.emit,
type=Qt.QueuedConnection)
self.book_info.book_data.link_clicked.connect(self._link_clicked)
self.addWidget(self.scroll_area, 100)
self.setMinimumHeight(120)
self.resized.connect(self.book_info.cover_display.relayout)
self.book_info.cover_display.relayout(self.size())
def _link_clicked(self, link):
typ, _, val = link.partition(':')
if typ == 'path':
self.open_containing_folder.emit(int(val))
elif typ == 'format':
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
path = os.path.dirname(val)
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def resizeEvent(self, ev):
self.resized.emit(self.size())
def reset_info(self):
self.book_info.show_data({})
def showMessage(self, msg, timeout=0):
ret = QStatusBar.showMessage(self, msg, timeout)
if self.notifier is not None and not config['disable_tray_notification']:
if isosx and isinstance(msg, unicode):
try:
msg = msg.encode(preferred_encoding)
except UnicodeEncodeError:
msg = msg.encode('utf-8')
self.notifier(msg)
return ret
def show_data(self, data):
self.book_info.show_data(data)

View File

@ -10,14 +10,18 @@ Browsing book collection by tags.
from itertools import izip
from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, QSize, QIcon, QPoint, \
QAbstractItemModel, QVariant, QModelIndex, QMenu
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
QPushButton, QWidget
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
from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
class TagsView(QTreeView): # {{{
@ -29,12 +33,16 @@ class TagsView(QTreeView): # {{{
tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal()
def __init__(self, *args):
QTreeView.__init__(self, *args)
def __init__(self, parent=None):
QTreeView.__init__(self, parent=None)
self.tag_match = None
self.setUniformRowHeights(True)
self.setCursor(Qt.PointingHandCursor)
self.setIconSize(QSize(30, 30))
self.tag_match = None
self.setTabKeyNavigation(True)
self.setAlternatingRowColors(True)
self.setAnimated(True)
self.setHeaderHidden(True)
def set_database(self, db, tag_match, popularity):
self.hidden_categories = config['tag_browser_hidden_categories']
@ -588,3 +596,71 @@ class TagsModel(QAbstractItemModel): # {{{
# }}}
class TagBrowserMixin(object): # {{{
def __init__(self, db):
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
self.tags_view.set_database(self.library_view.model().db,
self.tag_match, self.popularity)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
self.edit_categories.clicked.connect(lambda x:
self.do_user_categories_edit())
def do_user_categories_edit(self, on_category=None):
d = TagCategories(self, self.library_view.model().db, on_category)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.set_new_model()
self.tags_view.recount()
def do_tags_list_edit(self, tag, category):
d = TagListEditor(self, self.library_view.model().db, tag, category)
d.exec_()
if d.result() == d.Accepted:
# Clean up everything, as information could have changed for many books.
self.library_view.model().refresh()
self.tags_view.set_new_model()
self.tags_view.recount()
self.saved_search.clear_to_help()
self.search.clear_to_help()
def do_tag_item_renamed(self):
# Clean up library view and search
self.library_view.model().refresh()
self.saved_search.clear_to_help()
self.search.clear_to_help()
# }}}
class TagBrowserWidget(QWidget): # {{{
def __init__(self, parent):
QWidget.__init__(self, parent)
self._layout = QVBoxLayout()
self.setLayout(self._layout)
parent.tags_view = TagsView(parent)
self._layout.addWidget(parent.tags_view)
parent.popularity = QCheckBox(parent)
parent.popularity.setText(_('Sort by &popularity'))
self._layout.addWidget(parent.popularity)
parent.tag_match = QComboBox(parent)
for x in (_('Match any'), _('Match all')):
parent.tag_match.addItem(x)
parent.tag_match.setCurrentIndex(0)
self._layout.addWidget(parent.tag_match)
parent.edit_categories = QPushButton(_('Manage &user categories'), parent)
self._layout.addWidget(parent.edit_categories)
# }}}

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import traceback
from PyQt4.QtCore import QThread, pyqtSignal
from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt
import mechanize
from calibre.constants import __version__, iswindows, isosx
from calibre.constants import __appname__, __version__, iswindows, isosx
from calibre import browser
from calibre.utils.config import prefs
from calibre.gui2 import config, dynamic, question_dialog
URL = 'http://status.calibre-ebook.com/latest'
@ -36,3 +37,35 @@ class CheckForUpdates(QThread):
traceback.print_exc()
self.sleep(self.INTERVAL)
class UpdateMixin(object):
def __init__(self, opts):
if not opts.no_update_check:
self.update_checker = CheckForUpdates(self)
self.update_checker.update_found.connect(self.update_found,
type=Qt.QueuedConnection)
self.update_checker.start()
def update_found(self, version):
os = 'windows' if iswindows else 'osx' if isosx else 'linux'
url = 'http://calibre-ebook.com/download_%s'%os
self.latest_version = '<br>' + _('<span style="color:red; font-weight:bold">'
'Latest version: <a href="%s">%s</a></span>')%(url, version)
self.vanity.setText(self.vanity_template%\
(dict(version=self.latest_version,
device=self.device_info)))
self.vanity.update()
if config.get('new_version_notification') and \
dynamic.get('update to version %s'%version, True):
if question_dialog(self, _('Update available'),
_('%s has been updated to version %s. '
'See the <a href="http://calibre-ebook.com/whats-new'
'">new features</a>. Visit the download pa'
'ge?')%(__appname__, version)):
url = 'http://calibre-ebook.com/download_'+\
('windows' if iswindows else 'osx' if isosx else 'linux')
QDesktopServices.openUrl(QUrl(url))
dynamic.set('update to version %s'%version, False)

View File

@ -229,9 +229,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.connect(self.action_previous_page, SIGNAL('triggered(bool)'),
lambda x:self.view.previous_page())
self.connect(self.action_find_next, SIGNAL('triggered(bool)'),
lambda x:self.find(self.search.smart_text, True, repeat=True))
lambda x:self.find(self.search.smart_text, repeat=True))
self.connect(self.action_find_previous, SIGNAL('triggered(bool)'),
lambda x:self.find(self.search.smart_text, True,
lambda x:self.find(self.search.smart_text,
repeat=True, backwards=True))
self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),

View File

@ -4,16 +4,18 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
Miscellaneous widgets used in the GUI
'''
import re, os, traceback
from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
QListWidgetItem, QTextCharFormat, QApplication, \
QSyntaxHighlighter, QCursor, QColor, QWidget, \
QPixmap, QPalette, QSplitterHandle, \
QPixmap, QPalette, QSplitterHandle, QToolButton, \
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
QRegExp, QSettings, QSize, QModelIndex, QSplitter, \
QAbstractButton, QPainter, QLineEdit, QComboBox, \
QMenu, QStringListModel, QCompleter, QStringList
QMenu, QStringListModel, QCompleter, QStringList, \
QTimer
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, dynamic
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
from calibre.gui2.filename_pattern_ui import Ui_Form
from calibre import fit_image, human_readable
@ -927,6 +929,7 @@ class SplitterHandle(QSplitterHandle):
self.double_clicked.connect(splitter.double_clicked,
type=Qt.QueuedConnection)
self.highlight = False
self.setToolTip(_('Drag to resize')+' '+splitter.label)
def splitter_moved(self, *args):
oh = self.highlight
@ -944,20 +947,62 @@ class SplitterHandle(QSplitterHandle):
def mouseDoubleClickEvent(self, ev):
self.double_clicked.emit(self)
class LayoutButton(QToolButton):
def __init__(self, icon, text, splitter, parent=None):
QToolButton.__init__(self, parent)
self.label = text
self.setIcon(QIcon(icon))
self.setCheckable(True)
self.splitter = splitter
splitter.state_changed.connect(self.update_state)
def set_state_to_show(self, *args):
self.setChecked(False)
label =_('Show')
self.setText(label + ' ' + self.label)
def set_state_to_hide(self, *args):
self.setChecked(True)
label = _('Hide')
self.setText(label + ' ' + self.label)
def update_state(self, *args):
if self.splitter.is_side_index_hidden:
self.set_state_to_show()
else:
self.set_state_to_hide()
class Splitter(QSplitter):
state_changed = pyqtSignal(object)
def __init__(self, *args):
QSplitter.__init__(self, *args)
def __init__(self, name, label, icon, initial_show=True,
initial_side_size=120, connect_button=True,
orientation=Qt.Horizontal, side_index=0, parent=None):
QSplitter.__init__(self, parent)
self.resize_timer = QTimer(self)
self.resize_timer.setSingleShot(True)
self.desired_side_size = initial_side_size
self.desired_show = initial_show
self.resize_timer.setInterval(5)
self.resize_timer.timeout.connect(self.do_resize)
self.setOrientation(orientation)
self.side_index = side_index
self._name = name
self.label = label
self.initial_side_size = initial_side_size
self.initial_show = initial_show
self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection)
self.button = LayoutButton(icon, label, self)
if connect_button:
self.button.clicked.connect(self.double_clicked)
def createHandle(self):
return SplitterHandle(self.orientation(), self)
def initialize(self, name=None):
if name is not None:
self._name = name
def initialize(self):
for i in range(self.count()):
h = self.handle(i)
if h is not None:
@ -965,40 +1010,117 @@ class Splitter(QSplitter):
self.state_changed.emit(not self.is_side_index_hidden)
def splitter_moved(self, *args):
self.desired_side_size = self.side_index_size
self.state_changed.emit(not self.is_side_index_hidden)
@property
def side_index(self):
return 0 if self.orientation() == Qt.Horizontal else 1
@property
def is_side_index_hidden(self):
sizes = list(self.sizes())
return sizes[self.side_index] == 0
def toggle_side_index(self):
self.double_clicked(None)
@property
def save_name(self):
ori = 'horizontal' if self.orientation() == Qt.Horizontal \
else 'vertical'
return self._name + '_' + ori
def double_clicked(self, handle):
visible = not self.is_side_index_hidden
sizes = list(self.sizes())
if 0 in sizes:
idx = sizes.index(0)
sizes[idx] = 80
else:
sizes[self.side_index] = 0
def print_sizes(self):
if self.count() > 1:
print self.save_name, 'side:', self.side_index_size, 'other:',
print list(self.sizes())[self.other_index]
if visible:
dynamic.set(self._name + '_last_open_state', str(self.saveState()))
@dynamic_property
def side_index_size(self):
def fget(self):
if self.count() < 2: return 0
return self.sizes()[self.side_index]
def fset(self, val):
if self.count() < 2: return
if val == 0 and not self.is_side_index_hidden:
self.save_state()
sizes = list(self.sizes())
for i in range(len(sizes)):
sizes[i] = val if i == self.side_index else 10
self.setSizes(sizes)
total = sum(self.sizes())
sizes = list(self.sizes())
for i in range(len(sizes)):
sizes[i] = val if i == self.side_index else total-val
self.setSizes(sizes)
self.initialize()
return property(fget=fget, fset=fset)
def do_resize(self, *args):
orig = self.desired_side_size
QSplitter.resizeEvent(self, self._resize_ev)
if orig > 20 and self.desired_show:
c = 0
while abs(self.side_index_size - orig) > 10 and c < 5:
self.apply_state(self.get_state(), save_desired=False)
c += 1
def resizeEvent(self, ev):
if self.resize_timer.isActive():
self.resize_timer.stop()
self._resize_ev = ev
self.resize_timer.start()
def get_state(self):
if self.count() < 2: return (False, 200)
return (self.desired_show, self.desired_side_size)
def apply_state(self, state, save_desired=True):
if state[0]:
self.side_index_size = state[1]
if save_desired:
self.desired_side_size = self.side_index_size
else:
state = dynamic.get(self._name+ '_last_open_state', None)
if state is not None:
self.restoreState(state)
else:
self.setSizes(sizes)
self.initialize()
self.side_index_size = 0
self.desired_show = state[0]
def default_state(self):
return (self.initial_show, self.initial_side_size)
# Public API {{{
def save_state(self):
if self.count() > 1:
gprefs[self.save_name+'_state'] = self.get_state()
@property
def other_index(self):
return (self.side_index+1)%2
def restore_state(self):
if self.count() > 1:
state = gprefs.get(self.save_name+'_state',
self.default_state())
self.apply_state(state, save_desired=False)
self.desired_side_size = state[1]
def toggle_side_pane(self, hide=None):
if hide is None:
action = 'show' if self.is_side_index_hidden else 'hide'
else:
action = 'hide' if hide else 'show'
getattr(self, action+'_side_pane')()
def show_side_pane(self):
if self.count() < 2 or not self.is_side_index_hidden:
return
if self.desired_side_size == 0:
self.desired_side_size = self.initial_side_size
self.apply_state((True, self.desired_side_size))
def hide_side_pane(self):
if self.count() < 2 or self.is_side_index_hidden:
return
self.apply_state((False, self.desired_side_size))
def double_clicked(self, *args):
self.toggle_side_pane()
# }}}

View File

@ -96,14 +96,14 @@ class Kobo(Device):
class Booq(Device):
name = 'Booq Reader'
manufacturer = 'Booq'
output_profile = 'prs505'
output_profile = 'sony'
output_format = 'EPUB'
id = 'booq'
class TheBook(Device):
name = 'The Book'
manufacturer = 'Augen'
output_profile = 'prs505'
output_profile = 'sony'
output_format = 'EPUB'
id = 'thebook'

View File

@ -10,14 +10,14 @@ import collections, glob, os, re, itertools, functools
from itertools import repeat
from datetime import timedelta
from PyQt4.QtCore import QThread, QReadWriteLock
from PyQt4.QtGui import QImage
from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.pyparsing import ParseException
from calibre.ebooks.metadata import title_sort
from calibre import fit_image
class CoverCache(QThread):
@ -96,6 +96,11 @@ class CoverCache(QThread):
img.loadFromData(data)
if img.isNull():
continue
scaled, nwidth, nheight = fit_image(img.width(),
img.height(), 600, 800)
if scaled:
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
Qt.SmoothTransformation)
except:
continue
self.cache_lock.lockForWrite()
@ -619,9 +624,12 @@ class ResultCache(SearchQueryParser):
if self.first_sort:
subsort = True
self.first_sort = False
fcmp = self.seriescmp if field == 'series' else \
functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort,
asstr=as_string)
fcmp = self.seriescmp \
if field == 'series' and \
tweaks['title_series_sorting'] == 'library_order' \
else \
functools.partial(self.cmp, self.FIELD_MAP[field],
subsort=subsort, asstr=as_string)
self._map.sort(cmp=fcmp, reverse=not ascending)
self._map_filtered = [id for id in self._map if id in self._map_filtered]

View File

@ -83,7 +83,7 @@ class CSV_XML(CatalogPlugin):
if not len(data):
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
raise SystemExit(1)
#raise SystemExit(1)
# Get the requested output fields as a list
fields = self.get_output_fields(opts)

View File

@ -28,7 +28,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
from calibre.utils.config import prefs, tweaks
from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick_draw import save_cover_data_to
@ -736,8 +736,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
icon=icon, tooltip = tooltip)
for r in data if item_not_zero_func(r)]
if category == 'series' and not sort_on_count:
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(),
title_sort(y.name).lower()))
if tweaks['title_series_sorting'] == 'library_order':
ts = lambda x: title_sort(x)
else:
ts = lambda x:x
categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(),
ts(y.name).lower()))
# We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically
@ -950,7 +954,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
title = title.decode(preferred_encoding, 'replace')
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True)
self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True)
if tweaks['title_series_sorting'] == 'library_order':
self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True)
else:
self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True)
self.set_path(id, True)
self.conn.commit()
if notify:
@ -1297,12 +1304,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
self.conn.commit()
try:
row = self.row(id)
if row is not None:
self.data.set(row, 9, series)
except ValueError:
pass
self.data.set(id, self.FIELD_MAP['series'], series, row_is_id=True)
if notify:
self.notify('metadata', [id])
@ -1835,6 +1836,8 @@ books_series_link feeds
os.remove(self.dbpath)
shutil.copyfile(dest, self.dbpath)
self.connect()
self.field_metadata.remove_dynamic_categories()
self.field_metadata.remove_custom_fields()
self.initialize_dynamic()
self.refresh()
if os.path.exists(dest):

View File

@ -379,6 +379,17 @@ class FieldMetadata(dict):
self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label] = key
def remove_custom_fields(self):
for key in self.get_custom_fields():
del self._tb_cats[key]
def remove_dynamic_categories(self):
for key in list(self._tb_cats.keys()):
val = self._tb_cats[key]
if val['is_category'] and val['kind'] in ('user', 'search'):
del self._tb_cats[key]
def add_user_category(self, label, name):
if label in self._tb_cats:
raise ValueError('Duplicate user field [%s]'%(label))

View File

@ -291,4 +291,8 @@ class SchemaUpgrade(object):
for field in self.field_metadata.itervalues():
if field['is_category'] and not field['is_custom'] and 'link_column' in field:
create_tag_browser_view(field['table'], field['link_column'], field['column'])
table = self.conn.get(
'SELECT name FROM sqlite_master WHERE type="table" AND name=?',
('books_%s_link'%field['table'],), all=False)
if table is not None:
create_tag_browser_view(field['table'], field['link_column'], field['column'])

View File

@ -60,6 +60,7 @@ class ContentServer(object):
items.sort(cmp=self.seriescmp, reverse=not order)
else:
lookup = 'sort' if field == 'title' else field
lookup = 'author_sort' if field == 'authors' else field
field = self.db.FIELD_MAP[lookup]
getter = operator.itemgetter(field)
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)

View File

@ -15,6 +15,7 @@ from threading import RLock
from datetime import datetime
from calibre.ebooks.metadata import title_sort
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat
global_lock = RLock()
@ -115,7 +116,10 @@ class DBThread(Thread):
self.conn.create_aggregate('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
self.conn.create_function('title_sort', 1, title_sort)
if tweaks['title_series_sorting'] == 'library_order':
self.conn.create_function('title_sort', 1, title_sort)
else:
self.conn.create_function('title_sort', 1, lambda x:x)
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
# Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1)

View File

@ -411,7 +411,8 @@ def options(option_parser):
def opts_and_words(name, op, words):
opts = '|'.join(options(op))
words = '|'.join([w.replace("'", "\\'") for w in words])
return ('_'+name+'()'+\
fname = name.replace('-', '_')
return ('_'+fname+'()'+\
'''
{
local cur opts
@ -435,7 +436,7 @@ def opts_and_words(name, op, words):
esac
}
complete -F _'''%(opts, words) + name + ' ' + name +"\n\n").encode('utf-8')
complete -F _'''%(opts, words) + fname + ' ' + name +"\n\n").encode('utf-8')
def opts_and_exts(name, op, exts):

View File

@ -159,7 +159,8 @@ Alternative for the iPad
As of |app| version 0.7.0, you can plugin your iPad into the computer using its charging cable, and |app| will detect it and show you a list of books on the iPad. You can then use the Send to device button to send books directly to iBooks on the iPad.
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported.
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported. For more details, see
`this forum post http://www.mobileread.com/forums/showpost.php?p=944079&postcount=1`_.
How do I use |app| with my Android phone?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -111,7 +111,7 @@ Pre/post processing of downloaded HTML
.. automember:: BasicNewsRecipe.remove_javascript
.. automethod:: BasicNewsRecipe.prepreprocess_html
.. automethod:: BasicNewsRecipe.skip_ad_pages
.. automethod:: BasicNewsRecipe.preprocess_html

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@ class SafeLocalTimeZone(tzlocal):
def compute_locale_info_for_parse_date():
try:
dt = datetime.strptime('1/5/2000', "%x")
except ValueError:
except:
try:
dt = datetime.strptime('1/5/01', '%x')
except:

View File

@ -175,7 +175,7 @@ def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
p.DestroyMagickWand(canvas)
def create_cover_page(top_lines, logo_path, width=590, height=750,
bgcolor='white', output_format='png'):
bgcolor='white', output_format='jpg'):
ans = None
with p.ImageMagick():
canvas = create_canvas(width, height, bgcolor)

View File

@ -137,7 +137,7 @@ class Feed(object):
def populate_from_preparsed_feed(self, title, articles, oldest_article=7,
max_articles_per_feed=100):
self.title = title if title else _('Unknown feed')
self.title = unicode(title if title else _('Unknown feed'))
self.description = ''
self.image_url = None
self.articles = []

View File

@ -413,18 +413,19 @@ class BasicNewsRecipe(Recipe):
return url
return article.get('link', None)
def prepreprocess_html(self, soup):
def skip_ad_pages(self, soup):
'''
This method is called with the source of each downloaded :term:`HTML` file, before
any of the cleanup attributes like remove_tags, keep_only_tags are
applied. Note that preprocess_regexps will have already been applied.
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
It should return `soup` after processing it.
It is meant to allow the recipe to skip ad pages. If the soup represents
an ad page, return the HTML of the real page. Otherwise return
None.
`soup`: A `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/documentation.html>`_
instance containing the downloaded :term:`HTML`.
'''
return soup
return None
def preprocess_html(self, soup):
@ -628,7 +629,7 @@ class BasicNewsRecipe(Recipe):
self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
'prepreprocess_html', 'preprocess_html', 'remove_tags_after',
'skip_ad_pages', 'preprocess_html', 'remove_tags_after',
'remove_tags_before', 'is_link_wanted'):
setattr(self.web2disk_options, extra, getattr(self, extra))
self.web2disk_options.postprocess_html = self._postprocess_html
@ -801,11 +802,6 @@ class BasicNewsRecipe(Recipe):
.calibre_navbar {
font-family:monospace;
}
hr {
border-color:gray;
border-style:solid;
border-width:thin;
}
'''

View File

@ -65,6 +65,7 @@ class NavBarTemplate(Template):
text = 'This article was downloaded by '
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
p[0].tail = ' from '
navbar.append(p)
navbar.append(BR())
navbar.append(BR())
else:
@ -107,10 +108,11 @@ class TouchscreenNavBarTemplate(Template):
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_100',
style='text-align:'+align))
if bottom:
navbar.append(HR())
navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white"))
text = 'This article was downloaded by '
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
p[0].tail = ' from '
navbar.append(p)
navbar.append(BR())
navbar.append(BR())
else:
@ -134,7 +136,7 @@ class TouchscreenNavBarTemplate(Template):
navbar.iterchildren(reversed=True).next().tail = ' | '
if not bottom:
navbar.append(HR())
navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white"))
self.root = HTML(head, BODY(navbar))
@ -191,6 +193,8 @@ class TouchscreenIndexTemplate(Template):
div = DIV(
masthead_p,
PT(date, style='text-align:center'),
#DIV(style="border-color:gray;border-top-style:solid;border-width:thin"),
DIV(style="border-top:1px solid gray;border-bottom:1em solid white"),
toc)
self.root = HTML(head, BODY(div))
@ -254,10 +258,9 @@ class TouchscreenFeedTemplate(Template):
head.append(STYLE(extra_css, type='text/css'))
body = BODY(style='page-break-before:always')
div = DIV(
H2(feed.title,
CLASS('calibre_feed_title', 'calibre_rescale_160')),
CLASS('calibre_rescale_100')
)
H2(feed.title, CLASS('calibre_feed_title', 'calibre_rescale_160')),
DIV(style="border-top:1px solid gray;border-bottom:1em solid white")
)
body.append(div)
if getattr(feed, 'image', None):
div.append(DIV(IMG(
@ -276,17 +279,33 @@ class TouchscreenFeedTemplate(Template):
if not getattr(article, 'downloaded', False):
continue
tr = TR()
td = TD(
A(article.title, CLASS('summary_headline','calibre_rescale_120',
href=article.url))
)
if article.author:
td.append(DIV(article.author,
CLASS('summary_byline', 'calibre_rescale_100')))
if article.summary:
td.append(DIV(cutoff(article.text_summary),
CLASS('summary_text', 'calibre_rescale_100')))
tr.append(td)
if True:
div_td = DIV(
A(article.title, CLASS('summary_headline','calibre_rescale_120',
href=article.url)),
style="display:inline-block")
if article.author:
div_td.append(DIV(article.author,
CLASS('summary_byline', 'calibre_rescale_100')))
if article.summary:
div_td.append(DIV(cutoff(article.text_summary),
CLASS('summary_text', 'calibre_rescale_100')))
tr.append(TD(div_td))
else:
td = TD(
A(article.title, CLASS('summary_headline','calibre_rescale_120',
href=article.url))
)
if article.author:
td.append(DIV(article.author,
CLASS('summary_byline', 'calibre_rescale_100')))
if article.summary:
td.append(DIV(cutoff(article.text_summary),
CLASS('summary_text', 'calibre_rescale_100')))
tr.append(td)
toc.append(tr)
div.append(toc)

View File

@ -136,7 +136,7 @@ class RecursiveFetcher(object):
self.remove_tags_before = getattr(options, 'remove_tags_before', None)
self.keep_only_tags = getattr(options, 'keep_only_tags', [])
self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup)
self.prepreprocess_html_ext = getattr(options, 'prepreprocess_html', lambda soup: soup)
self.prepreprocess_html_ext = getattr(options, 'skip_ad_pages', lambda soup: None)
self.postprocess_html_ext= getattr(options, 'postprocess_html', None)
self._is_link_wanted = getattr(options, 'is_link_wanted',
default_is_link_wanted)
@ -154,7 +154,9 @@ class RecursiveFetcher(object):
nmassage.append((re.compile(r'<!--.*?-->', re.DOTALL), lambda m: ''))
soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage)
soup = self.prepreprocess_html_ext(soup)
replace = self.prepreprocess_html_ext(soup)
if replace is not None:
soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage)
if self.keep_only_tags:
body = Tag(soup, 'body')