mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Pull from trunk
This commit is contained in:
commit
4414ed01f6
@ -4,6 +4,65 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.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
|
- version: 0.7.1
|
||||||
date: 2010-06-04
|
date: 2010-06-04
|
||||||
|
|
||||||
|
@ -61,3 +61,13 @@ sort_columns_at_startup = None
|
|||||||
# default if not set: MMM yyyy
|
# default if not set: MMM yyyy
|
||||||
gui_pubdate_display_format = '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 |
63
resources/recipes/auto.recipe
Normal file
63
resources/recipes/auto.recipe
Normal 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')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
__license__ = 'GPL v3'
|
__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
|
www.boston.com
|
||||||
'''
|
'''
|
||||||
@ -7,10 +7,10 @@ www.boston.com
|
|||||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
class BusinessStandard(BasicNewsRecipe):
|
class BusinessStandard(BasicNewsRecipe):
|
||||||
title = 'Boston'
|
title = 'The Boston Globe'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = 'News from Boston'
|
description = 'News from Boston'
|
||||||
oldest_article = 7
|
oldest_article = 2
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
delay = 1
|
delay = 1
|
||||||
@ -19,6 +19,9 @@ class BusinessStandard(BasicNewsRecipe):
|
|||||||
publisher = 'Boston'
|
publisher = 'Boston'
|
||||||
category = 'news, boston, usa, world'
|
category = 'news, boston, usa, world'
|
||||||
language = 'en'
|
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 = {
|
conversion_options = {
|
||||||
'comments' : description
|
'comments' : description
|
||||||
@ -27,8 +30,11 @@ class BusinessStandard(BasicNewsRecipe):
|
|||||||
,'publisher' : publisher
|
,'publisher' : publisher
|
||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'class':'story'})]
|
keep_only_tags = [dict(attrs={'id':['INDblogEntry','blogEntry','articleHeader','articleGraphs','galleryShell']})]
|
||||||
remove_tags = [dict(name=['object','link','script','iframe'])]
|
remove_tags = [
|
||||||
|
dict(name=['object','link','script','iframe'])
|
||||||
|
,dict(attrs={'id':['blogheadTools','bdc_emailWidget','tools','relatedContent']})
|
||||||
|
]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Top Stories' , u'http://feeds.boston.com/boston/topstories' )
|
(u'Top Stories' , u'http://feeds.boston.com/boston/topstories' )
|
||||||
@ -38,12 +44,9 @@ class BusinessStandard(BasicNewsRecipe):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url + '?mode=PF'
|
return url + '?page=full'
|
||||||
|
|
||||||
def get_article_url(self, article):
|
def get_article_url(self, article):
|
||||||
rawarticle = article.get('pheedo_origlink', None)
|
rawarticle = article.get('guid', None)
|
||||||
artls, sep, rsep = rawarticle.rpartition('/?')
|
return rawarticle.rpartition('?')[0]
|
||||||
if artls == '':
|
|
||||||
artls = rawarticle.rpartition('?')[0]
|
|
||||||
return artls
|
|
||||||
|
|
||||||
|
60
resources/recipes/corriere_dello_sport.recipe
Normal file
60
resources/recipes/corriere_dello_sport.recipe
Normal 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')
|
||||||
|
]
|
@ -24,9 +24,10 @@ class Economist(BasicNewsRecipe):
|
|||||||
oldest_article = 7.0
|
oldest_article = 7.0
|
||||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||||
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
||||||
dict(attrs={'class':['dblClkTrk']})]
|
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
|
||||||
remove_tags_before = dict(name=lambda tag: tag.name=='title' and tag.parent.name=='body')
|
keep_only_tags = [dict(id='ec-article-body')]
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
|
no_stylesheets = True
|
||||||
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
||||||
lambda x:'</html>')]
|
lambda x:'</html>')]
|
||||||
|
|
||||||
@ -87,7 +88,9 @@ class Economist(BasicNewsRecipe):
|
|||||||
continue
|
continue
|
||||||
a = tag.find('a', href=True)
|
a = tag.find('a', href=True)
|
||||||
if a is not None:
|
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'):
|
if url.startswith('Printer'):
|
||||||
url = '/'+url
|
url = '/'+url
|
||||||
if url.startswith('/'):
|
if url.startswith('/'):
|
||||||
|
@ -17,8 +17,9 @@ class Economist(BasicNewsRecipe):
|
|||||||
oldest_article = 7.0
|
oldest_article = 7.0
|
||||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||||
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
||||||
dict(attrs={'class':['dblClkTrk']})]
|
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
|
||||||
remove_tags_before = dict(name=lambda tag: tag.name=='title' and tag.parent.name=='body')
|
keep_only_tags = [dict(id='ec-article-body')]
|
||||||
|
no_stylesheets = True
|
||||||
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
||||||
lambda x:'</html>')]
|
lambda x:'</html>')]
|
||||||
|
|
||||||
@ -88,19 +89,20 @@ class Economist(BasicNewsRecipe):
|
|||||||
br = browser()
|
br = browser()
|
||||||
ret = br.open(url)
|
ret = br.open(url)
|
||||||
raw = ret.read()
|
raw = ret.read()
|
||||||
url = br.geturl().replace('displaystory', 'PrinterFriendly').strip()
|
url = br.geturl().split('?')[0]+'/print'
|
||||||
root = html.fromstring(raw)
|
root = html.fromstring(raw)
|
||||||
matches = root.xpath('//*[@class = "article-section"]')
|
matches = root.xpath('//*[@class = "ec-article-info"]')
|
||||||
feedtitle = 'Miscellaneous'
|
feedtitle = 'Miscellaneous'
|
||||||
if matches:
|
if matches:
|
||||||
feedtitle = string.capwords(html.tostring(matches[0], method='text',
|
feedtitle = string.capwords(html.tostring(matches[-1], method='text',
|
||||||
encoding=unicode))
|
encoding=unicode).split('|')[-1].strip())
|
||||||
return (i, feedtitle, url, title, description, author, published)
|
return (i, feedtitle, url, title, description, author, published)
|
||||||
|
|
||||||
def eco_article_found(self, req, result):
|
def eco_article_found(self, req, result):
|
||||||
from calibre.web.feeds import Article
|
from calibre.web.feeds import Article
|
||||||
i, feedtitle, link, title, description, author, published = result
|
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, '')
|
a = Article(i, title, link, author, description, published, '')
|
||||||
|
|
||||||
|
@ -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'
|
|
||||||
|
|
@ -51,6 +51,7 @@ class LeMondeDiplomatiqueEn(BasicNewsRecipe):
|
|||||||
, dict(name='div',attrs={'class':'notes surlignable'})
|
, dict(name='div',attrs={'class':'notes surlignable'})
|
||||||
]
|
]
|
||||||
remove_tags = [dict(name=['object','link','script','iframe','base'])]
|
remove_tags = [dict(name=['object','link','script','iframe','base'])]
|
||||||
|
remove_attributes = ['height','width']
|
||||||
|
|
||||||
def parse_index(self):
|
def parse_index(self):
|
||||||
articles = []
|
articles = []
|
||||||
@ -72,5 +73,5 @@ class LeMondeDiplomatiqueEn(BasicNewsRecipe):
|
|||||||
,'url' :url
|
,'url' :url
|
||||||
,'description':description
|
,'description':description
|
||||||
})
|
})
|
||||||
return [(soup.head.title.string, articles)]
|
return [(self.title, articles)]
|
||||||
|
|
||||||
|
@ -11,11 +11,11 @@ http://www.libero-news.it/
|
|||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class LiberoNews(BasicNewsRecipe):
|
class LiberoNews(BasicNewsRecipe):
|
||||||
__author__ = 'Marini Gabriele'
|
__author__ = 'Marini Gabriele'
|
||||||
description = 'Italian daily newspaper'
|
description = 'Italian daily newspaper'
|
||||||
|
|
||||||
cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif'
|
cover_url = 'http://www.libero-news.it/images/logo.png'
|
||||||
title = u'Libero'
|
title = u'Libero '
|
||||||
publisher = 'EDITORIALE LIBERO s.r.l 2006'
|
publisher = 'EDITORIALE LIBERO s.r.l 2006'
|
||||||
category = 'News, politics, culture, economy, general interest'
|
category = 'News, politics, culture, economy, general interest'
|
||||||
|
|
||||||
|
43
resources/recipes/reptantes.recipe
Normal file
43
resources/recipes/reptantes.recipe
Normal 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)
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ class Smh_au(BasicNewsRecipe):
|
|||||||
language = 'en_AU'
|
language = 'en_AU'
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
masthead_url = 'http://images.smh.com.au/2010/02/02/1087188/smh-620.jpg'
|
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} '
|
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 = {
|
conversion_options = {
|
||||||
@ -47,7 +47,7 @@ class Smh_au(BasicNewsRecipe):
|
|||||||
for itimg in soup.findAll('img',src=True):
|
for itimg in soup.findAll('img',src=True):
|
||||||
if itimg['src'].endswith('frontpage.jpg'):
|
if itimg['src'].endswith('frontpage.jpg'):
|
||||||
self.cover_url = itimg['src']
|
self.cover_url = itimg['src']
|
||||||
|
|
||||||
for item in soup.findAll(attrs={'class':'cN-storyHeadlineLead cfix'}):
|
for item in soup.findAll(attrs={'class':'cN-storyHeadlineLead cfix'}):
|
||||||
description = ''
|
description = ''
|
||||||
title_prefix = ''
|
title_prefix = ''
|
||||||
@ -65,4 +65,4 @@ class Smh_au(BasicNewsRecipe):
|
|||||||
,'url' :url
|
,'url' :url
|
||||||
,'description':description
|
,'description':description
|
||||||
})
|
})
|
||||||
return [(soup.head.title.string, articles)]
|
return [(self.tag_to_string(soup.find('title')), articles)]
|
||||||
|
47
resources/recipes/staradvertiser.recipe
Normal file
47
resources/recipes/staradvertiser.recipe
Normal 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' )
|
||||||
|
]
|
@ -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
|
|
||||||
|
|
@ -16,7 +16,7 @@ class DailyTelegraph(BasicNewsRecipe):
|
|||||||
language = 'en_AU'
|
language = 'en_AU'
|
||||||
|
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
max_articles_per_feed = 10
|
max_articles_per_feed = 20
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf8'
|
encoding = 'utf8'
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
from calibre.ebooks.BeautifulSoup import Tag
|
||||||
|
|
||||||
class AdvancedUserRecipe1268409464(BasicNewsRecipe):
|
class AdvancedUserRecipe1268409464(BasicNewsRecipe):
|
||||||
title = u'The Sun'
|
title = u'The Sun'
|
||||||
@ -14,24 +15,27 @@ class AdvancedUserRecipe1268409464(BasicNewsRecipe):
|
|||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
|
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'class':'medium-centered'})
|
dict(id='column-print')
|
||||||
,dict(name='div', attrs={'class':'article'})
|
|
||||||
,dict(name='div', attrs={'class':'clear-left'})
|
|
||||||
,dict(name='div', attrs={'class':'text-center'})
|
|
||||||
]
|
]
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'class':'slideshow'})
|
dict(name='div', attrs={'class':[
|
||||||
,dict(name='div', attrs={'class':'float-left'})
|
'clear text-center small padding-left-right-5 text-999 padding-top-5 padding-bottom-10 grey-solid-line',
|
||||||
,dict(name='div', attrs={'class':'ltbx-slideshow ltbx-btn-ss'})
|
'clear width-625 bg-fff padding-top-10'
|
||||||
,dict(name='a', attrs={'class':'add_a_comment'})
|
]}),
|
||||||
,dict(name='div', attrs={'id':'vxFlashPlayerContent'})
|
dict(name='video'),
|
||||||
,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'})
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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')
|
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')
|
,(u'Sport', u'http://www.thesun.co.uk/sol/homepage/feeds/rss/article247732.ece')
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.1'
|
__version__ = '0.7.2'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -445,7 +445,7 @@ from calibre.devices.nook.driver import NOOK
|
|||||||
from calibre.devices.prs505.driver import PRS505
|
from calibre.devices.prs505.driver import PRS505
|
||||||
from calibre.devices.android.driver import ANDROID, S60
|
from calibre.devices.android.driver import ANDROID, S60
|
||||||
from calibre.devices.nokia.driver import N770, N810, E71X
|
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.nuut2.driver import NUUT2
|
||||||
from calibre.devices.iriver.driver import IRIVER_STORY
|
from calibre.devices.iriver.driver import IRIVER_STORY
|
||||||
from calibre.devices.binatone.driver import README
|
from calibre.devices.binatone.driver import README
|
||||||
@ -519,6 +519,7 @@ plugins += [
|
|||||||
N810,
|
N810,
|
||||||
COOL_ER,
|
COOL_ER,
|
||||||
ESLICK,
|
ESLICK,
|
||||||
|
EBK52,
|
||||||
NUUT2,
|
NUUT2,
|
||||||
IRIVER_STORY,
|
IRIVER_STORY,
|
||||||
GER2,
|
GER2,
|
||||||
|
@ -30,7 +30,7 @@ class ANDROID(USBMS):
|
|||||||
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
|
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
|
||||||
|
|
||||||
# Samsung
|
# Samsung
|
||||||
0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222, 0x0224]},
|
0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]},
|
||||||
|
|
||||||
# Acer
|
# Acer
|
||||||
0x502 : { 0x3203 : [0x0100]},
|
0x502 : { 0x3203 : [0x0100]},
|
||||||
@ -41,10 +41,12 @@ class ANDROID(USBMS):
|
|||||||
'be used')
|
'be used')
|
||||||
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
|
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',
|
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD']
|
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD',
|
||||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE']
|
'PROD_GT-I9000']
|
||||||
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'PROD_GT-I9000_CARD']
|
||||||
|
|
||||||
OSX_MAIN_MEM = 'HTC Android Phone Media'
|
OSX_MAIN_MEM = 'HTC Android Phone Media'
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -36,4 +36,29 @@ class ESLICK(USBMS):
|
|||||||
|
|
||||||
SUPPORTS_SUB_DIRS = True
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
@ -289,7 +289,10 @@ class DevicePlugin(Plugin):
|
|||||||
word "card" if C{on_card} is not None otherwise it must contain the word "memory".
|
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
|
: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
|
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
|
:names: A list of file names that the books should have
|
||||||
once uploaded to the device. len(names) == len(files)
|
once uploaded to the device. len(names) == len(files)
|
||||||
:return: A list of 3-element tuples. The list is meant to be passed
|
:return: A list of 3-element tuples. The list is meant to be passed
|
||||||
|
@ -7,12 +7,33 @@ __docformat__ = 'restructuredtext en'
|
|||||||
'''
|
'''
|
||||||
Device driver for Amazon's Kindle
|
Device driver for Amazon's Kindle
|
||||||
'''
|
'''
|
||||||
import datetime, os, re, sys
|
import datetime, os, re, sys, json, hashlib
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS
|
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):
|
class KINDLE(USBMS):
|
||||||
|
|
||||||
name = 'Kindle Device Interface'
|
name = 'Kindle Device Interface'
|
||||||
@ -60,6 +81,7 @@ class KINDLE(USBMS):
|
|||||||
'replace')
|
'replace')
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
|
|
||||||
def get_annotations(self, path_map):
|
def get_annotations(self, path_map):
|
||||||
MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt']
|
MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt']
|
||||||
mbp_formats = set(MBP_FORMATS)
|
mbp_formats = set(MBP_FORMATS)
|
||||||
@ -150,6 +172,37 @@ class KINDLE2(KINDLE):
|
|||||||
PRODUCT_ID = [0x0002]
|
PRODUCT_ID = [0x0002]
|
||||||
BCD = [0x0100]
|
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):
|
class KINDLE_DX(KINDLE2):
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ def strptime(src):
|
|||||||
src[2] = str(MONTH_MAP[src[2]])
|
src[2] = str(MONTH_MAP[src[2]])
|
||||||
return time.strptime(' '.join(src), '%w, %d %m %Y %H:%M:%S %Z')
|
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 = time.strftime("%w, %d %m %Y %H:%M:%S GMT", zone(epoch)).split()
|
||||||
src[0] = INVERSE_DAY_MAP[int(src[0][:-1])]+','
|
src[0] = INVERSE_DAY_MAP[int(src[0][:-1])]+','
|
||||||
src[2] = INVERSE_MONTH_MAP[int(src[2])]
|
src[2] = INVERSE_MONTH_MAP[int(src[2])]
|
||||||
@ -423,7 +423,7 @@ class XMLCache(object):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
def update_text_record(self, record, book, path, bl_index):
|
def update_text_record(self, record, book, path, bl_index):
|
||||||
timestamp = os.path.getctime(path)
|
timestamp = os.path.getmtime(path)
|
||||||
date = strftime(timestamp)
|
date = strftime(timestamp)
|
||||||
if date != record.get('date', None):
|
if date != record.get('date', None):
|
||||||
record.set('date', date)
|
record.set('date', date)
|
||||||
|
@ -47,7 +47,7 @@ class Book(MetaInformation):
|
|||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
# use lpath because the prefix can change, changing path
|
# 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
|
@dynamic_property
|
||||||
def db_id(self):
|
def db_id(self):
|
||||||
|
@ -294,6 +294,18 @@ class USBMS(CLI, Device):
|
|||||||
self.report_progress(1.0, _('Sending metadata to device...'))
|
self.report_progress(1.0, _('Sending metadata to device...'))
|
||||||
debug_print('USBMS: finished sync_booklists')
|
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
|
@classmethod
|
||||||
def path_to_unicode(cls, path):
|
def path_to_unicode(cls, path):
|
||||||
if isbytestring(path):
|
if isbytestring(path):
|
||||||
@ -355,22 +367,22 @@ class USBMS(CLI, Device):
|
|||||||
from calibre.ebooks.metadata.meta import metadata_from_formats
|
from calibre.ebooks.metadata.meta import metadata_from_formats
|
||||||
from calibre.customize.ui import quick_metadata
|
from calibre.customize.ui import quick_metadata
|
||||||
with 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
|
@classmethod
|
||||||
def book_from_path(cls, prefix, path):
|
def book_from_path(cls, prefix, lpath):
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
|
||||||
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
|
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:
|
else:
|
||||||
from calibre.ebooks.metadata.meta import metadata_from_filename
|
from calibre.ebooks.metadata.meta import metadata_from_filename
|
||||||
mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)),
|
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
|
||||||
re.compile(r'^(?P<title>[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+'))
|
cls.build_template_regexp())
|
||||||
|
|
||||||
if mi is None:
|
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')])
|
[_('Unknown')])
|
||||||
size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size
|
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
|
||||||
book = cls.book_class(prefix, path, other=mi, size=size)
|
book = cls.book_class(prefix, lpath, other=mi, size=size)
|
||||||
return book
|
return book
|
||||||
|
@ -706,15 +706,22 @@ OptionRecommendation(name='timestamp',
|
|||||||
for rec in group:
|
for rec in group:
|
||||||
setattr(self.opts, rec.option.name, rec.recommended_value)
|
setattr(self.opts, rec.option.name, rec.recommended_value)
|
||||||
|
|
||||||
for x in input_profiles():
|
def set_profile(profiles, which):
|
||||||
if x.short_name == self.opts.input_profile:
|
attr = which + '_profile'
|
||||||
self.opts.input_profile = x
|
sval = getattr(self.opts, attr)
|
||||||
break
|
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():
|
set_profile(input_profiles, 'input')
|
||||||
if x.short_name == self.opts.output_profile:
|
set_profile(output_profiles, 'output')
|
||||||
self.opts.output_profile = x
|
|
||||||
break
|
|
||||||
|
|
||||||
self.read_user_metadata()
|
self.read_user_metadata()
|
||||||
self.opts.no_inline_navbars = self.opts.output_profile.supports_mobi_indexing \
|
self.opts.no_inline_navbars = self.opts.output_profile.supports_mobi_indexing \
|
||||||
|
@ -10,12 +10,31 @@ import os
|
|||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
from calibre.customize import FileTypePlugin
|
||||||
|
from calibre.utils.zipfile import ZipFile, stringFileHeader
|
||||||
|
|
||||||
def is_comic(list_of_names):
|
def is_comic(list_of_names):
|
||||||
extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names])
|
extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names])
|
||||||
comic_extensions = set(['jpg', 'jpeg', 'png'])
|
comic_extensions = set(['jpg', 'jpeg', 'png'])
|
||||||
return len(extensions - comic_extensions) == 0
|
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):
|
class ArchiveExtract(FileTypePlugin):
|
||||||
name = 'Archive Extract'
|
name = 'Archive Extract'
|
||||||
author = 'Kovid Goyal'
|
author = 'Kovid Goyal'
|
||||||
@ -31,7 +50,6 @@ class ArchiveExtract(FileTypePlugin):
|
|||||||
if is_rar:
|
if is_rar:
|
||||||
from calibre.libunrar import extract_member, names
|
from calibre.libunrar import extract_member, names
|
||||||
else:
|
else:
|
||||||
from calibre.utils.zipfile import ZipFile
|
|
||||||
zf = ZipFile(archive, 'r')
|
zf = ZipFile(archive, 'r')
|
||||||
|
|
||||||
if is_rar:
|
if is_rar:
|
||||||
|
@ -27,16 +27,16 @@ for i, ext in enumerate(_METADATA_PRIORITIES):
|
|||||||
def path_to_ext(path):
|
def path_to_ext(path):
|
||||||
return os.path.splitext(path)[1][1:].lower()
|
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:
|
try:
|
||||||
return _metadata_from_formats(formats)
|
return _metadata_from_formats(formats, force_read_metadata, pattern)
|
||||||
except:
|
except:
|
||||||
mi = metadata_from_filename(list(iter(formats))[0])
|
mi = metadata_from_filename(list(iter(formats), pattern)[0])
|
||||||
if not mi.authors:
|
if not mi.authors:
|
||||||
mi.authors = [_('Unknown')]
|
mi.authors = [_('Unknown')]
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
def _metadata_from_formats(formats):
|
def _metadata_from_formats(formats, force_read_metadata=False, pattern=None):
|
||||||
mi = MetaInformation(None, None)
|
mi = MetaInformation(None, None)
|
||||||
formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)],
|
formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)],
|
||||||
METADATA_PRIORITIES[path_to_ext(y)]))
|
METADATA_PRIORITIES[path_to_ext(y)]))
|
||||||
@ -51,7 +51,9 @@ def _metadata_from_formats(formats):
|
|||||||
with open(path, 'rb') as stream:
|
with open(path, 'rb') as stream:
|
||||||
try:
|
try:
|
||||||
newmi = get_metadata(stream, stream_type=ext,
|
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)
|
mi.smart_update(newmi)
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
@ -69,18 +71,21 @@ def is_recipe(filename):
|
|||||||
return filename.startswith('calibre') and \
|
return filename.startswith('calibre') and \
|
||||||
filename.rpartition('.')[0].endswith('_recipe_out')
|
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
|
pos = 0
|
||||||
if hasattr(stream, 'tell'):
|
if hasattr(stream, 'tell'):
|
||||||
pos = stream.tell()
|
pos = stream.tell()
|
||||||
try:
|
try:
|
||||||
return _get_metadata(stream, stream_type, use_libprs_metadata)
|
return _get_metadata(stream, stream_type, use_libprs_metadata,
|
||||||
|
force_read_metadata, pattern)
|
||||||
finally:
|
finally:
|
||||||
if hasattr(stream, 'seek'):
|
if hasattr(stream, 'seek'):
|
||||||
stream.seek(pos)
|
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: stream_type = stream_type.lower()
|
||||||
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
|
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
|
||||||
stream_type = 'html'
|
stream_type = 'html'
|
||||||
@ -100,8 +105,8 @@ def _get_metadata(stream, stream_type, use_libprs_metadata):
|
|||||||
|
|
||||||
mi = MetaInformation(None, None)
|
mi = MetaInformation(None, None)
|
||||||
name = os.path.basename(getattr(stream, 'name', ''))
|
name = os.path.basename(getattr(stream, 'name', ''))
|
||||||
base = metadata_from_filename(name)
|
base = metadata_from_filename(name, pat=pattern)
|
||||||
if is_recipe(name) or prefs['read_file_metadata']:
|
if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']:
|
||||||
mi = get_file_type_metadata(stream, stream_type)
|
mi = get_file_type_metadata(stream, stream_type)
|
||||||
if base.title == os.path.splitext(name)[0] and base.authors is None:
|
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
|
# 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'))
|
pat = re.compile(prefs.get('filename_pattern'))
|
||||||
name = name.replace('_', ' ')
|
name = name.replace('_', ' ')
|
||||||
match = pat.search(name)
|
match = pat.search(name)
|
||||||
if match:
|
if match is not None:
|
||||||
try:
|
try:
|
||||||
mi.title = match.group('title')
|
mi.title = match.group('title')
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
@ -103,8 +103,8 @@ class CoverManager(object):
|
|||||||
32)]
|
32)]
|
||||||
img_data = create_cover_page(lines, I('library.png'))
|
img_data = create_cover_page(lines, I('library.png'))
|
||||||
id, href = self.oeb.manifest.generate('cover_image',
|
id, href = self.oeb.manifest.generate('cover_image',
|
||||||
'cover_image.png')
|
'cover_image.jpg')
|
||||||
item = self.oeb.manifest.add(id, href, guess_type('t.png')[0],
|
item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0],
|
||||||
data=img_data)
|
data=img_data)
|
||||||
m.clear('cover')
|
m.clear('cover')
|
||||||
m.add('cover', item.id)
|
m.add('cover', item.id)
|
||||||
|
@ -99,6 +99,8 @@ def _config():
|
|||||||
help=_('Limit max simultaneous jobs to number of CPUs'))
|
help=_('Limit max simultaneous jobs to number of CPUs'))
|
||||||
c.add_opt('tag_browser_hidden_categories', default=set(),
|
c.add_opt('tag_browser_hidden_categories', default=set(),
|
||||||
help=_('tag browser categories not to display'))
|
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)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
config = _config()
|
config = _config()
|
||||||
@ -125,6 +127,17 @@ def available_width():
|
|||||||
desktop = QCoreApplication.instance().desktop()
|
desktop = QCoreApplication.instance().desktop()
|
||||||
return desktop.availableGeometry().width()
|
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):
|
def extension(path):
|
||||||
return os.path.splitext(path)[1][1:].lower()
|
return os.path.splitext(path)[1][1:].lower()
|
||||||
|
|
||||||
|
1209
src/calibre/gui2/actions.py
Normal file
1209
src/calibre/gui2/actions.py
Normal file
File diff suppressed because it is too large
Load Diff
280
src/calibre/gui2/book_details.py
Normal file
280
src/calibre/gui2/book_details.py
Normal 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({})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -7,16 +7,18 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Module to implement the Cover Flow feature
|
Module to implement the Cover Flow feature
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import sys, os
|
import sys, os, time
|
||||||
|
|
||||||
from PyQt4.QtGui import QImage, QSizePolicy
|
from PyQt4.Qt import QImage, QSizePolicy, QTimer, QDialog, Qt, QSize, \
|
||||||
from PyQt4.QtCore import Qt, QSize, SIGNAL, QObject
|
QStackedLayout, QLabel
|
||||||
|
|
||||||
from calibre import plugins
|
from calibre import plugins
|
||||||
from calibre.gui2 import config
|
from calibre.gui2 import config, available_height, available_width
|
||||||
|
|
||||||
pictureflow, pictureflowerror = plugins['pictureflow']
|
pictureflow, pictureflowerror = plugins['pictureflow']
|
||||||
|
|
||||||
if pictureflow is not None:
|
if pictureflow is not None:
|
||||||
|
|
||||||
class EmptyImageList(pictureflow.FlowImages):
|
class EmptyImageList(pictureflow.FlowImages):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pictureflow.FlowImages.__init__(self)
|
pictureflow.FlowImages.__init__(self)
|
||||||
@ -51,7 +53,7 @@ if pictureflow is not None:
|
|||||||
def __init__(self, model, buffer=20):
|
def __init__(self, model, buffer=20):
|
||||||
pictureflow.FlowImages.__init__(self)
|
pictureflow.FlowImages.__init__(self)
|
||||||
self.model = model
|
self.model = model
|
||||||
QObject.connect(self.model, SIGNAL('modelReset()'), self.reset)
|
self.model.modelReset.connect(self.reset)
|
||||||
|
|
||||||
def count(self):
|
def count(self):
|
||||||
return self.model.count()
|
return self.model.count()
|
||||||
@ -66,7 +68,7 @@ if pictureflow is not None:
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.emit(SIGNAL('dataChanged()'))
|
self.dataChanged.emit()
|
||||||
|
|
||||||
def image(self, index):
|
def image(self, index):
|
||||||
return self.model.cover(index)
|
return self.model.cover(index)
|
||||||
@ -74,13 +76,16 @@ if pictureflow is not None:
|
|||||||
|
|
||||||
class CoverFlow(pictureflow.PictureFlow):
|
class CoverFlow(pictureflow.PictureFlow):
|
||||||
|
|
||||||
def __init__(self, height=300, parent=None, text_height=25):
|
def __init__(self, parent=None):
|
||||||
pictureflow.PictureFlow.__init__(self, parent,
|
pictureflow.PictureFlow.__init__(self, parent,
|
||||||
config['cover_flow_queue_length']+1)
|
config['cover_flow_queue_length']+1)
|
||||||
self.setSlideSize(QSize(int(2/3. * height), height))
|
self.setMinimumSize(QSize(300, 150))
|
||||||
self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+text_height))
|
|
||||||
self.setFocusPolicy(Qt.WheelFocus)
|
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):
|
def wheelEvent(self, ev):
|
||||||
ev.accept()
|
ev.accept()
|
||||||
@ -95,6 +100,118 @@ else:
|
|||||||
DatabaseImages = None
|
DatabaseImages = None
|
||||||
FileSystemImages = 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):
|
def main(args=sys.argv):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@ -103,12 +220,12 @@ if __name__ == '__main__':
|
|||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
w = QMainWindow()
|
w = QMainWindow()
|
||||||
cf = CoverFlow()
|
cf = CoverFlow()
|
||||||
cf.resize(cf.minimumSize())
|
cf.resize(int(available_width()/1.5), available_height()-60)
|
||||||
w.resize(cf.minimumSize()+QSize(30, 20))
|
w.resize(cf.size()+QSize(30, 20))
|
||||||
path = sys.argv[1]
|
path = sys.argv[1]
|
||||||
model = FileSystemImages(sys.argv[1])
|
model = FileSystemImages(sys.argv[1])
|
||||||
|
cf.currentChanged[int].connect(model.currentChanged)
|
||||||
cf.setImages(model)
|
cf.setImages(model)
|
||||||
cf.connect(cf, SIGNAL('currentChanged(int)'), model.currentChanged)
|
|
||||||
w.setCentralWidget(cf)
|
w.setCentralWidget(cf)
|
||||||
|
|
||||||
w.show()
|
w.show()
|
||||||
|
@ -101,6 +101,11 @@ class Float(Int):
|
|||||||
w.setSpecialValueText(_('Undefined'))
|
w.setSpecialValueText(_('Undefined'))
|
||||||
w.setSingleStep(1)
|
w.setSingleStep(1)
|
||||||
|
|
||||||
|
def setter(self, val):
|
||||||
|
if val is None:
|
||||||
|
val = self.widgets[1].minimum()
|
||||||
|
self.widgets[1].setValue(val)
|
||||||
|
|
||||||
class Rating(Int):
|
class Rating(Int):
|
||||||
|
|
||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
|
@ -3,24 +3,26 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
# Imports {{{
|
# Imports {{{
|
||||||
import os, traceback, Queue, time, socket, cStringIO, re
|
import os, traceback, Queue, time, socket, cStringIO, re, sys
|
||||||
from threading import Thread, RLock
|
from threading import Thread, RLock
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
|
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, \
|
from calibre.customize.ui import available_input_formats, available_output_formats, \
|
||||||
device_plugins
|
device_plugins
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
|
from calibre.devices.errors import UserFeedback
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.utils.ipc.job import BaseJob
|
from calibre.utils.ipc.job import BaseJob
|
||||||
from calibre.devices.scanner import DeviceScanner
|
from calibre.devices.scanner import DeviceScanner
|
||||||
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
||||||
pixmap_to_data, warning_dialog, \
|
pixmap_to_data, warning_dialog, \
|
||||||
question_dialog
|
question_dialog, info_dialog, choose_dir
|
||||||
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
|
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
|
||||||
from calibre import preferred_encoding, prints
|
from calibre import preferred_encoding, prints
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
@ -391,16 +393,14 @@ class DeviceMenu(QMenu): # {{{
|
|||||||
default_account = (dest, False, False, I('mail.svg'),
|
default_account = (dest, False, False, I('mail.svg'),
|
||||||
_('Email to')+' '+account)
|
_('Email to')+' '+account)
|
||||||
action1 = DeviceAction(dest, False, False, I('mail.svg'),
|
action1 = DeviceAction(dest, False, False, I('mail.svg'),
|
||||||
_('Email to')+' '+account, self)
|
_('Email to')+' '+account)
|
||||||
action2 = DeviceAction(dest, True, False, I('mail.svg'),
|
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.email_to_menu.addAction, (action1, action2))
|
||||||
map(self._memory.append, (action1, action2))
|
map(self._memory.append, (action1, action2))
|
||||||
self.email_to_menu.addSeparator()
|
self.email_to_menu.addSeparator()
|
||||||
self.connect(action1, SIGNAL('a_s(QAction)'),
|
action1.a_s.connect(self.action_triggered)
|
||||||
self.action_triggered)
|
action2.a_s.connect(self.action_triggered)
|
||||||
self.connect(action2, SIGNAL('a_s(QAction)'),
|
|
||||||
self.action_triggered)
|
|
||||||
|
|
||||||
basic_actions = [
|
basic_actions = [
|
||||||
('main:', False, False, I('reader.svg'),
|
('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):
|
def dispatch_sync_event(self, dest, delete, specific):
|
||||||
rows = self.library_view.selectionModel().selectedRows()
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
@ -686,7 +883,7 @@ class DeviceGUI(object):
|
|||||||
self.emailer.send_mails(jobnames,
|
self.emailer.send_mails(jobnames,
|
||||||
Dispatcher(partial(self.emails_sent, remove=remove)),
|
Dispatcher(partial(self.emails_sent, remove=remove)),
|
||||||
attachments, to_s, subjects, texts, attachment_names)
|
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 = []
|
auto = []
|
||||||
if _auto_ids != []:
|
if _auto_ids != []:
|
||||||
@ -747,7 +944,7 @@ class DeviceGUI(object):
|
|||||||
'%s'%errors, show=True
|
'%s'%errors, show=True
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.status_bar.showMessage(_('Sent by email:') + ', '.join(good),
|
self.status_bar.show_message(_('Sent by email:') + ', '.join(good),
|
||||||
5000)
|
5000)
|
||||||
|
|
||||||
def cover_to_thumbnail(self, data):
|
def cover_to_thumbnail(self, data):
|
||||||
@ -786,7 +983,7 @@ class DeviceGUI(object):
|
|||||||
attachments, to_s, subjects, texts, attachment_names)
|
attachments, to_s, subjects, texts, attachment_names)
|
||||||
sent_mails.append(to_s[0])
|
sent_mails.append(to_s[0])
|
||||||
if sent_mails:
|
if sent_mails:
|
||||||
self.status_bar.showMessage(_('Sent news to')+' '+\
|
self.status_bar.show_message(_('Sent news to')+' '+\
|
||||||
', '.join(sent_mails), 3000)
|
', '.join(sent_mails), 3000)
|
||||||
|
|
||||||
def sync_catalogs(self, send_ids=None, do_auto_convert=True):
|
def sync_catalogs(self, send_ids=None, do_auto_convert=True):
|
||||||
@ -845,12 +1042,13 @@ class DeviceGUI(object):
|
|||||||
self.upload_books(files, names, metadata,
|
self.upload_books(files, names, metadata,
|
||||||
on_card=on_card,
|
on_card=on_card,
|
||||||
memory=[files, remove])
|
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):
|
def sync_news(self, send_ids=None, do_auto_convert=True):
|
||||||
if self.device_connected:
|
if self.device_connected:
|
||||||
|
del_on_upload = config['delete_news_from_library_on_upload']
|
||||||
settings = self.device_manager.device.settings()
|
settings = self.device_manager.device.settings()
|
||||||
ids = list(dynamic.get('news_to_be_synced', set([]))) if send_ids is None else send_ids
|
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)]
|
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):
|
'the device?'), det_msg=autos):
|
||||||
self.auto_convert_news(auto, format)
|
self.auto_convert_news(auto, format)
|
||||||
files = [f for f in files if f is not None]
|
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:
|
if not files:
|
||||||
dynamic.set('news_to_be_synced', set([]))
|
dynamic.set('news_to_be_synced', set([]))
|
||||||
return
|
return
|
||||||
@ -897,8 +1097,7 @@ class DeviceGUI(object):
|
|||||||
'rb').read())
|
'rb').read())
|
||||||
dynamic.set('news_to_be_synced', set([]))
|
dynamic.set('news_to_be_synced', set([]))
|
||||||
if config['upload_news_to_device'] and files:
|
if config['upload_news_to_device'] and files:
|
||||||
remove = ids if \
|
remove = ids if del_on_upload else []
|
||||||
config['delete_news_from_library_on_upload'] else []
|
|
||||||
space = { self.location_view.model().free[0] : None,
|
space = { self.location_view.model().free[0] : None,
|
||||||
self.location_view.model().free[1] : 'carda',
|
self.location_view.model().free[1] : 'carda',
|
||||||
self.location_view.model().free[2] : 'cardb' }
|
self.location_view.model().free[2] : 'cardb' }
|
||||||
@ -906,7 +1105,7 @@ class DeviceGUI(object):
|
|||||||
self.upload_books(files, names, metadata,
|
self.upload_books(files, names, metadata,
|
||||||
on_card=on_card,
|
on_card=on_card,
|
||||||
memory=[files, remove])
|
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,
|
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]))
|
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
|
||||||
remove = remove_ids if delete_from_library else []
|
remove = remove_ids if delete_from_library else []
|
||||||
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
|
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 = []
|
auto = []
|
||||||
if _auto_ids != []:
|
if _auto_ids != []:
|
||||||
@ -1135,6 +1334,13 @@ class DeviceGUI(object):
|
|||||||
in cache['authors']:
|
in cache['authors']:
|
||||||
loc[i] = True
|
loc[i] = True
|
||||||
continue
|
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
|
return loc
|
||||||
|
|
||||||
def set_books_in_library(self, booklists, reset=False):
|
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)
|
mi = db.get_metadata(id, index_is_id=True)
|
||||||
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
||||||
if title not in self.db_book_title_cache:
|
if title not in self.db_book_title_cache:
|
||||||
self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}}
|
self.db_book_title_cache[title] = \
|
||||||
authors = authors_to_string(mi.authors).lower() if mi.authors else ''
|
{'authors':{}, 'author_sort':{}, 'db_ids':{}}
|
||||||
authors = re.sub('(?u)\W|[_]', '', authors)
|
if mi.authors:
|
||||||
self.db_book_title_cache[title]['authors'][authors] = mi
|
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_title_cache[title]['db_ids'][mi.application_id] = mi
|
||||||
self.db_book_uuid_cache.add(mi.uuid)
|
self.db_book_uuid_cache.add(mi.uuid)
|
||||||
|
|
||||||
@ -1181,12 +1393,19 @@ class DeviceGUI(object):
|
|||||||
book.smart_update(d['db_ids'][book.db_id])
|
book.smart_update(d['db_ids'][book.db_id])
|
||||||
resend_metadata = True
|
resend_metadata = True
|
||||||
continue
|
continue
|
||||||
book_authors = authors_to_string(book.authors).lower() if book.authors else ''
|
if book.authors:
|
||||||
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
# Compare against both author and author sort, because
|
||||||
if book_authors in d['authors']:
|
# either can appear as the author
|
||||||
book.in_library = True
|
book_authors = authors_to_string(book.authors).lower()
|
||||||
book.smart_update(d['authors'][book_authors])
|
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
||||||
resend_metadata = True
|
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
|
# Set author_sort if it isn't already
|
||||||
asort = getattr(book, 'author_sort', None)
|
asort = getattr(book, 'author_sort', None)
|
||||||
if not asort and book.authors:
|
if not asort and book.authors:
|
||||||
@ -1197,3 +1416,6 @@ class DeviceGUI(object):
|
|||||||
# Correct the metadata cache on device.
|
# Correct the metadata cache on device.
|
||||||
if self.device_manager.is_device_connected:
|
if self.device_manager.is_device_connected:
|
||||||
self.device_manager.sync_booklists(None, booklists)
|
self.device_manager.sync_booklists(None, booklists)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -49,12 +49,12 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
|
|
||||||
def open_book_path(self, path):
|
def open_book_path(self, path):
|
||||||
if os.sep in unicode(path):
|
if os.sep in unicode(path):
|
||||||
QDesktopServices.openUrl(QUrl('file:'+path))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
||||||
else:
|
else:
|
||||||
format = unicode(path)
|
format = unicode(path)
|
||||||
path = self.view.model().db.format_abspath(self.current_row, format)
|
path = self.view.model().db.format_abspath(self.current_row, format)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
QDesktopServices.openUrl(QUrl('file:'+path))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
||||||
|
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
@ -121,6 +121,7 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
f = f.strip()
|
f = f.strip()
|
||||||
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
|
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
|
||||||
for key in info.keys():
|
for key in info.keys():
|
||||||
|
if key == 'id': continue
|
||||||
txt = info[key]
|
txt = info[key]
|
||||||
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
||||||
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
||||||
|
@ -483,6 +483,14 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.port.editingFinished.connect(self.check_port_value)
|
self.port.editingFinished.connect(self.check_port_value)
|
||||||
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
|
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
|
||||||
True))
|
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):
|
def check_port_value(self, *args):
|
||||||
port = self.port.value()
|
port = self.port.value()
|
||||||
@ -863,6 +871,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
if self.viewer.item(i).checkState() == Qt.Checked:
|
if self.viewer.item(i).checkState() == Qt.Checked:
|
||||||
fmts.append(str(self.viewer.item(i).text()))
|
fmts.append(str(self.viewer.item(i).text()))
|
||||||
config['internally_viewed_formats'] = fmts
|
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):
|
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
||||||
d = error_dialog(self, _('Invalid database location'),
|
d = error_dialog(self, _('Invalid database location'),
|
||||||
|
@ -89,8 +89,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>608</width>
|
<width>604</width>
|
||||||
<height>683</height>
|
<height>679</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_7">
|
<layout class="QGridLayout" name="gridLayout_7">
|
||||||
@ -332,7 +332,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page">
|
<widget class="QWidget" name="page">
|
||||||
<layout class="QGridLayout" name="gridLayout_8">
|
<layout class="QGridLayout" name="gridLayout_8">
|
||||||
<item row="0" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QCheckBox" name="roman_numerals">
|
<widget class="QCheckBox" name="roman_numerals">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Use &Roman numerals for series number</string>
|
<string>Use &Roman numerals for series number</string>
|
||||||
@ -342,35 +342,35 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QCheckBox" name="systray_icon">
|
<widget class="QCheckBox" name="systray_icon">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable system &tray icon (needs restart)</string>
|
<string>Enable system &tray icon (needs restart)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QCheckBox" name="systray_notifications">
|
<widget class="QCheckBox" name="systray_notifications">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &notifications in system tray</string>
|
<string>Show &notifications in system tray</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0" colspan="2">
|
<item row="3" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="show_splash_screen">
|
<widget class="QCheckBox" name="show_splash_screen">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &splash screen at startup</string>
|
<string>Show &splash screen at startup</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0" colspan="2">
|
<item row="4" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="separate_cover_flow">
|
<widget class="QCheckBox" name="separate_cover_flow">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show cover &browser in a separate window (needs restart)</string>
|
<string>Show cover &browser in a separate window (needs restart)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QCheckBox" name="search_as_you_type">
|
<widget class="QCheckBox" name="search_as_you_type">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Search as you type</string>
|
<string>Search as you type</string>
|
||||||
@ -380,21 +380,21 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0" colspan="2">
|
<item row="6" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="sync_news">
|
<widget class="QCheckBox" name="sync_news">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Automatically send downloaded &news to ebook reader</string>
|
<string>Automatically send downloaded &news to ebook reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0" colspan="2">
|
<item row="7" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="delete_news">
|
<widget class="QCheckBox" name="delete_news">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0" colspan="2">
|
<item row="8" column="0" colspan="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_6">
|
<widget class="QLabel" name="label_6">
|
||||||
@ -411,7 +411,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="0" colspan="2">
|
<item row="9" column="0" colspan="2">
|
||||||
<widget class="QGroupBox" name="groupBox_2">
|
<widget class="QGroupBox" name="groupBox_2">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Toolbar</string>
|
<string>Toolbar</string>
|
||||||
@ -459,7 +459,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0" colspan="2">
|
<item row="10" column="0" colspan="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
@ -625,6 +625,26 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_17">
|
||||||
|
<property name="text">
|
||||||
|
<string>User Interface &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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page_6">
|
<widget class="QWidget" name="page_6">
|
||||||
|
@ -357,7 +357,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
aus = self.db.author_sort(row)
|
aus = self.db.author_sort(row)
|
||||||
self.author_sort.setText(aus if aus else '')
|
self.author_sort.setText(aus if aus else '')
|
||||||
tags = self.db.tags(row)
|
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())
|
self.tags.update_tags_cache(self.db.all_tags())
|
||||||
rating = self.db.rating(row)
|
rating = self.db.rating(row)
|
||||||
if rating > 0:
|
if rating > 0:
|
||||||
@ -527,6 +528,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.publisher.setCurrentIndex(idx)
|
self.publisher.setCurrentIndex(idx)
|
||||||
|
|
||||||
def edit_tags(self):
|
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 = TagEditor(self, self.db, self.row)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
if d.result() == QDialog.Accepted:
|
if d.result() == QDialog.Accepted:
|
||||||
|
424
src/calibre/gui2/init.py
Normal file
424
src/calibre/gui2/init.py
Normal 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()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
@ -7,11 +7,14 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Job management.
|
Job management.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from Queue import Empty, Queue
|
from Queue import Empty, Queue
|
||||||
|
|
||||||
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
|
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
|
||||||
QTimer, SIGNAL, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
|
QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
|
||||||
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip
|
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \
|
||||||
|
QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication
|
||||||
|
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.utils.ipc.job import ParallelJob
|
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.gui2.dialogs.jobs_ui import Ui_JobsDialog
|
||||||
from calibre import __appname__
|
from calibre import __appname__
|
||||||
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
|
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
|
||||||
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
|
|
||||||
class JobManager(QAbstractTableModel):
|
class JobManager(QAbstractTableModel):
|
||||||
|
|
||||||
|
job_added = pyqtSignal(int)
|
||||||
|
job_done = pyqtSignal(int)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QAbstractTableModel.__init__(self)
|
QAbstractTableModel.__init__(self)
|
||||||
self.wait_icon = QVariant(QIcon(I('jobs.svg')))
|
self.wait_icon = QVariant(QIcon(I('jobs.svg')))
|
||||||
@ -37,8 +44,7 @@ class JobManager(QAbstractTableModel):
|
|||||||
self.changed_queue = Queue()
|
self.changed_queue = Queue()
|
||||||
|
|
||||||
self.timer = QTimer(self)
|
self.timer = QTimer(self)
|
||||||
self.connect(self.timer, SIGNAL('timeout()'), self.update,
|
self.timer.timeout.connect(self.update, type=Qt.QueuedConnection)
|
||||||
Qt.QueuedConnection)
|
|
||||||
self.timer.start(1000)
|
self.timer.start(1000)
|
||||||
|
|
||||||
def columnCount(self, parent=QModelIndex()):
|
def columnCount(self, parent=QModelIndex()):
|
||||||
@ -130,8 +136,7 @@ class JobManager(QAbstractTableModel):
|
|||||||
for i, j in enumerate(self.jobs):
|
for i, j in enumerate(self.jobs):
|
||||||
if j.run_state == j.RUNNING:
|
if j.run_state == j.RUNNING:
|
||||||
idx = self.index(i, 3)
|
idx = self.index(i, 3)
|
||||||
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
|
self.dataChanged.emit(idx, idx)
|
||||||
idx, idx)
|
|
||||||
|
|
||||||
# Update parallel jobs
|
# Update parallel jobs
|
||||||
jobs = set([])
|
jobs = set([])
|
||||||
@ -157,20 +162,19 @@ class JobManager(QAbstractTableModel):
|
|||||||
self.jobs.sort()
|
self.jobs.sort()
|
||||||
self.reset()
|
self.reset()
|
||||||
if job.is_finished:
|
if job.is_finished:
|
||||||
self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs()))
|
self.job_done.emit(len(self.unfinished_jobs()))
|
||||||
else:
|
else:
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
idx = self.jobs.index(job)
|
idx = self.jobs.index(job)
|
||||||
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
|
self.dataChanged.emit(
|
||||||
self.index(idx, 0), self.index(idx, 3))
|
self.index(idx, 0), self.index(idx, 3))
|
||||||
|
|
||||||
|
|
||||||
def _add_job(self, job):
|
def _add_job(self, job):
|
||||||
self.emit(SIGNAL('layoutAboutToBeChanged()'))
|
self.layoutAboutToBeChanged.emit()
|
||||||
self.jobs.append(job)
|
self.jobs.append(job)
|
||||||
self.jobs.sort()
|
self.jobs.sort()
|
||||||
self.emit(SIGNAL('job_added(int)'), len(self.unfinished_jobs()))
|
self.job_added.emit(len(self.unfinished_jobs()))
|
||||||
self.emit(SIGNAL('layoutChanged()'))
|
|
||||||
|
|
||||||
def done_jobs(self):
|
def done_jobs(self):
|
||||||
return [j for j in self.jobs if j.is_finished]
|
return [j for j in self.jobs if j.is_finished]
|
||||||
@ -266,6 +270,80 @@ class DetailView(QDialog, Ui_Dialog):
|
|||||||
if more:
|
if more:
|
||||||
self.log.appendPlainText(more.decode('utf-8', 'replace'))
|
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):
|
class JobsDialog(QDialog, Ui_JobsDialog):
|
||||||
@ -278,14 +356,9 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.setWindowModality(Qt.NonModal)
|
self.setWindowModality(Qt.NonModal)
|
||||||
self.setWindowTitle(__appname__ + _(' - Jobs'))
|
self.setWindowTitle(__appname__ + _(' - Jobs'))
|
||||||
self.connect(self.kill_button, SIGNAL('clicked()'),
|
self.kill_button.clicked.connect(self.kill_job)
|
||||||
self.kill_job)
|
self.details_button.clicked.connect(self.show_details)
|
||||||
self.connect(self.details_button, SIGNAL('clicked()'),
|
self.stop_all_jobs_button.clicked.connect(self.kill_all_jobs)
|
||||||
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.pb_delegate = ProgressBarDelegate(self)
|
self.pb_delegate = ProgressBarDelegate(self)
|
||||||
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
|
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
|
||||||
self.jobs_view.doubleClicked.connect(self.show_job_details)
|
self.jobs_view.doubleClicked.connect(self.show_job_details)
|
||||||
@ -304,18 +377,18 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
|||||||
d.exec_()
|
d.exec_()
|
||||||
d.timer.stop()
|
d.timer.stop()
|
||||||
|
|
||||||
def kill_job(self):
|
def kill_job(self, *args):
|
||||||
for index in self.jobs_view.selectedIndexes():
|
for index in self.jobs_view.selectedIndexes():
|
||||||
row = index.row()
|
row = index.row()
|
||||||
self.model.kill_job(row, self)
|
self.model.kill_job(row, self)
|
||||||
return
|
return
|
||||||
|
|
||||||
def show_details(self):
|
def show_details(self, *args):
|
||||||
for index in self.jobs_view.selectedIndexes():
|
for index in self.jobs_view.selectedIndexes():
|
||||||
self.show_job_details(index)
|
self.show_job_details(index)
|
||||||
return
|
return
|
||||||
|
|
||||||
def kill_all_jobs(self):
|
def kill_all_jobs(self, *args):
|
||||||
self.model.kill_all_jobs()
|
self.model.kill_all_jobs()
|
||||||
|
|
||||||
def closeEvent(self, e):
|
def closeEvent(self, e):
|
||||||
|
@ -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.ebooks.metadata.meta import set_metadata as _set_metadata
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
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.constants import filesystem_encoding
|
||||||
from calibre.gui2.library import DEFAULT_SORT
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ class FormatPath(unicode):
|
|||||||
def __new__(cls, path, orig_file_path):
|
def __new__(cls, path, orig_file_path):
|
||||||
ans = unicode.__new__(cls, path)
|
ans = unicode.__new__(cls, path)
|
||||||
ans.orig_file_path = orig_file_path
|
ans.orig_file_path = orig_file_path
|
||||||
|
ans.deleted_after_upload = False
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
class BooksModel(QAbstractTableModel): # {{{
|
class BooksModel(QAbstractTableModel): # {{{
|
||||||
@ -199,7 +200,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.count_changed()
|
self.count_changed()
|
||||||
self.clear_caches()
|
self.clear_caches()
|
||||||
self.reset()
|
self.reset()
|
||||||
|
return ids
|
||||||
|
|
||||||
def delete_books_by_id(self, ids):
|
def delete_books_by_id(self, ids):
|
||||||
for id in ids:
|
for id in ids:
|
||||||
@ -299,6 +300,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
formats = _('None')
|
formats = _('None')
|
||||||
data[_('Formats')] = formats
|
data[_('Formats')] = formats
|
||||||
data[_('Path')] = self.db.abspath(idx)
|
data[_('Path')] = self.db.abspath(idx)
|
||||||
|
data['id'] = self.id(idx)
|
||||||
comments = self.db.comments(idx)
|
comments = self.db.comments(idx)
|
||||||
if not comments:
|
if not comments:
|
||||||
comments = _('None')
|
comments = _('None')
|
||||||
@ -307,7 +309,9 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
if series:
|
if series:
|
||||||
sidx = self.db.series_index(idx)
|
sidx = self.db.series_index(idx)
|
||||||
sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
|
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
|
return data
|
||||||
|
|
||||||
@ -881,6 +885,15 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
ans.extend(v)
|
ans.extend(v)
|
||||||
return ans
|
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):
|
def flags(self, index):
|
||||||
if self.map[index.row()] in self.indices_to_be_deleted():
|
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
|
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
|
||||||
|
@ -26,6 +26,15 @@ class BooksView(QTableView): # {{{
|
|||||||
|
|
||||||
def __init__(self, parent, modelcls=BooksModel):
|
def __init__(self, parent, modelcls=BooksModel):
|
||||||
QTableView.__init__(self, parent)
|
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.rating_delegate = RatingDelegate(self)
|
||||||
self.timestamp_delegate = DateDelegate(self)
|
self.timestamp_delegate = DateDelegate(self)
|
||||||
self.pubdate_delegate = PubDateDelegate(self)
|
self.pubdate_delegate = PubDateDelegate(self)
|
||||||
@ -283,7 +292,8 @@ class BooksView(QTableView): # {{{
|
|||||||
old_state['column_positions'][name] = i
|
old_state['column_positions'][name] = i
|
||||||
if name != 'ondevice':
|
if name != 'ondevice':
|
||||||
old_state['column_sizes'][name] = \
|
old_state['column_sizes'][name] = \
|
||||||
max(self.sizeHintForColumn(i), h.sectionSizeHint(i))
|
min(350, max(self.sizeHintForColumn(i),
|
||||||
|
h.sectionSizeHint(i)))
|
||||||
if name == 'timestamp':
|
if name == 'timestamp':
|
||||||
old_state['column_sizes'][name] += 12
|
old_state['column_sizes'][name] += 12
|
||||||
return old_state
|
return old_state
|
||||||
@ -434,6 +444,18 @@ class BooksView(QTableView): # {{{
|
|||||||
self.scrollTo(self.model().index(row, i))
|
self.scrollTo(self.model().index(row, i))
|
||||||
break
|
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):
|
def close(self):
|
||||||
self._model.close()
|
self._model.close()
|
||||||
|
|
||||||
|
@ -53,7 +53,24 @@ def init_qt(args):
|
|||||||
app.setWindowIcon(QIcon(I('library.png')))
|
app.setWindowIcon(QIcon(I('library.png')))
|
||||||
return app, opts, args, actions
|
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']
|
library_path = prefs['library_path']
|
||||||
if library_path is None: # Need to migrate to new database layout
|
if library_path is None: # Need to migrate to new database layout
|
||||||
base = os.path.expanduser('~')
|
base = os.path.expanduser('~')
|
||||||
@ -73,10 +90,12 @@ def get_library_path():
|
|||||||
try:
|
try:
|
||||||
os.makedirs(library_path)
|
os.makedirs(library_path)
|
||||||
except:
|
except:
|
||||||
error_dialog(None, _('Failed to create library'),
|
error_dialog(parent, _('Failed to create library'),
|
||||||
_('Failed to create calibre library at: %r. Aborting.')%library_path,
|
_('Failed to create calibre library at: %r.')%library_path,
|
||||||
det_msg=traceback.format_exc(), show=True)
|
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
|
return library_path
|
||||||
|
|
||||||
class DBRepair(QThread):
|
class DBRepair(QThread):
|
||||||
@ -159,22 +178,9 @@ class GuiRunner(QObject):
|
|||||||
'a new empty library.'),
|
'a new empty library.'),
|
||||||
det_msg=tb, show=True)
|
det_msg=tb, show=True)
|
||||||
if db is None:
|
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',
|
candidate = choose_dir(self.splash_screen, 'choose calibre library',
|
||||||
_('Choose a location for your new calibre e-book library'),
|
_('Choose a location for your new calibre e-book library'),
|
||||||
default_dir=x)
|
default_dir=get_default_library_path())
|
||||||
|
|
||||||
if not candidate:
|
if not candidate:
|
||||||
self.initialization_failed()
|
self.initialization_failed()
|
||||||
|
|
||||||
@ -236,8 +242,8 @@ class GuiRunner(QObject):
|
|||||||
if gprefs.get('show_splash_screen', True):
|
if gprefs.get('show_splash_screen', True):
|
||||||
self.show_splash_screen()
|
self.show_splash_screen()
|
||||||
|
|
||||||
self.library_path = get_library_path()
|
self.library_path = get_library_path(parent=self.splash_screen)
|
||||||
if self.library_path is None:
|
if not self.library_path:
|
||||||
self.initialization_failed()
|
self.initialization_failed()
|
||||||
|
|
||||||
self.initialize_db()
|
self.initialize_db()
|
||||||
|
@ -169,6 +169,12 @@
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="search_restriction">
|
<widget class="QComboBox" name="search_restriction">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>150</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Books display will be restricted to those matching the selected saved search</string>
|
<string>Books display will be restricted to those matching the selected saved search</string>
|
||||||
</property>
|
</property>
|
||||||
@ -304,270 +310,6 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</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 &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 &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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="tool_bar">
|
<widget class="QToolBar" name="tool_bar">
|
||||||
@ -804,26 +546,11 @@
|
|||||||
</action>
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
|
||||||
<class>BooksView</class>
|
|
||||||
<extends>QTableView</extends>
|
|
||||||
<header>calibre/gui2/library/views.h</header>
|
|
||||||
</customwidget>
|
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>LocationView</class>
|
<class>LocationView</class>
|
||||||
<extends>QListView</extends>
|
<extends>QListView</extends>
|
||||||
<header>widgets.h</header>
|
<header>widgets.h</header>
|
||||||
</customwidget>
|
</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>
|
<customwidget>
|
||||||
<class>SearchBox2</class>
|
<class>SearchBox2</class>
|
||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
@ -834,24 +561,6 @@
|
|||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
<header>calibre.gui2.search_box</header>
|
<header>calibre.gui2.search_box</header>
|
||||||
</customwidget>
|
</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>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../resources/images.qrc"/>
|
<include location="../../../resources/images.qrc"/>
|
||||||
|
@ -85,6 +85,10 @@ typedef long PFreal;
|
|||||||
|
|
||||||
typedef unsigned short QRgb565;
|
typedef unsigned short QRgb565;
|
||||||
|
|
||||||
|
#define REFLECTION_FACTOR 1.5
|
||||||
|
|
||||||
|
#define MAX(x, y) ((x > y) ? x : y)
|
||||||
|
|
||||||
#define RGB565_RED_MASK 0xF800
|
#define RGB565_RED_MASK 0xF800
|
||||||
#define RGB565_GREEN_MASK 0x07E0
|
#define RGB565_GREEN_MASK 0x07E0
|
||||||
#define RGB565_BLUE_MASK 0x001F
|
#define RGB565_BLUE_MASK 0x001F
|
||||||
@ -122,6 +126,7 @@ inline PFreal floatToFixed(float val)
|
|||||||
return (PFreal)(val*PFREAL_ONE);
|
return (PFreal)(val*PFREAL_ONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sinTable {{{
|
||||||
#define IANGLE_MAX 1024
|
#define IANGLE_MAX 1024
|
||||||
#define IANGLE_MASK 1023
|
#define IANGLE_MASK 1023
|
||||||
|
|
||||||
@ -291,6 +296,7 @@ int main(int, char**)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
// }}}
|
||||||
|
|
||||||
inline PFreal fsin(int iangle)
|
inline PFreal fsin(int iangle)
|
||||||
{
|
{
|
||||||
@ -313,6 +319,8 @@ struct SlideInfo
|
|||||||
PFreal cy;
|
PFreal cy;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PicturePlowPrivate {{{
|
||||||
|
|
||||||
class PictureFlowPrivate
|
class PictureFlowPrivate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -367,6 +375,7 @@ private:
|
|||||||
|
|
||||||
int slideWidth;
|
int slideWidth;
|
||||||
int slideHeight;
|
int slideHeight;
|
||||||
|
int fontSize;
|
||||||
int zoom;
|
int zoom;
|
||||||
int queueLength;
|
int queueLength;
|
||||||
|
|
||||||
@ -404,6 +413,7 @@ PictureFlowPrivate::PictureFlowPrivate(PictureFlow* w, int queueLength_)
|
|||||||
|
|
||||||
slideWidth = 200;
|
slideWidth = 200;
|
||||||
slideHeight = 200;
|
slideHeight = 200;
|
||||||
|
fontSize = 10;
|
||||||
zoom = 100;
|
zoom = 100;
|
||||||
|
|
||||||
centerIndex = 0;
|
centerIndex = 0;
|
||||||
@ -540,6 +550,11 @@ void PictureFlowPrivate::showSlide(int index)
|
|||||||
|
|
||||||
void PictureFlowPrivate::resize(int w, int h)
|
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);
|
recalc(w, h);
|
||||||
resetSlides();
|
resetSlides();
|
||||||
triggerRender();
|
triggerRender();
|
||||||
@ -588,8 +603,8 @@ static QImage prepareSurface(QImage img, int w, int h)
|
|||||||
img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
|
img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
|
||||||
|
|
||||||
// slightly larger, to accomodate for the reflection
|
// slightly larger, to accomodate for the reflection
|
||||||
int hs = h * 2;
|
int hs = int(h * REFLECTION_FACTOR);
|
||||||
int hofs = h / 3;
|
int hofs = 0;
|
||||||
|
|
||||||
// offscreen buffer: black is sweet
|
// offscreen buffer: black is sweet
|
||||||
QImage result(hs, w, QImage::Format_RGB16);
|
QImage result(hs, w, QImage::Format_RGB16);
|
||||||
@ -709,14 +724,15 @@ void PictureFlowPrivate::render()
|
|||||||
QPainter painter;
|
QPainter painter;
|
||||||
painter.begin(&buffer);
|
painter.begin(&buffer);
|
||||||
|
|
||||||
QFont font("Arial", 14);
|
QFont font = QFont();
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
|
font.setPixelSize(fontSize);
|
||||||
painter.setFont(font);
|
painter.setFont(font);
|
||||||
painter.setPen(Qt::white);
|
painter.setPen(Qt::white);
|
||||||
//painter.setPen(QColor(255,255,255,127));
|
//painter.setPen(QColor(255,255,255,127));
|
||||||
|
|
||||||
if (centerIndex < slideCount() && centerIndex > -1)
|
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));
|
Qt::AlignCenter, slideImages->caption(centerIndex));
|
||||||
|
|
||||||
painter.end();
|
painter.end();
|
||||||
@ -759,8 +775,9 @@ void PictureFlowPrivate::render()
|
|||||||
QPainter painter;
|
QPainter painter;
|
||||||
painter.begin(&buffer);
|
painter.begin(&buffer);
|
||||||
|
|
||||||
QFont font("Arial", 14);
|
QFont font = QFont();
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
|
font.setPixelSize(fontSize);
|
||||||
painter.setFont(font);
|
painter.setFont(font);
|
||||||
|
|
||||||
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
|
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
|
||||||
@ -768,12 +785,12 @@ void PictureFlowPrivate::render()
|
|||||||
|
|
||||||
painter.setPen(QColor(255,255,255, (255-fade) ));
|
painter.setPen(QColor(255,255,255, (255-fade) ));
|
||||||
if (leftTextIndex < sc && leftTextIndex > -1)
|
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));
|
Qt::AlignCenter, slideImages->caption(leftTextIndex));
|
||||||
|
|
||||||
painter.setPen(QColor(255,255,255, fade));
|
painter.setPen(QColor(255,255,255, fade));
|
||||||
if (leftTextIndex+1 < sc && leftTextIndex > -2)
|
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));
|
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
|
||||||
|
|
||||||
|
|
||||||
@ -887,7 +904,7 @@ int col1, int col2)
|
|||||||
int center = (sh*BILINEAR_STRETCH_VER/2);
|
int center = (sh*BILINEAR_STRETCH_VER/2);
|
||||||
int dy = dist*BILINEAR_STRETCH_VER / h;
|
int dy = dist*BILINEAR_STRETCH_VER / h;
|
||||||
#else
|
#else
|
||||||
int center = (sh/2);
|
int center = sh/2;
|
||||||
int dy = dist / h;
|
int dy = dist / h;
|
||||||
#endif
|
#endif
|
||||||
int p1 = center*PFREAL_ONE - dy/2;
|
int p1 = center*PFREAL_ONE - dy/2;
|
||||||
@ -1104,8 +1121,9 @@ void PictureFlowPrivate::clearSurfaceCache()
|
|||||||
surfaceCache.clear();
|
surfaceCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
// }}}
|
||||||
|
|
||||||
|
// PictureFlow {{{
|
||||||
PictureFlow::PictureFlow(QWidget* parent, int queueLength): QWidget(parent)
|
PictureFlow::PictureFlow(QWidget* parent, int queueLength): QWidget(parent)
|
||||||
{
|
{
|
||||||
d = new PictureFlowPrivate(this, queueLength);
|
d = new PictureFlowPrivate(this, queueLength);
|
||||||
@ -1381,3 +1399,5 @@ void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); }
|
|||||||
int FlowImages::count() { return 0; }
|
int FlowImages::count() { return 0; }
|
||||||
QImage FlowImages::image(int index) { index=0; return QImage(); }
|
QImage FlowImages::image(int index) { index=0; return QImage(); }
|
||||||
QString FlowImages::caption(int index) {index=0; return QString(); }
|
QString FlowImages::caption(int index) {index=0; return QString(); }
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
@ -115,7 +115,8 @@ public:
|
|||||||
QSize slideSize() const;
|
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);
|
void setSlideSize(QSize size);
|
||||||
|
|
||||||
|
@ -16,6 +16,10 @@ public:
|
|||||||
virtual int count();
|
virtual int count();
|
||||||
virtual QImage image(int index);
|
virtual QImage image(int index);
|
||||||
virtual QString caption(int index);
|
virtual QString caption(int index);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void dataChanged();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,11 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
|
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
|
||||||
pyqtSignal, SIGNAL
|
pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \
|
||||||
from PyQt4.QtGui import QCompleter
|
QAction, QKeySequence
|
||||||
|
|
||||||
from calibre.gui2 import config
|
from calibre.gui2 import config
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
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):
|
class SearchLineEdit(QLineEdit):
|
||||||
|
|
||||||
@ -69,7 +73,7 @@ class SearchBox2(QComboBox):
|
|||||||
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
|
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
|
||||||
self.mouse_released, Qt.DirectConnection)
|
self.mouse_released, Qt.DirectConnection)
|
||||||
self.setEditable(True)
|
self.setEditable(True)
|
||||||
self.help_state = True
|
self.help_state = False
|
||||||
self.as_you_type = True
|
self.as_you_type = True
|
||||||
self.prev_search = ''
|
self.prev_search = ''
|
||||||
self.timer = None
|
self.timer = None
|
||||||
@ -79,8 +83,7 @@ class SearchBox2(QComboBox):
|
|||||||
self.setMinimumContentsLength(25)
|
self.setMinimumContentsLength(25)
|
||||||
self._in_a_search = False
|
self._in_a_search = False
|
||||||
|
|
||||||
def initialize(self, opt_name, colorize=False,
|
def initialize(self, opt_name, colorize=False, help_text=_('Search')):
|
||||||
help_text=_('Search')):
|
|
||||||
self.as_you_type = config['search_as_you_type']
|
self.as_you_type = config['search_as_you_type']
|
||||||
self.opt_name = opt_name
|
self.opt_name = opt_name
|
||||||
self.addItems(QStringList(list(set(config[opt_name]))))
|
self.addItems(QStringList(list(set(config[opt_name]))))
|
||||||
@ -97,6 +100,9 @@ class SearchBox2(QComboBox):
|
|||||||
self.help_state = False
|
self.help_state = False
|
||||||
|
|
||||||
def clear_to_help(self):
|
def clear_to_help(self):
|
||||||
|
if self.help_state:
|
||||||
|
return
|
||||||
|
self.help_state = True
|
||||||
self.search.emit('')
|
self.search.emit('')
|
||||||
self._in_a_search = False
|
self._in_a_search = False
|
||||||
self.setEditText(self.help_text)
|
self.setEditText(self.help_text)
|
||||||
@ -104,7 +110,6 @@ class SearchBox2(QComboBox):
|
|||||||
self.killTimer(self.timer)
|
self.killTimer(self.timer)
|
||||||
self.timer = None
|
self.timer = None
|
||||||
self.line_edit.home(False)
|
self.line_edit.home(False)
|
||||||
self.help_state = True
|
|
||||||
self.line_edit.setStyleSheet(
|
self.line_edit.setStyleSheet(
|
||||||
'QLineEdit { color: gray; background-color: %s; }' %
|
'QLineEdit { color: gray; background-color: %s; }' %
|
||||||
self.normal_background)
|
self.normal_background)
|
||||||
@ -239,9 +244,9 @@ class SavedSearchBox(QComboBox):
|
|||||||
self.setInsertPolicy(self.NoInsert)
|
self.setInsertPolicy(self.NoInsert)
|
||||||
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
||||||
self.setMinimumContentsLength(10)
|
self.setMinimumContentsLength(10)
|
||||||
|
self.tool_tip_text = self.toolTip()
|
||||||
|
|
||||||
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
|
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
|
||||||
self.tool_tip_text = self.toolTip()
|
|
||||||
self.saved_searches = _saved_searches
|
self.saved_searches = _saved_searches
|
||||||
self.search_box = _search_box
|
self.search_box = _search_box
|
||||||
self.help_text = help_text
|
self.help_text = help_text
|
||||||
@ -331,3 +336,75 @@ class SavedSearchBox(QComboBox):
|
|||||||
if idx < 0:
|
if idx < 0:
|
||||||
return
|
return
|
||||||
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
57
src/calibre/gui2/search_restriction_mixin.py
Normal file
57
src/calibre/gui2/search_restriction_mixin.py
Normal 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)
|
@ -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)
|
|
||||||
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
import os, collections
|
|
||||||
|
|
||||||
from PyQt4.QtGui import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
|
import os
|
||||||
QSizePolicy, QScrollArea
|
|
||||||
from PyQt4.QtCore import Qt, QSize, pyqtSignal
|
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 import fit_image, preferred_encoding, isosx
|
||||||
from calibre.gui2 import config
|
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.gui2.notify import get_notifier
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
|
from calibre.gui2.book_details import render_rows
|
||||||
|
|
||||||
class BookInfoDisplay(QWidget):
|
class BookInfoDisplay(QWidget):
|
||||||
|
|
||||||
@ -46,10 +49,14 @@ class BookInfoDisplay(QWidget):
|
|||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
|
||||||
|
|
||||||
class BookCoverDisplay(QLabel):
|
class BookCoverDisplay(QLabel): # {{{
|
||||||
|
|
||||||
def __init__(self, coverpath=I('book.svg')):
|
def __init__(self, coverpath=I('book.svg')):
|
||||||
QLabel.__init__(self)
|
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.setMaximumWidth(81)
|
||||||
self.setMaximumHeight(108)
|
self.setMaximumHeight(108)
|
||||||
self.default_pixmap = QPixmap(coverpath)
|
self.default_pixmap = QPixmap(coverpath)
|
||||||
@ -58,21 +65,23 @@ class BookInfoDisplay(QWidget):
|
|||||||
self.setPixmap(self.default_pixmap)
|
self.setPixmap(self.default_pixmap)
|
||||||
|
|
||||||
def do_layout(self):
|
def do_layout(self):
|
||||||
|
self.animation.stop()
|
||||||
pixmap = self.pixmap()
|
pixmap = self.pixmap()
|
||||||
pwidth, pheight = pixmap.width(), pixmap.height()
|
pwidth, pheight = pixmap.width(), pixmap.height()
|
||||||
width, height = fit_image(pwidth, pheight,
|
width, height = fit_image(pwidth, pheight,
|
||||||
pwidth, self.statusbar_height-12)[1:]
|
pwidth, self.statusbar_height-20)[1:]
|
||||||
self.setMaximumHeight(height)
|
self.setMaximumHeight(height)
|
||||||
try:
|
try:
|
||||||
aspect_ratio = pwidth/float(pheight)
|
aspect_ratio = pwidth/float(pheight)
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
aspect_ratio = 1
|
aspect_ratio = 1
|
||||||
self.setMaximumWidth(int(aspect_ratio*self.maximumHeight()))
|
self.setMaximumWidth(int(aspect_ratio*self.maximumHeight()))
|
||||||
|
self.animation.setEndValue(self.maximumSize())
|
||||||
|
|
||||||
def setPixmap(self, pixmap):
|
def setPixmap(self, pixmap):
|
||||||
QLabel.setPixmap(self, pixmap)
|
QLabel.setPixmap(self, pixmap)
|
||||||
self.do_layout()
|
self.do_layout()
|
||||||
|
self.animation.start()
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
return QSize(self.maximumWidth(), self.maximumHeight())
|
return QSize(self.maximumWidth(), self.maximumHeight())
|
||||||
@ -81,27 +90,31 @@ class BookInfoDisplay(QWidget):
|
|||||||
self.statusbar_height = statusbar_size.height()
|
self.statusbar_height = statusbar_size.height()
|
||||||
self.do_layout()
|
self.do_layout()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class BookDataDisplay(QLabel):
|
class BookDataDisplay(QLabel):
|
||||||
|
|
||||||
mr = pyqtSignal(int)
|
mr = pyqtSignal(object)
|
||||||
|
link_clicked = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QLabel.__init__(self)
|
QLabel.__init__(self)
|
||||||
self.setText('')
|
self.setText('')
|
||||||
self.setWordWrap(True)
|
self.setWordWrap(True)
|
||||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
|
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
|
||||||
|
self.linkActivated.connect(self.link_activated)
|
||||||
|
self._link_clicked = False
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
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)
|
def link_activated(self, link):
|
||||||
WEIGHTS[_('Path')] = 0
|
self._link_clicked = True
|
||||||
WEIGHTS[_('Formats')] = 1
|
link = unicode(link)
|
||||||
WEIGHTS[_('Collections')] = 2
|
self.link_clicked.emit(link)
|
||||||
WEIGHTS[_('Series')] = 3
|
|
||||||
WEIGHTS[_('Tags')] = 4
|
|
||||||
WEIGHTS[_('Comments')] = 5
|
|
||||||
|
|
||||||
show_book_info = pyqtSignal()
|
show_book_info = pyqtSignal()
|
||||||
|
|
||||||
@ -122,6 +135,7 @@ class BookInfoDisplay(QWidget):
|
|||||||
self._layout.setAlignment(self.cover_display, Qt.AlignTop|Qt.AlignLeft)
|
self._layout.setAlignment(self.cover_display, Qt.AlignTop|Qt.AlignLeft)
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
|
ev.accept()
|
||||||
self.show_book_info.emit()
|
self.show_book_info.emit()
|
||||||
|
|
||||||
def show_data(self, data):
|
def show_data(self, data):
|
||||||
@ -133,24 +147,12 @@ class BookInfoDisplay(QWidget):
|
|||||||
rows, comments = [], ''
|
rows, comments = [], ''
|
||||||
self.book_data.setText('')
|
self.book_data.setText('')
|
||||||
self.data = data.copy()
|
self.data = data.copy()
|
||||||
keys = data.keys()
|
rows = render_rows(self.data)
|
||||||
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 = '\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
|
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])
|
k, t in rows])
|
||||||
if comments:
|
if _('Comments') in self.data:
|
||||||
comments = '<b>Comments:</b>'+comments
|
comments = comments_to_html(self.data[_('Comments')])
|
||||||
|
comments = ('<b>%s:</b>'%_('Comments'))+comments
|
||||||
left_pane = u'<table>%s</table>'%rows
|
left_pane = u'<table>%s</table>'%rows
|
||||||
right_pane = u'<div>%s</div>'%comments
|
right_pane = u'<div>%s</div>'%comments
|
||||||
self.book_data.setText(u'<table><tr><td valign="top" '
|
self.book_data.setText(u'<table><tr><td valign="top" '
|
||||||
@ -162,17 +164,54 @@ class BookInfoDisplay(QWidget):
|
|||||||
self.updateGeometry()
|
self.updateGeometry()
|
||||||
self.setVisible(True)
|
self.setVisible(True)
|
||||||
|
|
||||||
|
class StatusBarInterface(object):
|
||||||
class StatusBar(QStatusBar):
|
|
||||||
|
|
||||||
resized = pyqtSignal(object)
|
|
||||||
files_dropped = pyqtSignal(object, object)
|
|
||||||
show_book_info = pyqtSignal()
|
|
||||||
|
|
||||||
def initialize(self, systray=None):
|
def initialize(self, systray=None):
|
||||||
self.systray = systray
|
self.systray = systray
|
||||||
self.notifier = get_notifier(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.book_info.setAcceptDrops(True)
|
||||||
self.scroll_area = QScrollArea()
|
self.scroll_area = QScrollArea()
|
||||||
self.scroll_area.setWidget(self.book_info)
|
self.scroll_area.setWidget(self.book_info)
|
||||||
@ -181,26 +220,31 @@ class StatusBar(QStatusBar):
|
|||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
self.book_info.files_dropped.connect(self.files_dropped.emit,
|
self.book_info.files_dropped.connect(self.files_dropped.emit,
|
||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
|
self.book_info.book_data.link_clicked.connect(self._link_clicked)
|
||||||
self.addWidget(self.scroll_area, 100)
|
self.addWidget(self.scroll_area, 100)
|
||||||
self.setMinimumHeight(120)
|
self.setMinimumHeight(120)
|
||||||
self.resized.connect(self.book_info.cover_display.relayout)
|
self.resized.connect(self.book_info.cover_display.relayout)
|
||||||
self.book_info.cover_display.relayout(self.size())
|
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):
|
def resizeEvent(self, ev):
|
||||||
self.resized.emit(self.size())
|
self.resized.emit(self.size())
|
||||||
|
|
||||||
def reset_info(self):
|
def reset_info(self):
|
||||||
self.book_info.show_data({})
|
self.book_info.show_data({})
|
||||||
|
|
||||||
def showMessage(self, msg, timeout=0):
|
def show_data(self, data):
|
||||||
ret = QStatusBar.showMessage(self, msg, timeout)
|
self.book_info.show_data(data)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,14 +10,18 @@ Browsing book collection by tags.
|
|||||||
from itertools import izip
|
from itertools import izip
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
|
||||||
QFont, QSize, QIcon, QPoint, \
|
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
|
||||||
QAbstractItemModel, QVariant, QModelIndex, QMenu
|
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
|
||||||
|
QPushButton, QWidget
|
||||||
|
|
||||||
from calibre.gui2 import config, NONE
|
from calibre.gui2 import config, NONE
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.library.field_metadata import TagsIcons
|
from calibre.library.field_metadata import TagsIcons
|
||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
from calibre.gui2 import error_dialog
|
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): # {{{
|
class TagsView(QTreeView): # {{{
|
||||||
|
|
||||||
@ -29,12 +33,16 @@ class TagsView(QTreeView): # {{{
|
|||||||
tag_item_renamed = pyqtSignal()
|
tag_item_renamed = pyqtSignal()
|
||||||
search_item_renamed = pyqtSignal()
|
search_item_renamed = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, parent=None):
|
||||||
QTreeView.__init__(self, *args)
|
QTreeView.__init__(self, parent=None)
|
||||||
|
self.tag_match = None
|
||||||
self.setUniformRowHeights(True)
|
self.setUniformRowHeights(True)
|
||||||
self.setCursor(Qt.PointingHandCursor)
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
self.setIconSize(QSize(30, 30))
|
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):
|
def set_database(self, db, tag_match, popularity):
|
||||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
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
@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from PyQt4.QtCore import QThread, pyqtSignal
|
from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt
|
||||||
import mechanize
|
import mechanize
|
||||||
|
|
||||||
from calibre.constants import __version__, iswindows, isosx
|
from calibre.constants import __appname__, __version__, iswindows, isosx
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.gui2 import config, dynamic, question_dialog
|
||||||
|
|
||||||
URL = 'http://status.calibre-ebook.com/latest'
|
URL = 'http://status.calibre-ebook.com/latest'
|
||||||
|
|
||||||
@ -36,3 +37,35 @@ class CheckForUpdates(QThread):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.sleep(self.INTERVAL)
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -229,9 +229,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.connect(self.action_previous_page, SIGNAL('triggered(bool)'),
|
self.connect(self.action_previous_page, SIGNAL('triggered(bool)'),
|
||||||
lambda x:self.view.previous_page())
|
lambda x:self.view.previous_page())
|
||||||
self.connect(self.action_find_next, SIGNAL('triggered(bool)'),
|
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)'),
|
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))
|
repeat=True, backwards=True))
|
||||||
|
|
||||||
self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),
|
self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),
|
||||||
|
@ -4,16 +4,18 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
Miscellaneous widgets used in the GUI
|
Miscellaneous widgets used in the GUI
|
||||||
'''
|
'''
|
||||||
import re, os, traceback
|
import re, os, traceback
|
||||||
|
|
||||||
from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
|
from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
|
||||||
QListWidgetItem, QTextCharFormat, QApplication, \
|
QListWidgetItem, QTextCharFormat, QApplication, \
|
||||||
QSyntaxHighlighter, QCursor, QColor, QWidget, \
|
QSyntaxHighlighter, QCursor, QColor, QWidget, \
|
||||||
QPixmap, QPalette, QSplitterHandle, \
|
QPixmap, QPalette, QSplitterHandle, QToolButton, \
|
||||||
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
||||||
QRegExp, QSettings, QSize, QModelIndex, QSplitter, \
|
QRegExp, QSettings, QSize, QModelIndex, QSplitter, \
|
||||||
QAbstractButton, QPainter, QLineEdit, QComboBox, \
|
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.gui2.filename_pattern_ui import Ui_Form
|
||||||
from calibre import fit_image, human_readable
|
from calibre import fit_image, human_readable
|
||||||
@ -927,6 +929,7 @@ class SplitterHandle(QSplitterHandle):
|
|||||||
self.double_clicked.connect(splitter.double_clicked,
|
self.double_clicked.connect(splitter.double_clicked,
|
||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
self.highlight = False
|
self.highlight = False
|
||||||
|
self.setToolTip(_('Drag to resize')+' '+splitter.label)
|
||||||
|
|
||||||
def splitter_moved(self, *args):
|
def splitter_moved(self, *args):
|
||||||
oh = self.highlight
|
oh = self.highlight
|
||||||
@ -944,20 +947,62 @@ class SplitterHandle(QSplitterHandle):
|
|||||||
def mouseDoubleClickEvent(self, ev):
|
def mouseDoubleClickEvent(self, ev):
|
||||||
self.double_clicked.emit(self)
|
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):
|
class Splitter(QSplitter):
|
||||||
|
|
||||||
state_changed = pyqtSignal(object)
|
state_changed = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, name, label, icon, initial_show=True,
|
||||||
QSplitter.__init__(self, *args)
|
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.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):
|
def createHandle(self):
|
||||||
return SplitterHandle(self.orientation(), self)
|
return SplitterHandle(self.orientation(), self)
|
||||||
|
|
||||||
def initialize(self, name=None):
|
def initialize(self):
|
||||||
if name is not None:
|
|
||||||
self._name = name
|
|
||||||
for i in range(self.count()):
|
for i in range(self.count()):
|
||||||
h = self.handle(i)
|
h = self.handle(i)
|
||||||
if h is not None:
|
if h is not None:
|
||||||
@ -965,40 +1010,117 @@ class Splitter(QSplitter):
|
|||||||
self.state_changed.emit(not self.is_side_index_hidden)
|
self.state_changed.emit(not self.is_side_index_hidden)
|
||||||
|
|
||||||
def splitter_moved(self, *args):
|
def splitter_moved(self, *args):
|
||||||
|
self.desired_side_size = self.side_index_size
|
||||||
self.state_changed.emit(not self.is_side_index_hidden)
|
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
|
@property
|
||||||
def is_side_index_hidden(self):
|
def is_side_index_hidden(self):
|
||||||
sizes = list(self.sizes())
|
sizes = list(self.sizes())
|
||||||
return sizes[self.side_index] == 0
|
return sizes[self.side_index] == 0
|
||||||
|
|
||||||
def toggle_side_index(self):
|
@property
|
||||||
self.double_clicked(None)
|
def save_name(self):
|
||||||
|
ori = 'horizontal' if self.orientation() == Qt.Horizontal \
|
||||||
|
else 'vertical'
|
||||||
|
return self._name + '_' + ori
|
||||||
|
|
||||||
def double_clicked(self, handle):
|
def print_sizes(self):
|
||||||
visible = not self.is_side_index_hidden
|
if self.count() > 1:
|
||||||
sizes = list(self.sizes())
|
print self.save_name, 'side:', self.side_index_size, 'other:',
|
||||||
if 0 in sizes:
|
print list(self.sizes())[self.other_index]
|
||||||
idx = sizes.index(0)
|
|
||||||
sizes[idx] = 80
|
|
||||||
else:
|
|
||||||
sizes[self.side_index] = 0
|
|
||||||
|
|
||||||
if visible:
|
@dynamic_property
|
||||||
dynamic.set(self._name + '_last_open_state', str(self.saveState()))
|
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)
|
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:
|
else:
|
||||||
state = dynamic.get(self._name+ '_last_open_state', None)
|
self.side_index_size = 0
|
||||||
if state is not None:
|
self.desired_show = state[0]
|
||||||
self.restoreState(state)
|
|
||||||
else:
|
|
||||||
self.setSizes(sizes)
|
|
||||||
self.initialize()
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -96,14 +96,14 @@ class Kobo(Device):
|
|||||||
class Booq(Device):
|
class Booq(Device):
|
||||||
name = 'Booq Reader'
|
name = 'Booq Reader'
|
||||||
manufacturer = 'Booq'
|
manufacturer = 'Booq'
|
||||||
output_profile = 'prs505'
|
output_profile = 'sony'
|
||||||
output_format = 'EPUB'
|
output_format = 'EPUB'
|
||||||
id = 'booq'
|
id = 'booq'
|
||||||
|
|
||||||
class TheBook(Device):
|
class TheBook(Device):
|
||||||
name = 'The Book'
|
name = 'The Book'
|
||||||
manufacturer = 'Augen'
|
manufacturer = 'Augen'
|
||||||
output_profile = 'prs505'
|
output_profile = 'sony'
|
||||||
output_format = 'EPUB'
|
output_format = 'EPUB'
|
||||||
id = 'thebook'
|
id = 'thebook'
|
||||||
|
|
||||||
|
@ -10,14 +10,14 @@ import collections, glob, os, re, itertools, functools
|
|||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from PyQt4.QtCore import QThread, QReadWriteLock
|
from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt
|
||||||
from PyQt4.QtGui import QImage
|
|
||||||
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.utils.pyparsing import ParseException
|
from calibre.utils.pyparsing import ParseException
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
|
from calibre import fit_image
|
||||||
|
|
||||||
class CoverCache(QThread):
|
class CoverCache(QThread):
|
||||||
|
|
||||||
@ -96,6 +96,11 @@ class CoverCache(QThread):
|
|||||||
img.loadFromData(data)
|
img.loadFromData(data)
|
||||||
if img.isNull():
|
if img.isNull():
|
||||||
continue
|
continue
|
||||||
|
scaled, nwidth, nheight = fit_image(img.width(),
|
||||||
|
img.height(), 600, 800)
|
||||||
|
if scaled:
|
||||||
|
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
|
||||||
|
Qt.SmoothTransformation)
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
self.cache_lock.lockForWrite()
|
self.cache_lock.lockForWrite()
|
||||||
@ -619,9 +624,12 @@ class ResultCache(SearchQueryParser):
|
|||||||
if self.first_sort:
|
if self.first_sort:
|
||||||
subsort = True
|
subsort = True
|
||||||
self.first_sort = False
|
self.first_sort = False
|
||||||
fcmp = self.seriescmp if field == 'series' else \
|
fcmp = self.seriescmp \
|
||||||
functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort,
|
if field == 'series' and \
|
||||||
asstr=as_string)
|
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.sort(cmp=fcmp, reverse=not ascending)
|
||||||
self._map_filtered = [id for id in self._map if id in self._map_filtered]
|
self._map_filtered = [id for id in self._map if id in self._map_filtered]
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ class CSV_XML(CatalogPlugin):
|
|||||||
|
|
||||||
if not len(data):
|
if not len(data):
|
||||||
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
|
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
|
# Get the requested output fields as a list
|
||||||
fields = self.get_output_fields(opts)
|
fields = self.get_output_fields(opts)
|
||||||
|
@ -28,7 +28,7 @@ from calibre.customize.ui import run_plugins_on_import
|
|||||||
|
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
|
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.utils.search_query_parser import saved_searches
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
||||||
from calibre.utils.magick_draw import save_cover_data_to
|
from calibre.utils.magick_draw import save_cover_data_to
|
||||||
@ -736,8 +736,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
icon=icon, tooltip = tooltip)
|
icon=icon, tooltip = tooltip)
|
||||||
for r in data if item_not_zero_func(r)]
|
for r in data if item_not_zero_func(r)]
|
||||||
if category == 'series' and not sort_on_count:
|
if category == 'series' and not sort_on_count:
|
||||||
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(),
|
if tweaks['title_series_sorting'] == 'library_order':
|
||||||
title_sort(y.name).lower()))
|
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
|
# We delayed computing the standard formats category because it does not
|
||||||
# use a view, but is computed dynamically
|
# use a view, but is computed dynamically
|
||||||
@ -950,7 +954,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
title = title.decode(preferred_encoding, 'replace')
|
title = title.decode(preferred_encoding, 'replace')
|
||||||
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
|
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['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.set_path(id, True)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
if notify:
|
if notify:
|
||||||
@ -1297,12 +1304,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
|
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.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
|
||||||
self.conn.commit()
|
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)
|
self.data.set(id, self.FIELD_MAP['series'], series, row_is_id=True)
|
||||||
if notify:
|
if notify:
|
||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
@ -1835,6 +1836,8 @@ books_series_link feeds
|
|||||||
os.remove(self.dbpath)
|
os.remove(self.dbpath)
|
||||||
shutil.copyfile(dest, self.dbpath)
|
shutil.copyfile(dest, self.dbpath)
|
||||||
self.connect()
|
self.connect()
|
||||||
|
self.field_metadata.remove_dynamic_categories()
|
||||||
|
self.field_metadata.remove_custom_fields()
|
||||||
self.initialize_dynamic()
|
self.initialize_dynamic()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
if os.path.exists(dest):
|
if os.path.exists(dest):
|
||||||
|
@ -379,6 +379,17 @@ class FieldMetadata(dict):
|
|||||||
self._add_search_terms_to_map(key, [key])
|
self._add_search_terms_to_map(key, [key])
|
||||||
self.custom_label_to_key_map[label] = 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):
|
def add_user_category(self, label, name):
|
||||||
if label in self._tb_cats:
|
if label in self._tb_cats:
|
||||||
raise ValueError('Duplicate user field [%s]'%(label))
|
raise ValueError('Duplicate user field [%s]'%(label))
|
||||||
|
@ -291,4 +291,8 @@ class SchemaUpgrade(object):
|
|||||||
|
|
||||||
for field in self.field_metadata.itervalues():
|
for field in self.field_metadata.itervalues():
|
||||||
if field['is_category'] and not field['is_custom'] and 'link_column' in field:
|
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'])
|
||||||
|
@ -60,6 +60,7 @@ class ContentServer(object):
|
|||||||
items.sort(cmp=self.seriescmp, reverse=not order)
|
items.sort(cmp=self.seriescmp, reverse=not order)
|
||||||
else:
|
else:
|
||||||
lookup = 'sort' if field == 'title' else field
|
lookup = 'sort' if field == 'title' else field
|
||||||
|
lookup = 'author_sort' if field == 'authors' else field
|
||||||
field = self.db.FIELD_MAP[lookup]
|
field = self.db.FIELD_MAP[lookup]
|
||||||
getter = operator.itemgetter(field)
|
getter = operator.itemgetter(field)
|
||||||
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
|
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
|
||||||
|
@ -15,6 +15,7 @@ from threading import RLock
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import parse_date, isoformat
|
from calibre.utils.date import parse_date, isoformat
|
||||||
|
|
||||||
global_lock = RLock()
|
global_lock = RLock()
|
||||||
@ -115,7 +116,10 @@ class DBThread(Thread):
|
|||||||
self.conn.create_aggregate('concat', 1, Concatenate)
|
self.conn.create_aggregate('concat', 1, Concatenate)
|
||||||
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
|
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
|
||||||
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
|
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()))
|
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
|
||||||
# Dummy functions for dynamically created filters
|
# Dummy functions for dynamically created filters
|
||||||
self.conn.create_function('books_list_filter', 1, lambda x: 1)
|
self.conn.create_function('books_list_filter', 1, lambda x: 1)
|
||||||
|
@ -411,7 +411,8 @@ def options(option_parser):
|
|||||||
def opts_and_words(name, op, words):
|
def opts_and_words(name, op, words):
|
||||||
opts = '|'.join(options(op))
|
opts = '|'.join(options(op))
|
||||||
words = '|'.join([w.replace("'", "\\'") for w in words])
|
words = '|'.join([w.replace("'", "\\'") for w in words])
|
||||||
return ('_'+name+'()'+\
|
fname = name.replace('-', '_')
|
||||||
|
return ('_'+fname+'()'+\
|
||||||
'''
|
'''
|
||||||
{
|
{
|
||||||
local cur opts
|
local cur opts
|
||||||
@ -435,7 +436,7 @@ def opts_and_words(name, op, words):
|
|||||||
esac
|
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):
|
def opts_and_exts(name, op, exts):
|
||||||
|
@ -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.
|
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?
|
How do I use |app| with my Android phone?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -111,7 +111,7 @@ Pre/post processing of downloaded HTML
|
|||||||
|
|
||||||
.. automember:: BasicNewsRecipe.remove_javascript
|
.. automember:: BasicNewsRecipe.remove_javascript
|
||||||
|
|
||||||
.. automethod:: BasicNewsRecipe.prepreprocess_html
|
.. automethod:: BasicNewsRecipe.skip_ad_pages
|
||||||
|
|
||||||
.. automethod:: BasicNewsRecipe.preprocess_html
|
.. 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
9513
src/calibre/translations/fo.po
Normal file
9513
src/calibre/translations/fo.po
Normal file
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
@ -31,7 +31,7 @@ class SafeLocalTimeZone(tzlocal):
|
|||||||
def compute_locale_info_for_parse_date():
|
def compute_locale_info_for_parse_date():
|
||||||
try:
|
try:
|
||||||
dt = datetime.strptime('1/5/2000', "%x")
|
dt = datetime.strptime('1/5/2000', "%x")
|
||||||
except ValueError:
|
except:
|
||||||
try:
|
try:
|
||||||
dt = datetime.strptime('1/5/01', '%x')
|
dt = datetime.strptime('1/5/01', '%x')
|
||||||
except:
|
except:
|
||||||
|
@ -175,7 +175,7 @@ def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
|
|||||||
p.DestroyMagickWand(canvas)
|
p.DestroyMagickWand(canvas)
|
||||||
|
|
||||||
def create_cover_page(top_lines, logo_path, width=590, height=750,
|
def create_cover_page(top_lines, logo_path, width=590, height=750,
|
||||||
bgcolor='white', output_format='png'):
|
bgcolor='white', output_format='jpg'):
|
||||||
ans = None
|
ans = None
|
||||||
with p.ImageMagick():
|
with p.ImageMagick():
|
||||||
canvas = create_canvas(width, height, bgcolor)
|
canvas = create_canvas(width, height, bgcolor)
|
||||||
|
@ -137,7 +137,7 @@ class Feed(object):
|
|||||||
|
|
||||||
def populate_from_preparsed_feed(self, title, articles, oldest_article=7,
|
def populate_from_preparsed_feed(self, title, articles, oldest_article=7,
|
||||||
max_articles_per_feed=100):
|
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.description = ''
|
||||||
self.image_url = None
|
self.image_url = None
|
||||||
self.articles = []
|
self.articles = []
|
||||||
|
@ -413,18 +413,19 @@ class BasicNewsRecipe(Recipe):
|
|||||||
return url
|
return url
|
||||||
return article.get('link', None)
|
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
|
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
|
any of the cleanup attributes like remove_tags, keep_only_tags are
|
||||||
applied. Note that preprocess_regexps will have already been applied.
|
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 is meant to allow the recipe to skip ad pages. If the soup represents
|
||||||
It should return `soup` after processing it.
|
an ad page, return the HTML of the real page. Otherwise return
|
||||||
|
None.
|
||||||
|
|
||||||
`soup`: A `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/documentation.html>`_
|
`soup`: A `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/documentation.html>`_
|
||||||
instance containing the downloaded :term:`HTML`.
|
instance containing the downloaded :term:`HTML`.
|
||||||
'''
|
'''
|
||||||
return soup
|
return None
|
||||||
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
@ -628,7 +629,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
|
|
||||||
self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
|
self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
|
||||||
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
|
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'):
|
'remove_tags_before', 'is_link_wanted'):
|
||||||
setattr(self.web2disk_options, extra, getattr(self, extra))
|
setattr(self.web2disk_options, extra, getattr(self, extra))
|
||||||
self.web2disk_options.postprocess_html = self._postprocess_html
|
self.web2disk_options.postprocess_html = self._postprocess_html
|
||||||
@ -801,11 +802,6 @@ class BasicNewsRecipe(Recipe):
|
|||||||
.calibre_navbar {
|
.calibre_navbar {
|
||||||
font-family:monospace;
|
font-family:monospace;
|
||||||
}
|
}
|
||||||
hr {
|
|
||||||
border-color:gray;
|
|
||||||
border-style:solid;
|
|
||||||
border-width:thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ class NavBarTemplate(Template):
|
|||||||
text = 'This article was downloaded by '
|
text = 'This article was downloaded by '
|
||||||
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
|
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
|
||||||
p[0].tail = ' from '
|
p[0].tail = ' from '
|
||||||
|
navbar.append(p)
|
||||||
navbar.append(BR())
|
navbar.append(BR())
|
||||||
navbar.append(BR())
|
navbar.append(BR())
|
||||||
else:
|
else:
|
||||||
@ -107,10 +108,11 @@ class TouchscreenNavBarTemplate(Template):
|
|||||||
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_100',
|
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_100',
|
||||||
style='text-align:'+align))
|
style='text-align:'+align))
|
||||||
if bottom:
|
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 '
|
text = 'This article was downloaded by '
|
||||||
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
|
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
|
||||||
p[0].tail = ' from '
|
p[0].tail = ' from '
|
||||||
|
navbar.append(p)
|
||||||
navbar.append(BR())
|
navbar.append(BR())
|
||||||
navbar.append(BR())
|
navbar.append(BR())
|
||||||
else:
|
else:
|
||||||
@ -134,7 +136,7 @@ class TouchscreenNavBarTemplate(Template):
|
|||||||
|
|
||||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||||
if not bottom:
|
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))
|
self.root = HTML(head, BODY(navbar))
|
||||||
|
|
||||||
@ -191,6 +193,8 @@ class TouchscreenIndexTemplate(Template):
|
|||||||
div = DIV(
|
div = DIV(
|
||||||
masthead_p,
|
masthead_p,
|
||||||
PT(date, style='text-align:center'),
|
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)
|
toc)
|
||||||
self.root = HTML(head, BODY(div))
|
self.root = HTML(head, BODY(div))
|
||||||
|
|
||||||
@ -254,10 +258,9 @@ class TouchscreenFeedTemplate(Template):
|
|||||||
head.append(STYLE(extra_css, type='text/css'))
|
head.append(STYLE(extra_css, type='text/css'))
|
||||||
body = BODY(style='page-break-before:always')
|
body = BODY(style='page-break-before:always')
|
||||||
div = DIV(
|
div = DIV(
|
||||||
H2(feed.title,
|
H2(feed.title, CLASS('calibre_feed_title', 'calibre_rescale_160')),
|
||||||
CLASS('calibre_feed_title', 'calibre_rescale_160')),
|
DIV(style="border-top:1px solid gray;border-bottom:1em solid white")
|
||||||
CLASS('calibre_rescale_100')
|
)
|
||||||
)
|
|
||||||
body.append(div)
|
body.append(div)
|
||||||
if getattr(feed, 'image', None):
|
if getattr(feed, 'image', None):
|
||||||
div.append(DIV(IMG(
|
div.append(DIV(IMG(
|
||||||
@ -276,17 +279,33 @@ class TouchscreenFeedTemplate(Template):
|
|||||||
if not getattr(article, 'downloaded', False):
|
if not getattr(article, 'downloaded', False):
|
||||||
continue
|
continue
|
||||||
tr = TR()
|
tr = TR()
|
||||||
td = TD(
|
|
||||||
A(article.title, CLASS('summary_headline','calibre_rescale_120',
|
if True:
|
||||||
href=article.url))
|
div_td = DIV(
|
||||||
)
|
A(article.title, CLASS('summary_headline','calibre_rescale_120',
|
||||||
if article.author:
|
href=article.url)),
|
||||||
td.append(DIV(article.author,
|
style="display:inline-block")
|
||||||
CLASS('summary_byline', 'calibre_rescale_100')))
|
if article.author:
|
||||||
if article.summary:
|
div_td.append(DIV(article.author,
|
||||||
td.append(DIV(cutoff(article.text_summary),
|
CLASS('summary_byline', 'calibre_rescale_100')))
|
||||||
CLASS('summary_text', 'calibre_rescale_100')))
|
if article.summary:
|
||||||
tr.append(td)
|
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)
|
toc.append(tr)
|
||||||
div.append(toc)
|
div.append(toc)
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ class RecursiveFetcher(object):
|
|||||||
self.remove_tags_before = getattr(options, 'remove_tags_before', None)
|
self.remove_tags_before = getattr(options, 'remove_tags_before', None)
|
||||||
self.keep_only_tags = getattr(options, 'keep_only_tags', [])
|
self.keep_only_tags = getattr(options, 'keep_only_tags', [])
|
||||||
self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup)
|
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.postprocess_html_ext= getattr(options, 'postprocess_html', None)
|
||||||
self._is_link_wanted = getattr(options, 'is_link_wanted',
|
self._is_link_wanted = getattr(options, 'is_link_wanted',
|
||||||
default_is_link_wanted)
|
default_is_link_wanted)
|
||||||
@ -154,7 +154,9 @@ class RecursiveFetcher(object):
|
|||||||
nmassage.append((re.compile(r'<!--.*?-->', re.DOTALL), lambda m: ''))
|
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 = 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:
|
if self.keep_only_tags:
|
||||||
body = Tag(soup, 'body')
|
body = Tag(soup, 'body')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user