mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk.
This commit is contained in:
commit
184cfb32d7
@ -4,6 +4,97 @@
|
|||||||
# 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
|
||||||
|
date: 2010-06-04
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "Content server: Add option to control category groupiong in OPDS feeds"
|
||||||
|
|
||||||
|
- title: "Make the book details pane occupy the full lower part of the window"
|
||||||
|
|
||||||
|
- title: "Add true and false searches for date based columns"
|
||||||
|
tickets: [5717]
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "iPad driver: Various bug fixes."
|
||||||
|
|
||||||
|
- title: "SONY driver: Fix Launcher partition being detected as storage card in linux"
|
||||||
|
|
||||||
|
- title: "Fix news downloading breaking on windows systems with local encoding other than UTF-8."
|
||||||
|
|
||||||
|
- title: "SONY driver: Fix problem caused by null titles"
|
||||||
|
|
||||||
|
- title: "Make the new splash screen not always stay on top"
|
||||||
|
tickets: [5700]
|
||||||
|
|
||||||
|
- title: "When setting an image with transparent pixels as the book cover, overlay it on a white background first. Fixes transparent covers getting random backgrounds."
|
||||||
|
|
||||||
|
- title: "Content server: Fix stanza integration when entering the server URL my hand"
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Gizmodo
|
||||||
|
- Vreme
|
||||||
|
|
||||||
|
|
||||||
- version: 0.7.0
|
- version: 0.7.0
|
||||||
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'
|
||||||
|
BIN
resources/images/news/haaretz_en.png
Normal file
BIN
resources/images/news/haaretz_en.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 712 B |
BIN
resources/images/news/ourdailybread.png
Normal file
BIN
resources/images/news/ourdailybread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 739 B |
Binary file not shown.
Before Width: | Height: | Size: 983 B After Width: | Height: | Size: 983 B |
@ -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
|
|
||||||
|
|
||||||
|
25
resources/recipes/cbc_canada.recipe
Normal file
25
resources/recipes/cbc_canada.recipe
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1275798572(BasicNewsRecipe):
|
||||||
|
title = u'CBC Canada'
|
||||||
|
publisher = 'www.cbc.ca'
|
||||||
|
language = 'en_CA'
|
||||||
|
__author__ = 'rty'
|
||||||
|
category = 'news'
|
||||||
|
oldest_article = 4
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en'
|
||||||
|
masthead_url = 'http://www.cbc.ca/includes/gfx/cbcnews_logo_09.gif'
|
||||||
|
cover_url = 'http://img692.imageshack.us/img692/2814/cbc.png'
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':['storyhead','storybody']})]
|
||||||
|
remove_tags_after = dict(id=['socialtools'])
|
||||||
|
feeds = [(u'Top Stories', u'http://rss.cbc.ca/lineup/topstories.xml'),
|
||||||
|
(u'World', u'http://rss.cbc.ca/lineup/world.xml'),
|
||||||
|
(u'National', u'http://rss.cbc.ca/lineup/canada.xml'),
|
||||||
|
(u'Manitoba', u'http://rss.cbc.ca/lineup/canada-manitoba.xml'),
|
||||||
|
(u'Politics', u'http://rss.cbc.ca/lineup/politics.xml'),
|
||||||
|
(u'Tech & Science', u'http://rss.cbc.ca/lineup/technology.xml'),
|
||||||
|
(u'Books', u'http://rss.cbc.ca/lineup/arts-books.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, '')
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class Gizmodo(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
use_embedded_content = True
|
use_embedded_content = False
|
||||||
language = 'en'
|
language = 'en'
|
||||||
masthead_url = 'http://cache.gawkerassets.com/assets/gizmodo.com/img/logo.png'
|
masthead_url = 'http://cache.gawkerassets.com/assets/gizmodo.com/img/logo.png'
|
||||||
extra_css = ' body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif} img{margin-bottom: 1em} '
|
extra_css = ' body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif} img{margin-bottom: 1em} '
|
||||||
@ -30,8 +30,10 @@ class Gizmodo(BasicNewsRecipe):
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove_attributes = ['width','height']
|
remove_attributes = ['width','height']
|
||||||
remove_tags = [dict(name='div',attrs={'class':'feedflare'})]
|
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
||||||
remove_tags_after = dict(name='div',attrs={'class':'feedflare'})
|
remove_tags_before = dict(name='h1')
|
||||||
|
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
||||||
|
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
||||||
|
|
||||||
feeds = [(u'Articles', u'http://feeds.gawker.com/gizmodo/full')]
|
feeds = [(u'Articles', u'http://feeds.gawker.com/gizmodo/full')]
|
||||||
|
|
||||||
|
57
resources/recipes/haaretz_en.recipe
Normal file
57
resources/recipes/haaretz_en.recipe
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
haaretz.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Haaretz_en(BasicNewsRecipe):
|
||||||
|
title = 'Haaretz in English'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'Haaretz.com, the online edition of Haaretz Newspaper in Israel, and analysis from Israel and the Middle East. Haaretz.com provides extensive and in-depth coverage of Israel, the Jewish World and the Middle East, including defense, diplomacy, the Arab-Israeli conflict, the peace process, Israeli politics, Jerusalem affairs, international relations, Iran, Iraq, Syria, Lebanon, the Palestinian Authority, the West Bank and the Gaza Strip, the Israeli business world and Jewish life in Israel and the Diaspora. '
|
||||||
|
publisher = 'haaretz.com'
|
||||||
|
category = 'news, politics, Israel'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 200
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'cp1252'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'en_IL'
|
||||||
|
publication_type = 'newspaper'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
masthead_url = 'http://www.haaretz.com/images/logos/logoGrey.gif'
|
||||||
|
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } '
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_tags = [dict(name='div', attrs={'class':['rightcol']}),dict(name='table')]
|
||||||
|
remove_tags_before = dict(name='h1')
|
||||||
|
remove_tags_after = dict(attrs={'id':'innerArticle'})
|
||||||
|
keep_only_tags = [dict(attrs={'id':'content'})]
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Opinion' , u'http://www.haaretz.com/cmlink/opinion-rss-1.209234?localLinksEnabled=false' )
|
||||||
|
,(u'Defense and diplomacy' , u'http://www.haaretz.com/cmlink/defense-and-diplomacy-rss-1.208894?localLinksEnabled=false')
|
||||||
|
,(u'National' , u'http://www.haaretz.com/cmlink/national-rss-1.208896?localLinksEnabled=false' )
|
||||||
|
,(u'International' , u'http://www.haaretz.com/cmlink/international-rss-1.208898?localLinksEnabled=false' )
|
||||||
|
,(u'Jewish World' , u'http://www.haaretz.com/cmlink/jewish-world-rss-1.209085?localLinksEnabled=false' )
|
||||||
|
,(u'Business' , u'http://www.haaretz.com/cmlink/business-print-rss-1.264904?localLinksEnabled=false' )
|
||||||
|
,(u'Real Estate' , u'http://www.haaretz.com/cmlink/real-estate-print-rss-1.264977?localLinksEnabled=false' )
|
||||||
|
,(u'Features' , u'http://www.haaretz.com/cmlink/features-print-rss-1.264912?localLinksEnabled=false' )
|
||||||
|
,(u'Arts and leisure' , u'http://www.haaretz.com/cmlink/arts-and-leisure-rss-1.286090?localLinksEnabled=false' )
|
||||||
|
,(u'Books' , u'http://www.haaretz.com/cmlink/books-rss-1.264947?localLinksEnabled=false' )
|
||||||
|
,(u'Food and Wine' , u'http://www.haaretz.com/cmlink/food-and-wine-print-rss-1.265034?localLinksEnabled=false' )
|
||||||
|
,(u'Sports' , u'http://www.haaretz.com/cmlink/sports-rss-1.286092?localLinksEnabled=false' )
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return soup
|
@ -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)]
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
|
|
||||||
title = 'New York Times Top Stories'
|
title = 'New York Times Top Stories'
|
||||||
__author__ = 'GRiker'
|
__author__ = 'GRiker'
|
||||||
language = _('English')
|
language = 'en'
|
||||||
description = 'Top Stories from the New York Times'
|
description = 'Top Stories from the New York Times'
|
||||||
|
|
||||||
# List of sections typically included in Top Stories. Use a keyword from the
|
# List of sections typically included in Top Stories. Use a keyword from the
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
__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>'
|
||||||
'''
|
'''
|
||||||
rbc.org
|
odb.org
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
@ -11,27 +9,29 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class OurDailyBread(BasicNewsRecipe):
|
class OurDailyBread(BasicNewsRecipe):
|
||||||
title = 'Our Daily Bread'
|
title = 'Our Daily Bread'
|
||||||
__author__ = 'Darko Miletic and Sujata Raman'
|
__author__ = 'Darko Miletic and Sujata Raman'
|
||||||
description = 'Religion'
|
description = "Our Daily Bread is a daily devotional from RBC Ministries which helps readers spend time each day in God's Word."
|
||||||
oldest_article = 15
|
oldest_article = 15
|
||||||
language = 'en'
|
language = 'en'
|
||||||
lang = 'en'
|
|
||||||
|
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
category = 'religion'
|
category = 'ODB, Daily Devotional, Bible, Christian Devotional, Devotional, RBC Ministries, Our Daily Bread, Devotionals, Daily Devotionals, Christian Devotionals, Faith, Bible Study, Bible Studies, Scripture, RBC, religion'
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comments' : description
|
'comments' : description
|
||||||
,'tags' : category
|
,'tags' : category
|
||||||
,'language' : 'en'
|
,'language' : language
|
||||||
|
,'linearize_tables' : True
|
||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'class':['altbg','text']})]
|
keep_only_tags = [dict(attrs={'class':'module-content'})]
|
||||||
|
remove_tags = [
|
||||||
remove_tags = [dict(name='div', attrs={'id':['ctl00_cphPrimary_pnlBookCover']}),
|
dict(attrs={'id':'article-zoom'})
|
||||||
|
,dict(attrs={'class':'listen-now-box'})
|
||||||
]
|
]
|
||||||
|
remove_tags_after = dict(attrs={'class':'readable-area'})
|
||||||
|
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
.text{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
|
.text{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
|
||||||
.devotionalTitle{font-family:Arial,Helvetica,sans-serif; font-size:large; font-weight: bold;}
|
.devotionalTitle{font-family:Arial,Helvetica,sans-serif; font-size:large; font-weight: bold;}
|
||||||
@ -40,14 +40,9 @@ class OurDailyBread(BasicNewsRecipe):
|
|||||||
a{color:#000000;font-family:Arial,Helvetica,sans-serif; font-size:x-small;}
|
a{color:#000000;font-family:Arial,Helvetica,sans-serif; font-size:x-small;}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
feeds = [(u'Our Daily Bread', u'http://www.rbc.org/rss.ashx?id=50398')]
|
feeds = [(u'Our Daily Bread', u'http://odb.org/feed/')]
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
soup.html['xml:lang'] = self.lang
|
|
||||||
soup.html['lang'] = self.lang
|
|
||||||
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">'
|
|
||||||
soup.head.insert(0,mtag)
|
|
||||||
|
|
||||||
return self.adeify_images(soup)
|
return self.adeify_images(soup)
|
||||||
|
|
||||||
def get_cover_url(self):
|
def get_cover_url(self):
|
||||||
@ -61,3 +56,4 @@ class OurDailyBread(BasicNewsRecipe):
|
|||||||
cover_url = a.img['src']
|
cover_url = a.img['src']
|
||||||
|
|
||||||
return cover_url
|
return cover_url
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -19,7 +19,7 @@ class Sueddeutsche(BasicNewsRecipe):
|
|||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
language = 'de'
|
language = 'de'
|
||||||
|
|
||||||
encoding = 'iso-8859-15'
|
encoding = 'utf-8'
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
|
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -41,6 +41,8 @@ mimetypes.add_type('application/vnd.palm', '.pdb')
|
|||||||
mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
|
mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
|
||||||
mimetypes.add_type('application/x-mobipocket-ebook', '.prc')
|
mimetypes.add_type('application/x-mobipocket-ebook', '.prc')
|
||||||
mimetypes.add_type('application/x-mobipocket-ebook', '.azw')
|
mimetypes.add_type('application/x-mobipocket-ebook', '.azw')
|
||||||
|
mimetypes.add_type('application/x-cbz', '.cbz')
|
||||||
|
mimetypes.add_type('application/x-cbr', '.cbr')
|
||||||
mimetypes.add_type('image/wmf', '.wmf')
|
mimetypes.add_type('image/wmf', '.wmf')
|
||||||
guess_type = mimetypes.guess_type
|
guess_type = mimetypes.guess_type
|
||||||
import cssutils
|
import cssutils
|
||||||
|
@ -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.0'
|
__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,
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,5 +123,12 @@ class BOOX(HANLINV3):
|
|||||||
EBOOK_DIR_MAIN = 'MyBooks'
|
EBOOK_DIR_MAIN = 'MyBooks'
|
||||||
EBOOK_DIR_CARD_A = 'MyBooks'
|
EBOOK_DIR_CARD_A = 'MyBooks'
|
||||||
|
|
||||||
|
def windows_sort_drives(self, drives):
|
||||||
|
return drives
|
||||||
|
|
||||||
|
def osx_sort_names(self, names):
|
||||||
|
return names
|
||||||
|
|
||||||
|
def linux_swap_drives(self, drives):
|
||||||
|
return drives
|
||||||
|
|
||||||
|
@ -287,7 +287,12 @@ class DevicePlugin(Plugin):
|
|||||||
This method should raise a L{FreeSpaceError} if there is not enough
|
This method should raise a L{FreeSpaceError} if there is not enough
|
||||||
free space on the device. The text of the FreeSpaceError must contain the
|
free space on the device. The text of the FreeSpaceError must contain the
|
||||||
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.
|
:files: A list of paths and/or file-like objects. If they are paths and
|
||||||
|
the paths point to temporary files, they may have an additional
|
||||||
|
attribute, original_file_path pointing to the originals. 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
|
||||||
|
@ -337,7 +337,7 @@ def main():
|
|||||||
dev.touch(args[0])
|
dev.touch(args[0])
|
||||||
elif command == 'test_file':
|
elif command == 'test_file':
|
||||||
parser = OptionParser(usage=("usage: %prog test_file path\n"
|
parser = OptionParser(usage=("usage: %prog test_file path\n"
|
||||||
'Open device, copy file psecified by path to device and '
|
'Open device, copy file specified by path to device and '
|
||||||
'then eject device.'))
|
'then eject device.'))
|
||||||
options, args = parser.parse_args(args)
|
options, args = parser.parse_args(args)
|
||||||
if len(args) != 1:
|
if len(args) != 1:
|
||||||
|
@ -6,10 +6,9 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Device driver for the SONY devices
|
Device driver for the SONY devices
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import os
|
import os, time, re
|
||||||
import re
|
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS
|
from calibre.devices.usbms.driver import USBMS, debug_print
|
||||||
from calibre.devices.prs505 import MEDIA_XML
|
from calibre.devices.prs505 import MEDIA_XML
|
||||||
from calibre.devices.prs505 import CACHE_XML
|
from calibre.devices.prs505 import CACHE_XML
|
||||||
from calibre.devices.prs505.sony_cache import XMLCache
|
from calibre.devices.prs505.sony_cache import XMLCache
|
||||||
@ -66,6 +65,41 @@ class PRS505(USBMS):
|
|||||||
def windows_filter_pnp_id(self, pnp_id):
|
def windows_filter_pnp_id(self, pnp_id):
|
||||||
return '_LAUNCHER' in pnp_id
|
return '_LAUNCHER' in pnp_id
|
||||||
|
|
||||||
|
def post_open_callback(self):
|
||||||
|
|
||||||
|
def write_cache(prefix):
|
||||||
|
try:
|
||||||
|
cachep = os.path.join(prefix, *(CACHE_XML.split('/')))
|
||||||
|
if not os.path.exists(cachep):
|
||||||
|
dname = os.path.dirname(cachep)
|
||||||
|
if not os.path.exists(dname):
|
||||||
|
try:
|
||||||
|
os.makedirs(dname, mode=0777)
|
||||||
|
except:
|
||||||
|
time.sleep(5)
|
||||||
|
os.makedirs(dname, mode=0777)
|
||||||
|
with open(cachep, 'wb') as f:
|
||||||
|
f.write(u'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<cache xmlns="http://www.kinoma.com/FskCache/1">
|
||||||
|
</cache>
|
||||||
|
'''.encode('utf8'))
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Make sure we don't have the launcher partition
|
||||||
|
# as one of the cards
|
||||||
|
|
||||||
|
if self._card_a_prefix is not None:
|
||||||
|
if not write_cache(self._card_a_prefix):
|
||||||
|
self._card_a_prefix = None
|
||||||
|
if self._card_b_prefix is not None:
|
||||||
|
if not write_cache(self._card_b_prefix):
|
||||||
|
self._card_b_prefix = None
|
||||||
|
|
||||||
|
|
||||||
def get_device_information(self, end_session=True):
|
def get_device_information(self, end_session=True):
|
||||||
return (self.gui_name, '', '', '')
|
return (self.gui_name, '', '', '')
|
||||||
|
|
||||||
@ -94,26 +128,31 @@ class PRS505(USBMS):
|
|||||||
return XMLCache(paths, prefixes)
|
return XMLCache(paths, prefixes)
|
||||||
|
|
||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
|
debug_print('PRS505: starting fetching books for card', oncard)
|
||||||
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
|
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
|
||||||
c = self.initialize_XML_cache()
|
c = self.initialize_XML_cache()
|
||||||
c.update_booklist(bl, {'carda':1, 'cardb':2}.get(oncard, 0))
|
c.update_booklist(bl, {'carda':1, 'cardb':2}.get(oncard, 0))
|
||||||
|
debug_print('PRS505: finished fetching books for card', oncard)
|
||||||
return bl
|
return bl
|
||||||
|
|
||||||
def sync_booklists(self, booklists, end_session=True):
|
def sync_booklists(self, booklists, end_session=True):
|
||||||
|
debug_print('PRS505: started sync_booklists')
|
||||||
c = self.initialize_XML_cache()
|
c = self.initialize_XML_cache()
|
||||||
blists = {}
|
blists = {}
|
||||||
for i in c.paths:
|
for i in c.paths:
|
||||||
if booklists[i] is not None:
|
if booklists[i] is not None:
|
||||||
blists[i] = booklists[i]
|
blists[i] = booklists[i]
|
||||||
opts = self.settings()
|
opts = self.settings()
|
||||||
collections = ['series', 'tags']
|
|
||||||
if opts.extra_customization:
|
if opts.extra_customization:
|
||||||
collections = [x.strip() for x in
|
collections = [x.strip() for x in
|
||||||
opts.extra_customization.split(',')]
|
opts.extra_customization.split(',')]
|
||||||
|
else:
|
||||||
|
collections = []
|
||||||
|
debug_print('PRS505: collection fields:', collections)
|
||||||
c.update(blists, collections)
|
c.update(blists, collections)
|
||||||
c.write()
|
c.write()
|
||||||
|
|
||||||
USBMS.sync_booklists(self, booklists, end_session=end_session)
|
USBMS.sync_booklists(self, booklists, end_session=end_session)
|
||||||
|
debug_print('PRS505: finished sync_booklists')
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ from lxml import etree
|
|||||||
|
|
||||||
from calibre import prints, guess_type
|
from calibre import prints, guess_type
|
||||||
from calibre.devices.errors import DeviceError
|
from calibre.devices.errors import DeviceError
|
||||||
|
from calibre.devices.usbms.driver import debug_print
|
||||||
from calibre.constants import DEBUG
|
from calibre.constants import DEBUG
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from calibre.ebooks.metadata import authors_to_string, title_sort
|
from calibre.ebooks.metadata import authors_to_string, title_sort
|
||||||
@ -46,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])]
|
||||||
@ -61,7 +62,7 @@ class XMLCache(object):
|
|||||||
|
|
||||||
def __init__(self, paths, prefixes):
|
def __init__(self, paths, prefixes):
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Building XMLCache...')
|
debug_print('Building XMLCache...')
|
||||||
pprint(paths)
|
pprint(paths)
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.prefixes = prefixes
|
self.prefixes = prefixes
|
||||||
@ -97,16 +98,17 @@ class XMLCache(object):
|
|||||||
self.record_roots[0] = recs[0]
|
self.record_roots[0] = recs[0]
|
||||||
|
|
||||||
self.detect_namespaces()
|
self.detect_namespaces()
|
||||||
|
debug_print('Done building XMLCache...')
|
||||||
|
|
||||||
|
|
||||||
# Playlist management {{{
|
# Playlist management {{{
|
||||||
def purge_broken_playlist_items(self, root):
|
def purge_broken_playlist_items(self, root):
|
||||||
|
id_map = self.build_id_map(root)
|
||||||
for pl in root.xpath('//*[local-name()="playlist"]'):
|
for pl in root.xpath('//*[local-name()="playlist"]'):
|
||||||
seen = set([])
|
seen = set([])
|
||||||
for item in list(pl):
|
for item in list(pl):
|
||||||
id_ = item.get('id', None)
|
id_ = item.get('id', None)
|
||||||
if id_ is None or id_ in seen or not root.xpath(
|
if id_ is None or id_ in seen or id_map.get(id_, None) is None:
|
||||||
'//*[local-name()!="item" and @id="%s"]'%id_):
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
if id_ is None:
|
if id_ is None:
|
||||||
cause = 'invalid id'
|
cause = 'invalid id'
|
||||||
@ -127,7 +129,7 @@ class XMLCache(object):
|
|||||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||||
if len(playlist) == 0 or not playlist.get('title', None):
|
if len(playlist) == 0 or not playlist.get('title', None):
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Removing playlist id:', playlist.get('id', None),
|
debug_print('Removing playlist id:', playlist.get('id', None),
|
||||||
playlist.get('title', None))
|
playlist.get('title', None))
|
||||||
playlist.getparent().remove(playlist)
|
playlist.getparent().remove(playlist)
|
||||||
|
|
||||||
@ -149,20 +151,25 @@ class XMLCache(object):
|
|||||||
seen.add(title)
|
seen.add(title)
|
||||||
|
|
||||||
def get_playlist_map(self):
|
def get_playlist_map(self):
|
||||||
|
debug_print('Start get_playlist_map')
|
||||||
ans = {}
|
ans = {}
|
||||||
self.ensure_unique_playlist_titles()
|
self.ensure_unique_playlist_titles()
|
||||||
|
debug_print('after ensure_unique_playlist_titles')
|
||||||
self.prune_empty_playlists()
|
self.prune_empty_playlists()
|
||||||
|
debug_print('get_playlist_map loop')
|
||||||
for i, root in self.record_roots.items():
|
for i, root in self.record_roots.items():
|
||||||
|
debug_print('get_playlist_map loop', i)
|
||||||
|
id_map = self.build_id_map(root)
|
||||||
ans[i] = []
|
ans[i] = []
|
||||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||||
items = []
|
items = []
|
||||||
for item in playlist:
|
for item in playlist:
|
||||||
id_ = item.get('id', None)
|
id_ = item.get('id', None)
|
||||||
records = root.xpath(
|
record = id_map.get(id_, None)
|
||||||
'//*[local-name()="text" and @id="%s"]'%id_)
|
if record is not None:
|
||||||
if records:
|
items.append(record)
|
||||||
items.append(records[0])
|
|
||||||
ans[i].append((playlist.get('title'), items))
|
ans[i].append((playlist.get('title'), items))
|
||||||
|
debug_print('end get_playlist_map')
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def get_or_create_playlist(self, bl_idx, title):
|
def get_or_create_playlist(self, bl_idx, title):
|
||||||
@ -171,7 +178,7 @@ class XMLCache(object):
|
|||||||
if playlist.get('title', None) == title:
|
if playlist.get('title', None) == title:
|
||||||
return playlist
|
return playlist
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Creating playlist:', title)
|
debug_print('Creating playlist:', title)
|
||||||
ans = root.makeelement('{%s}playlist'%self.namespaces[bl_idx],
|
ans = root.makeelement('{%s}playlist'%self.namespaces[bl_idx],
|
||||||
nsmap=root.nsmap, attrib={
|
nsmap=root.nsmap, attrib={
|
||||||
'uuid' : uuid(),
|
'uuid' : uuid(),
|
||||||
@ -185,7 +192,7 @@ class XMLCache(object):
|
|||||||
|
|
||||||
def fix_ids(self): # {{{
|
def fix_ids(self): # {{{
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Running fix_ids()')
|
debug_print('Running fix_ids()')
|
||||||
|
|
||||||
def ensure_numeric_ids(root):
|
def ensure_numeric_ids(root):
|
||||||
idmap = {}
|
idmap = {}
|
||||||
@ -198,8 +205,8 @@ class XMLCache(object):
|
|||||||
idmap[id_] = '-1'
|
idmap[id_] = '-1'
|
||||||
|
|
||||||
if DEBUG and idmap:
|
if DEBUG and idmap:
|
||||||
prints('Found non numeric ids:')
|
debug_print('Found non numeric ids:')
|
||||||
prints(list(idmap.keys()))
|
debug_print(list(idmap.keys()))
|
||||||
return idmap
|
return idmap
|
||||||
|
|
||||||
def remap_playlist_references(root, idmap):
|
def remap_playlist_references(root, idmap):
|
||||||
@ -210,7 +217,7 @@ class XMLCache(object):
|
|||||||
if id_ in idmap:
|
if id_ in idmap:
|
||||||
item.set('id', idmap[id_])
|
item.set('id', idmap[id_])
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Remapping id %s to %s'%(id_, idmap[id_]))
|
debug_print('Remapping id %s to %s'%(id_, idmap[id_]))
|
||||||
|
|
||||||
def ensure_media_xml_base_ids(root):
|
def ensure_media_xml_base_ids(root):
|
||||||
for num, tag in enumerate(('library', 'watchSpecial')):
|
for num, tag in enumerate(('library', 'watchSpecial')):
|
||||||
@ -260,6 +267,8 @@ class XMLCache(object):
|
|||||||
last_bl = max(self.roots.keys())
|
last_bl = max(self.roots.keys())
|
||||||
max_id = self.max_id(self.roots[last_bl])
|
max_id = self.max_id(self.roots[last_bl])
|
||||||
self.roots[0].set('nextID', str(max_id+1))
|
self.roots[0].set('nextID', str(max_id+1))
|
||||||
|
debug_print('Finished running fix_ids()')
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Update JSON from XML {{{
|
# Update JSON from XML {{{
|
||||||
@ -267,7 +276,7 @@ class XMLCache(object):
|
|||||||
if bl_index not in self.record_roots:
|
if bl_index not in self.record_roots:
|
||||||
return
|
return
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Updating JSON cache:', bl_index)
|
debug_print('Updating JSON cache:', bl_index)
|
||||||
root = self.record_roots[bl_index]
|
root = self.record_roots[bl_index]
|
||||||
pmap = self.get_playlist_map()[bl_index]
|
pmap = self.get_playlist_map()[bl_index]
|
||||||
playlist_map = {}
|
playlist_map = {}
|
||||||
@ -279,13 +288,14 @@ class XMLCache(object):
|
|||||||
playlist_map[path] = []
|
playlist_map[path] = []
|
||||||
playlist_map[path].append(title)
|
playlist_map[path].append(title)
|
||||||
|
|
||||||
|
lpath_map = self.build_lpath_map(root)
|
||||||
for book in bl:
|
for book in bl:
|
||||||
record = self.book_by_lpath(book.lpath, root)
|
record = lpath_map.get(book.lpath, None)
|
||||||
if record is not None:
|
if record is not None:
|
||||||
title = record.get('title', None)
|
title = record.get('title', None)
|
||||||
if title is not None and title != book.title:
|
if title is not None and title != book.title:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Renaming title', book.title, 'to', title)
|
debug_print('Renaming title', book.title, 'to', title)
|
||||||
book.title = title
|
book.title = title
|
||||||
# We shouldn't do this for Sonys, because the reader strips
|
# We shouldn't do this for Sonys, because the reader strips
|
||||||
# all but the first author.
|
# all but the first author.
|
||||||
@ -310,20 +320,24 @@ class XMLCache(object):
|
|||||||
if book.lpath in playlist_map:
|
if book.lpath in playlist_map:
|
||||||
tags = playlist_map[book.lpath]
|
tags = playlist_map[book.lpath]
|
||||||
book.device_collections = tags
|
book.device_collections = tags
|
||||||
|
debug_print('Finished updating JSON cache:', bl_index)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Update XML from JSON {{{
|
# Update XML from JSON {{{
|
||||||
def update(self, booklists, collections_attributes):
|
def update(self, booklists, collections_attributes):
|
||||||
|
debug_print('Starting update XML from JSON')
|
||||||
playlist_map = self.get_playlist_map()
|
playlist_map = self.get_playlist_map()
|
||||||
|
|
||||||
for i, booklist in booklists.items():
|
for i, booklist in booklists.items():
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Updating XML Cache:', i)
|
debug_print('Updating XML Cache:', i)
|
||||||
root = self.record_roots[i]
|
root = self.record_roots[i]
|
||||||
|
lpath_map = self.build_lpath_map(root)
|
||||||
for book in booklist:
|
for book in booklist:
|
||||||
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
|
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
|
||||||
record = self.book_by_lpath(book.lpath, root)
|
# record = self.book_by_lpath(book.lpath, root)
|
||||||
|
record = lpath_map.get(book.lpath, None)
|
||||||
if record is None:
|
if record is None:
|
||||||
record = self.create_text_record(root, i, book.lpath)
|
record = self.create_text_record(root, i, book.lpath)
|
||||||
self.update_text_record(record, book, path, i)
|
self.update_text_record(record, book, path, i)
|
||||||
@ -337,16 +351,19 @@ class XMLCache(object):
|
|||||||
# This is needed to update device_collections
|
# This is needed to update device_collections
|
||||||
for i, booklist in booklists.items():
|
for i, booklist in booklists.items():
|
||||||
self.update_booklist(booklist, i)
|
self.update_booklist(booklist, i)
|
||||||
|
debug_print('Finished update XML from JSON')
|
||||||
|
|
||||||
def update_playlists(self, bl_index, root, booklist, playlist_map,
|
def update_playlists(self, bl_index, root, booklist, playlist_map,
|
||||||
collections_attributes):
|
collections_attributes):
|
||||||
|
debug_print('Starting update_playlists')
|
||||||
collections = booklist.get_collections(collections_attributes)
|
collections = booklist.get_collections(collections_attributes)
|
||||||
|
lpath_map = self.build_lpath_map(root)
|
||||||
for category, books in collections.items():
|
for category, books in collections.items():
|
||||||
records = [self.book_by_lpath(b.lpath, root) for b in books]
|
records = [lpath_map.get(b.lpath, None) for b in books]
|
||||||
# Remove any books that were not found, although this
|
# Remove any books that were not found, although this
|
||||||
# *should* never happen
|
# *should* never happen
|
||||||
if DEBUG and None in records:
|
if DEBUG and None in records:
|
||||||
prints('WARNING: Some elements in the JSON cache were not'
|
debug_print('WARNING: Some elements in the JSON cache were not'
|
||||||
' found in the XML cache')
|
' found in the XML cache')
|
||||||
records = [x for x in records if x is not None]
|
records = [x for x in records if x is not None]
|
||||||
for rec in records:
|
for rec in records:
|
||||||
@ -355,7 +372,7 @@ class XMLCache(object):
|
|||||||
ids = [x.get('id', None) for x in records]
|
ids = [x.get('id', None) for x in records]
|
||||||
if None in ids:
|
if None in ids:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('WARNING: Some <text> elements do not have ids')
|
debug_print('WARNING: Some <text> elements do not have ids')
|
||||||
ids = [x for x in ids if x is not None]
|
ids = [x for x in ids if x is not None]
|
||||||
|
|
||||||
playlist = self.get_or_create_playlist(bl_index, category)
|
playlist = self.get_or_create_playlist(bl_index, category)
|
||||||
@ -379,20 +396,21 @@ class XMLCache(object):
|
|||||||
title = playlist.get('title', None)
|
title = playlist.get('title', None)
|
||||||
if title not in collections:
|
if title not in collections:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Deleting playlist:', playlist.get('title', ''))
|
debug_print('Deleting playlist:', playlist.get('title', ''))
|
||||||
playlist.getparent().remove(playlist)
|
playlist.getparent().remove(playlist)
|
||||||
continue
|
continue
|
||||||
books = collections[title]
|
books = collections[title]
|
||||||
records = [self.book_by_lpath(b.lpath, root) for b in books]
|
records = [lpath_map.get(b.lpath, None) for b in books]
|
||||||
records = [x for x in records if x is not None]
|
records = [x for x in records if x is not None]
|
||||||
ids = [x.get('id', None) for x in records]
|
ids = [x.get('id', None) for x in records]
|
||||||
ids = [x for x in ids if x is not None]
|
ids = [x for x in ids if x is not None]
|
||||||
for item in list(playlist):
|
for item in list(playlist):
|
||||||
if item.get('id', None) not in ids:
|
if item.get('id', None) not in ids:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Deleting item:', item.get('id', ''),
|
debug_print('Deleting item:', item.get('id', ''),
|
||||||
'from playlist:', playlist.get('title', ''))
|
'from playlist:', playlist.get('title', ''))
|
||||||
playlist.remove(item)
|
playlist.remove(item)
|
||||||
|
debug_print('Finishing update_playlists')
|
||||||
|
|
||||||
def create_text_record(self, root, bl_id, lpath):
|
def create_text_record(self, root, bl_id, lpath):
|
||||||
namespace = self.namespaces[bl_id]
|
namespace = self.namespaces[bl_id]
|
||||||
@ -405,14 +423,9 @@ 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):
|
||||||
if DEBUG:
|
|
||||||
prints('Changing date of', path, 'from',
|
|
||||||
record.get('date', ''), 'to', date)
|
|
||||||
prints('\tctime', strftime(os.path.getctime(path)))
|
|
||||||
prints('\tmtime', strftime(os.path.getmtime(path)))
|
|
||||||
record.set('date', date)
|
record.set('date', date)
|
||||||
record.set('size', str(os.stat(path).st_size))
|
record.set('size', str(os.stat(path).st_size))
|
||||||
title = book.title if book.title else _('Unknown')
|
title = book.title if book.title else _('Unknown')
|
||||||
@ -475,12 +488,24 @@ class XMLCache(object):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Utility methods {{{
|
# Utility methods {{{
|
||||||
|
|
||||||
|
def build_lpath_map(self, root):
|
||||||
|
m = {}
|
||||||
|
for bk in root.xpath('//*[local-name()="text"]'):
|
||||||
|
m[bk.get('path')] = bk
|
||||||
|
return m
|
||||||
|
|
||||||
|
def build_id_map(self, root):
|
||||||
|
m = {}
|
||||||
|
for bk in root.xpath('//*[local-name()="text"]'):
|
||||||
|
m[bk.get('id')] = bk
|
||||||
|
return m
|
||||||
|
|
||||||
def book_by_lpath(self, lpath, root):
|
def book_by_lpath(self, lpath, root):
|
||||||
matches = root.xpath(u'//*[local-name()="text" and @path="%s"]'%lpath)
|
matches = root.xpath(u'//*[local-name()="text" and @path="%s"]'%lpath)
|
||||||
if matches:
|
if matches:
|
||||||
return matches[0]
|
return matches[0]
|
||||||
|
|
||||||
|
|
||||||
def max_id(self, root):
|
def max_id(self, root):
|
||||||
ans = -1
|
ans = -1
|
||||||
for x in root.xpath('//*[@id]'):
|
for x in root.xpath('//*[@id]'):
|
||||||
@ -515,10 +540,10 @@ class XMLCache(object):
|
|||||||
break
|
break
|
||||||
self.namespaces[i] = ns
|
self.namespaces[i] = ns
|
||||||
|
|
||||||
if DEBUG:
|
# if DEBUG:
|
||||||
prints('Found nsmaps:')
|
# debug_print('Found nsmaps:')
|
||||||
pprint(self.nsmaps)
|
# pprint(self.nsmaps)
|
||||||
prints('Found namespaces:')
|
# debug_print('Found namespaces:')
|
||||||
pprint(self.namespaces)
|
# pprint(self.namespaces)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -46,7 +46,8 @@ class Book(MetaInformation):
|
|||||||
self.smart_update(other)
|
self.smart_update(other)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.path == getattr(other, 'path', None)
|
# use lpath because the prefix can change, changing path
|
||||||
|
return self.lpath == getattr(other, 'lpath', None)
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def db_id(self):
|
def db_id(self):
|
||||||
@ -97,13 +98,24 @@ class Book(MetaInformation):
|
|||||||
|
|
||||||
class BookList(_BookList):
|
class BookList(_BookList):
|
||||||
|
|
||||||
|
def __init__(self, oncard, prefix, settings):
|
||||||
|
_BookList.__init__(self, oncard, prefix, settings)
|
||||||
|
self._bookmap = {}
|
||||||
|
|
||||||
def supports_collections(self):
|
def supports_collections(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add_book(self, book, replace_metadata):
|
def add_book(self, book, replace_metadata):
|
||||||
if book not in self:
|
try:
|
||||||
|
b = self.index(book)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
b = None
|
||||||
|
if b is None:
|
||||||
self.append(book)
|
self.append(book)
|
||||||
return True
|
return True
|
||||||
|
if replace_metadata:
|
||||||
|
self[b].smart_update(book)
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove_book(self, book):
|
def remove_book(self, book):
|
||||||
@ -112,7 +124,6 @@ class BookList(_BookList):
|
|||||||
def get_collections(self):
|
def get_collections(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class CollectionsBookList(BookList):
|
class CollectionsBookList(BookList):
|
||||||
|
|
||||||
def supports_collections(self):
|
def supports_collections(self):
|
||||||
|
@ -765,12 +765,8 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
path = existing[0]
|
path = existing[0]
|
||||||
|
|
||||||
def get_size(obj):
|
def get_size(obj):
|
||||||
if hasattr(obj, 'seek'):
|
path = getattr(obj, 'name', obj)
|
||||||
obj.seek(0, os.SEEK_END)
|
return os.path.getsize(path)
|
||||||
size = obj.tell()
|
|
||||||
obj.seek(0)
|
|
||||||
return size
|
|
||||||
return os.path.getsize(obj)
|
|
||||||
|
|
||||||
sizes = [get_size(f) for f in files]
|
sizes = [get_size(f) for f in files]
|
||||||
size = sum(sizes)
|
size = sum(sizes)
|
||||||
|
@ -12,15 +12,24 @@ for a particular device.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import json
|
import json
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
|
|
||||||
from calibre import prints, isbytestring
|
from calibre import prints, isbytestring
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding, DEBUG
|
||||||
from calibre.devices.usbms.cli import CLI
|
from calibre.devices.usbms.cli import CLI
|
||||||
from calibre.devices.usbms.device import Device
|
from calibre.devices.usbms.device import Device
|
||||||
from calibre.devices.usbms.books import BookList, Book
|
from calibre.devices.usbms.books import BookList, Book
|
||||||
|
|
||||||
|
BASE_TIME = None
|
||||||
|
def debug_print(*args):
|
||||||
|
global BASE_TIME
|
||||||
|
if BASE_TIME is None:
|
||||||
|
BASE_TIME = time.time()
|
||||||
|
if DEBUG:
|
||||||
|
prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args)
|
||||||
|
|
||||||
# CLI must come before Device as it implements the CLI functions that
|
# CLI must come before Device as it implements the CLI functions that
|
||||||
# are inherited from the device interface in Device.
|
# are inherited from the device interface in Device.
|
||||||
class USBMS(CLI, Device):
|
class USBMS(CLI, Device):
|
||||||
@ -47,6 +56,8 @@ class USBMS(CLI, Device):
|
|||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
from calibre.ebooks.metadata.meta import path_to_ext
|
from calibre.ebooks.metadata.meta import path_to_ext
|
||||||
|
|
||||||
|
debug_print ('USBMS: Fetching list of books from device. oncard=', oncard)
|
||||||
|
|
||||||
dummy_bl = BookList(None, None, None)
|
dummy_bl = BookList(None, None, None)
|
||||||
|
|
||||||
if oncard == 'carda' and not self._card_a_prefix:
|
if oncard == 'carda' and not self._card_a_prefix:
|
||||||
@ -136,8 +147,8 @@ class USBMS(CLI, Device):
|
|||||||
need_sync = True
|
need_sync = True
|
||||||
del bl[idx]
|
del bl[idx]
|
||||||
|
|
||||||
#print "count found in cache: %d, count of files in metadata: %d, need_sync: %s" % \
|
debug_print('USBMS: count found in cache: %d, count of files in metadata: %d, need_sync: %s' % \
|
||||||
# (len(bl_cache), len(bl), need_sync)
|
(len(bl_cache), len(bl), need_sync))
|
||||||
if need_sync: #self.count_found_in_bl != len(bl) or need_sync:
|
if need_sync: #self.count_found_in_bl != len(bl) or need_sync:
|
||||||
if oncard == 'cardb':
|
if oncard == 'cardb':
|
||||||
self.sync_booklists((None, None, bl))
|
self.sync_booklists((None, None, bl))
|
||||||
@ -147,10 +158,13 @@ class USBMS(CLI, Device):
|
|||||||
self.sync_booklists((bl, None, None))
|
self.sync_booklists((bl, None, None))
|
||||||
|
|
||||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||||
|
debug_print('USBMS: Finished fetching list of books from device. oncard=', oncard)
|
||||||
return bl
|
return bl
|
||||||
|
|
||||||
def upload_books(self, files, names, on_card=None, end_session=True,
|
def upload_books(self, files, names, on_card=None, end_session=True,
|
||||||
metadata=None):
|
metadata=None):
|
||||||
|
debug_print('USBMS: uploading %d books'%(len(files)))
|
||||||
|
|
||||||
path = self._sanity_check(on_card, files)
|
path = self._sanity_check(on_card, files)
|
||||||
|
|
||||||
paths = []
|
paths = []
|
||||||
@ -174,6 +188,7 @@ class USBMS(CLI, Device):
|
|||||||
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
|
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
|
||||||
|
|
||||||
self.report_progress(1.0, _('Transferring books to device...'))
|
self.report_progress(1.0, _('Transferring books to device...'))
|
||||||
|
debug_print('USBMS: finished uploading %d books'%(len(files)))
|
||||||
return zip(paths, cycle([on_card]))
|
return zip(paths, cycle([on_card]))
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata):
|
def upload_cover(self, path, filename, metadata):
|
||||||
@ -186,6 +201,8 @@ class USBMS(CLI, Device):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def add_books_to_metadata(self, locations, metadata, booklists):
|
def add_books_to_metadata(self, locations, metadata, booklists):
|
||||||
|
debug_print('USBMS: adding metadata for %d books'%(len(metadata)))
|
||||||
|
|
||||||
metadata = iter(metadata)
|
metadata = iter(metadata)
|
||||||
for i, location in enumerate(locations):
|
for i, location in enumerate(locations):
|
||||||
self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...'))
|
self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...'))
|
||||||
@ -218,8 +235,10 @@ class USBMS(CLI, Device):
|
|||||||
book.size = os.stat(self.normalize_path(path)).st_size
|
book.size = os.stat(self.normalize_path(path)).st_size
|
||||||
booklists[blist].add_book(book, replace_metadata=True)
|
booklists[blist].add_book(book, replace_metadata=True)
|
||||||
self.report_progress(1.0, _('Adding books to device metadata listing...'))
|
self.report_progress(1.0, _('Adding books to device metadata listing...'))
|
||||||
|
debug_print('USBMS: finished adding metadata')
|
||||||
|
|
||||||
def delete_books(self, paths, end_session=True):
|
def delete_books(self, paths, end_session=True):
|
||||||
|
debug_print('USBMS: deleting %d books'%(len(paths)))
|
||||||
for i, path in enumerate(paths):
|
for i, path in enumerate(paths):
|
||||||
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
|
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
|
||||||
path = self.normalize_path(path)
|
path = self.normalize_path(path)
|
||||||
@ -240,8 +259,11 @@ class USBMS(CLI, Device):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
self.report_progress(1.0, _('Removing books from device...'))
|
self.report_progress(1.0, _('Removing books from device...'))
|
||||||
|
debug_print('USBMS: finished deleting %d books'%(len(paths)))
|
||||||
|
|
||||||
def remove_books_from_metadata(self, paths, booklists):
|
def remove_books_from_metadata(self, paths, booklists):
|
||||||
|
debug_print('USBMS: removing metadata for %d books'%(len(paths)))
|
||||||
|
|
||||||
for i, path in enumerate(paths):
|
for i, path in enumerate(paths):
|
||||||
self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
|
self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
|
||||||
for bl in booklists:
|
for bl in booklists:
|
||||||
@ -249,8 +271,11 @@ class USBMS(CLI, Device):
|
|||||||
if path.endswith(book.path):
|
if path.endswith(book.path):
|
||||||
bl.remove_book(book)
|
bl.remove_book(book)
|
||||||
self.report_progress(1.0, _('Removing books from device metadata listing...'))
|
self.report_progress(1.0, _('Removing books from device metadata listing...'))
|
||||||
|
debug_print('USBMS: finished removing metadata for %d books'%(len(paths)))
|
||||||
|
|
||||||
def sync_booklists(self, booklists, end_session=True):
|
def sync_booklists(self, booklists, end_session=True):
|
||||||
|
debug_print('USBMS: starting sync_booklists')
|
||||||
|
|
||||||
if not os.path.exists(self.normalize_path(self._main_prefix)):
|
if not os.path.exists(self.normalize_path(self._main_prefix)):
|
||||||
os.makedirs(self.normalize_path(self._main_prefix))
|
os.makedirs(self.normalize_path(self._main_prefix))
|
||||||
|
|
||||||
@ -267,6 +292,19 @@ class USBMS(CLI, Device):
|
|||||||
write_prefix(self._card_b_prefix, 2)
|
write_prefix(self._card_b_prefix, 2)
|
||||||
|
|
||||||
self.report_progress(1.0, _('Sending metadata to device...'))
|
self.report_progress(1.0, _('Sending metadata to device...'))
|
||||||
|
debug_print('USBMS: finished sync_booklists')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build_template_regexp(cls):
|
||||||
|
def replfunc(match):
|
||||||
|
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
|
||||||
|
return '(?P<' + match.group(1) + '>.+?)'
|
||||||
|
elif match.group(1) == 'authors':
|
||||||
|
return '(?P<author>.+?)'
|
||||||
|
else:
|
||||||
|
return '(.+?)'
|
||||||
|
template = cls.save_template().rpartition('/')[2]
|
||||||
|
return re.compile(re.sub('{([^}]*)}', replfunc, template) + '([_\d]*$)')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def path_to_unicode(cls, path):
|
def path_to_unicode(cls, path):
|
||||||
@ -329,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)
|
||||||
|
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
|
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 \
|
||||||
|
@ -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:
|
||||||
|
@ -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='narrow')
|
||||||
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()
|
||||||
|
|
||||||
|
@ -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,17 @@ 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))
|
||||||
|
self.setZoomFactor(150)
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return self.minimumSize()
|
||||||
|
|
||||||
def wheelEvent(self, ev):
|
def wheelEvent(self, ev):
|
||||||
ev.accept()
|
ev.accept()
|
||||||
@ -95,6 +101,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 +221,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):
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
# Imports {{{
|
||||||
import os, traceback, Queue, time, socket, cStringIO, re
|
import os, traceback, Queue, time, socket, cStringIO, re
|
||||||
from threading import Thread, RLock
|
from threading import Thread, RLock
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
@ -27,7 +29,9 @@ from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
|||||||
config as email_config
|
config as email_config
|
||||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
||||||
|
|
||||||
class DeviceJob(BaseJob):
|
# }}}
|
||||||
|
|
||||||
|
class DeviceJob(BaseJob): # {{{
|
||||||
|
|
||||||
def __init__(self, func, done, job_manager, args=[], kwargs={},
|
def __init__(self, func, done, job_manager, args=[], kwargs={},
|
||||||
description=''):
|
description=''):
|
||||||
@ -78,8 +82,9 @@ class DeviceJob(BaseJob):
|
|||||||
def log_file(self):
|
def log_file(self):
|
||||||
return cStringIO.StringIO(self._details.encode('utf-8'))
|
return cStringIO.StringIO(self._details.encode('utf-8'))
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class DeviceManager(Thread):
|
class DeviceManager(Thread): # {{{
|
||||||
|
|
||||||
def __init__(self, connected_slot, job_manager, open_feedback_slot, sleep_time=2):
|
def __init__(self, connected_slot, job_manager, open_feedback_slot, sleep_time=2):
|
||||||
'''
|
'''
|
||||||
@ -122,7 +127,7 @@ class DeviceManager(Thread):
|
|||||||
try:
|
try:
|
||||||
dev.open()
|
dev.open()
|
||||||
except:
|
except:
|
||||||
print 'Unable to open device', dev
|
prints('Unable to open device', str(dev))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
continue
|
continue
|
||||||
self.connected_device = dev
|
self.connected_device = dev
|
||||||
@ -168,11 +173,11 @@ class DeviceManager(Thread):
|
|||||||
if possibly_connected_devices:
|
if possibly_connected_devices:
|
||||||
if not self.do_connect(possibly_connected_devices,
|
if not self.do_connect(possibly_connected_devices,
|
||||||
is_folder_device=False):
|
is_folder_device=False):
|
||||||
print 'Connect to device failed, retrying in 5 seconds...'
|
prints('Connect to device failed, retrying in 5 seconds...')
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
if not self.do_connect(possibly_connected_devices,
|
if not self.do_connect(possibly_connected_devices,
|
||||||
is_folder_device=False):
|
is_folder_device=False):
|
||||||
print 'Device connect failed again, giving up'
|
prints('Device connect failed again, giving up')
|
||||||
|
|
||||||
def umount_device(self, *args):
|
def umount_device(self, *args):
|
||||||
if self.is_device_connected and not self.job_manager.has_device_jobs():
|
if self.is_device_connected and not self.job_manager.has_device_jobs():
|
||||||
@ -317,7 +322,7 @@ class DeviceManager(Thread):
|
|||||||
def _save_books(self, paths, target):
|
def _save_books(self, paths, target):
|
||||||
'''Copy books from device to disk'''
|
'''Copy books from device to disk'''
|
||||||
for path in paths:
|
for path in paths:
|
||||||
name = path.rpartition(getattr(self.device, 'path_sep', '/'))[2]
|
name = path.rpartition(os.sep)[2]
|
||||||
dest = os.path.join(target, name)
|
dest = os.path.join(target, name)
|
||||||
if os.path.abspath(dest) != os.path.abspath(path):
|
if os.path.abspath(dest) != os.path.abspath(path):
|
||||||
f = open(dest, 'wb')
|
f = open(dest, 'wb')
|
||||||
@ -338,8 +343,9 @@ class DeviceManager(Thread):
|
|||||||
return self.create_job(self._view_book, done, args=[path, target],
|
return self.create_job(self._view_book, done, args=[path, target],
|
||||||
description=_('View book on device'))
|
description=_('View book on device'))
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class DeviceAction(QAction):
|
class DeviceAction(QAction): # {{{
|
||||||
|
|
||||||
a_s = pyqtSignal(object)
|
a_s = pyqtSignal(object)
|
||||||
|
|
||||||
@ -356,9 +362,9 @@ class DeviceAction(QAction):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
|
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
|
||||||
self.specific)
|
self.specific)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class DeviceMenu(QMenu): # {{{
|
||||||
class DeviceMenu(QMenu):
|
|
||||||
|
|
||||||
fetch_annotations = pyqtSignal()
|
fetch_annotations = pyqtSignal()
|
||||||
connect_to_folder = pyqtSignal()
|
connect_to_folder = pyqtSignal()
|
||||||
@ -385,16 +391,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'),
|
||||||
@ -532,8 +536,9 @@ class DeviceMenu(QMenu):
|
|||||||
annot_enable = enable and getattr(device, 'SUPPORTS_ANNOTATIONS', False)
|
annot_enable = enable and getattr(device, 'SUPPORTS_ANNOTATIONS', False)
|
||||||
self.annotation_action.setEnabled(annot_enable)
|
self.annotation_action.setEnabled(annot_enable)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class Emailer(Thread):
|
class Emailer(Thread): # {{{
|
||||||
|
|
||||||
def __init__(self, timeout=60):
|
def __init__(self, timeout=60):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
@ -590,8 +595,12 @@ class Emailer(Thread):
|
|||||||
results.append([jobname, e, traceback.format_exc()])
|
results.append([jobname, e, traceback.format_exc()])
|
||||||
callback(results)
|
callback(results)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class DeviceGUI(object):
|
class DeviceMixin(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.db_book_uuid_cache = set()
|
||||||
|
|
||||||
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()
|
||||||
@ -637,7 +646,7 @@ class DeviceGUI(object):
|
|||||||
if not ids or len(ids) == 0:
|
if not ids or len(ids) == 0:
|
||||||
return
|
return
|
||||||
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
||||||
fmts, paths=True, set_metadata=True,
|
fmts, set_metadata=True,
|
||||||
specific_format=specific_format,
|
specific_format=specific_format,
|
||||||
exclude_auto=do_auto_convert)
|
exclude_auto=do_auto_convert)
|
||||||
if do_auto_convert:
|
if do_auto_convert:
|
||||||
@ -647,7 +656,6 @@ class DeviceGUI(object):
|
|||||||
_auto_ids = []
|
_auto_ids = []
|
||||||
|
|
||||||
full_metadata = self.library_view.model().metadata_for(ids)
|
full_metadata = self.library_view.model().metadata_for(ids)
|
||||||
files = [getattr(f, 'name', None) for f in files]
|
|
||||||
|
|
||||||
bad, remove_ids, jobnames = [], [], []
|
bad, remove_ids, jobnames = [], [], []
|
||||||
texts, subjects, attachments, attachment_names = [], [], [], []
|
texts, subjects, attachments, attachment_names = [], [], [], []
|
||||||
@ -679,7 +687,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 != []:
|
||||||
@ -740,7 +748,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):
|
||||||
@ -760,7 +768,7 @@ class DeviceGUI(object):
|
|||||||
for account, fmts in accounts:
|
for account, fmts in accounts:
|
||||||
files, auto = self.library_view.model().\
|
files, auto = self.library_view.model().\
|
||||||
get_preferred_formats_from_ids([id], fmts)
|
get_preferred_formats_from_ids([id], fmts)
|
||||||
files = [f.name for f in files if f is not None]
|
files = [f for f in files if f is not None]
|
||||||
if not files:
|
if not files:
|
||||||
continue
|
continue
|
||||||
attachment = files[0]
|
attachment = files[0]
|
||||||
@ -779,7 +787,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):
|
||||||
@ -824,7 +832,7 @@ class DeviceGUI(object):
|
|||||||
prefix = prefix.decode(preferred_encoding, 'replace')
|
prefix = prefix.decode(preferred_encoding, 'replace')
|
||||||
prefix = ascii_filename(prefix)
|
prefix = ascii_filename(prefix)
|
||||||
names.append('%s_%d%s'%(prefix, id,
|
names.append('%s_%d%s'%(prefix, id,
|
||||||
os.path.splitext(f.name)[1]))
|
os.path.splitext(f)[1]))
|
||||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||||
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
|
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
|
||||||
'rb').read())
|
'rb').read())
|
||||||
@ -837,13 +845,14 @@ class DeviceGUI(object):
|
|||||||
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
|
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
|
||||||
self.upload_books(files, names, metadata,
|
self.upload_books(files, names, metadata,
|
||||||
on_card=on_card,
|
on_card=on_card,
|
||||||
memory=[[f.name for f in 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)]
|
||||||
@ -873,6 +882,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
|
||||||
@ -884,22 +895,21 @@ class DeviceGUI(object):
|
|||||||
prefix = prefix.decode(preferred_encoding, 'replace')
|
prefix = prefix.decode(preferred_encoding, 'replace')
|
||||||
prefix = ascii_filename(prefix)
|
prefix = ascii_filename(prefix)
|
||||||
names.append('%s_%d%s'%(prefix, id,
|
names.append('%s_%d%s'%(prefix, id,
|
||||||
os.path.splitext(f.name)[1]))
|
os.path.splitext(f)[1]))
|
||||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||||
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
|
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
|
||||||
'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' }
|
||||||
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
|
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
|
||||||
self.upload_books(files, names, metadata,
|
self.upload_books(files, names, metadata,
|
||||||
on_card=on_card,
|
on_card=on_card,
|
||||||
memory=[[f.name for f in 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,
|
||||||
@ -914,7 +924,7 @@ class DeviceGUI(object):
|
|||||||
|
|
||||||
_files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
_files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
||||||
settings.format_map,
|
settings.format_map,
|
||||||
paths=True, set_metadata=True,
|
set_metadata=True,
|
||||||
specific_format=specific_format,
|
specific_format=specific_format,
|
||||||
exclude_auto=do_auto_convert)
|
exclude_auto=do_auto_convert)
|
||||||
if do_auto_convert:
|
if do_auto_convert:
|
||||||
@ -930,9 +940,8 @@ class DeviceGUI(object):
|
|||||||
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover, 'rb').read())
|
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover, 'rb').read())
|
||||||
imetadata = iter(metadata)
|
imetadata = iter(metadata)
|
||||||
|
|
||||||
files = [getattr(f, 'name', None) for f in _files]
|
|
||||||
bad, good, gf, names, remove_ids = [], [], [], [], []
|
bad, good, gf, names, remove_ids = [], [], [], [], []
|
||||||
for f in files:
|
for f in _files:
|
||||||
mi = imetadata.next()
|
mi = imetadata.next()
|
||||||
id = ids.next()
|
id = ids.next()
|
||||||
if f is None:
|
if f is None:
|
||||||
@ -954,7 +963,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 != []:
|
||||||
@ -1129,6 +1138,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):
|
||||||
@ -1141,10 +1157,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':{}}
|
||||||
|
if mi.authors:
|
||||||
|
authors = authors_to_string(mi.authors).lower()
|
||||||
authors = re.sub('(?u)\W|[_]', '', authors)
|
authors = re.sub('(?u)\W|[_]', '', authors)
|
||||||
self.db_book_title_cache[title]['authors'][authors] = mi
|
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)
|
||||||
|
|
||||||
@ -1175,12 +1197,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:
|
||||||
|
# Compare against both author and author sort, because
|
||||||
|
# either can appear as the author
|
||||||
|
book_authors = authors_to_string(book.authors).lower()
|
||||||
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
||||||
if book_authors in d['authors']:
|
if book_authors in d['authors']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
book.smart_update(d['authors'][book_authors])
|
book.smart_update(d['authors'][book_authors])
|
||||||
resend_metadata = True
|
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:
|
||||||
|
@ -38,8 +38,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
self.opt_read_metadata.setChecked(self.settings.read_metadata)
|
self.opt_read_metadata.setChecked(self.settings.read_metadata)
|
||||||
else:
|
else:
|
||||||
self.opt_read_metadata.hide()
|
self.opt_read_metadata.hide()
|
||||||
if extra_customization_message and settings.extra_customization:
|
if extra_customization_message:
|
||||||
self.extra_customization_label.setText(extra_customization_message)
|
self.extra_customization_label.setText(extra_customization_message)
|
||||||
|
if settings.extra_customization:
|
||||||
self.opt_extra_customization.setText(settings.extra_customization)
|
self.opt_extra_customization.setText(settings.extra_customization)
|
||||||
else:
|
else:
|
||||||
self.extra_customization_label.setVisible(False)
|
self.extra_customization_label.setVisible(False)
|
||||||
|
@ -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):
|
||||||
|
@ -445,6 +445,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.username.setText(opts.username)
|
self.username.setText(opts.username)
|
||||||
self.password.setText(opts.password if opts.password else '')
|
self.password.setText(opts.password if opts.password else '')
|
||||||
self.opt_max_opds_items.setValue(opts.max_opds_items)
|
self.opt_max_opds_items.setValue(opts.max_opds_items)
|
||||||
|
self.opt_max_opds_ungrouped_items.setValue(opts.max_opds_ungrouped_items)
|
||||||
self.auto_launch.setChecked(config['autolaunch_server'])
|
self.auto_launch.setChecked(config['autolaunch_server'])
|
||||||
self.systray_icon.setChecked(config['systray_icon'])
|
self.systray_icon.setChecked(config['systray_icon'])
|
||||||
self.sync_news.setChecked(config['upload_news_to_device'])
|
self.sync_news.setChecked(config['upload_news_to_device'])
|
||||||
@ -848,6 +849,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
sc.set('port', self.port.value())
|
sc.set('port', self.port.value())
|
||||||
sc.set('max_cover', mcs)
|
sc.set('max_cover', mcs)
|
||||||
sc.set('max_opds_items', self.opt_max_opds_items.value())
|
sc.set('max_opds_items', self.opt_max_opds_items.value())
|
||||||
|
sc.set('max_opds_ungrouped_items',
|
||||||
|
self.opt_max_opds_ungrouped_items.value())
|
||||||
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
|
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
|
||||||
config['upload_news_to_device'] = self.sync_news.isChecked()
|
config['upload_news_to_device'] = self.sync_news.isChecked()
|
||||||
config['search_as_you_type'] = self.search_as_you_type.isChecked()
|
config['search_as_you_type'] = self.search_as_you_type.isChecked()
|
||||||
|
@ -892,6 +892,26 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="6" column="1">
|
||||||
|
<widget class="QSpinBox" name="opt_max_opds_ungrouped_items">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>25</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1000000</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<widget class="QLabel" name="label_16">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max. OPDS &ungrouped items:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_max_opds_ungrouped_items</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -3,6 +3,7 @@ __copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
'''Dialog to create a new custom column'''
|
'''Dialog to create a new custom column'''
|
||||||
|
|
||||||
|
import re
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.QtCore import SIGNAL
|
from PyQt4.QtCore import SIGNAL
|
||||||
@ -94,8 +95,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
col = unicode(self.column_name_box.text()).lower()
|
col = unicode(self.column_name_box.text()).lower()
|
||||||
if not col:
|
if not col:
|
||||||
return self.simple_error('', _('No lookup name was provided'))
|
return self.simple_error('', _('No lookup name was provided'))
|
||||||
if not col.isalnum() or not col[0].isalpha():
|
if re.match('^\w*$', col) is None or not col[0].isalpha():
|
||||||
return self.simple_error('', _('The label must contain only letters and digits, and start with a letter'))
|
return self.simple_error('', _('The label must contain only letters, digits and underscores, and start with a letter'))
|
||||||
col_heading = unicode(self.column_heading_box.text())
|
col_heading = unicode(self.column_heading_box.text())
|
||||||
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
||||||
if col_type == '*text':
|
if col_type == '*text':
|
||||||
|
@ -403,12 +403,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
ag = QCoreApplication.instance().desktop().availableGeometry(self)
|
ag = QCoreApplication.instance().desktop().availableGeometry(self)
|
||||||
self.cover.MAX_HEIGHT = ag.height()-(25 if (islinux or isfreebsd) else 0)-height_of_rest
|
self.cover.MAX_HEIGHT = ag.height()-(25 if (islinux or isfreebsd) else 0)-height_of_rest
|
||||||
self.cover.MAX_WIDTH = ag.width()-(25 if (islinux or isfreebsd) else 0)-width_of_rest
|
self.cover.MAX_WIDTH = ag.width()-(25 if (islinux or isfreebsd) else 0)-width_of_rest
|
||||||
if cover:
|
|
||||||
pm = QPixmap()
|
pm = QPixmap()
|
||||||
|
if cover:
|
||||||
pm.loadFromData(cover)
|
pm.loadFromData(cover)
|
||||||
if not pm.isNull():
|
if pm.isNull():
|
||||||
self.cover.setPixmap(pm)
|
pm = QPixmap(I('book.svg'))
|
||||||
|
else:
|
||||||
self.cover_data = cover
|
self.cover_data = cover
|
||||||
|
self.cover.setPixmap(pm)
|
||||||
self.original_series_name = unicode(self.series.text()).strip()
|
self.original_series_name = unicode(self.series.text()).strip()
|
||||||
if len(db.custom_column_label_map) == 0:
|
if len(db.custom_column_label_map) == 0:
|
||||||
self.central_widget.tabBar().setVisible(False)
|
self.central_widget.tabBar().setVisible(False)
|
||||||
|
403
src/calibre/gui2/init.py
Normal file
403
src/calibre/gui2/init.py
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
_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))
|
||||||
|
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' and \
|
||||||
|
not is_widescreen() else Qt.Horizontal
|
||||||
|
#orientation = 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':
|
||||||
|
from calibre.gui2.status import StatusBar
|
||||||
|
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.side_index+1)%2, False)
|
||||||
|
self.centralwidget.layout().addWidget(self.bd_splitter)
|
||||||
|
|
||||||
|
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,76 @@ 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)
|
||||||
|
self.pi = ProgressIndicator(self, size)
|
||||||
|
self._jobs = QLabel('<b>'+_('Jobs:')+' 0')
|
||||||
|
|
||||||
|
if horizontal:
|
||||||
|
self.setLayout(QHBoxLayout())
|
||||||
|
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 +352,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 +373,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,8 @@ 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
|
from calibre import strftime, isbytestring
|
||||||
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre.gui2.library import DEFAULT_SORT
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
|
||||||
def human_readable(size, precision=1):
|
def human_readable(size, precision=1):
|
||||||
@ -33,6 +34,14 @@ TIME_FMT = '%d %b %Y'
|
|||||||
ALIGNMENT_MAP = {'left': Qt.AlignLeft, 'right': Qt.AlignRight, 'center':
|
ALIGNMENT_MAP = {'left': Qt.AlignLeft, 'right': Qt.AlignRight, 'center':
|
||||||
Qt.AlignHCenter}
|
Qt.AlignHCenter}
|
||||||
|
|
||||||
|
class FormatPath(unicode):
|
||||||
|
|
||||||
|
def __new__(cls, path, orig_file_path):
|
||||||
|
ans = unicode.__new__(cls, path)
|
||||||
|
ans.orig_file_path = orig_file_path
|
||||||
|
ans.deleted_after_upload = False
|
||||||
|
return ans
|
||||||
|
|
||||||
class BooksModel(QAbstractTableModel): # {{{
|
class BooksModel(QAbstractTableModel): # {{{
|
||||||
|
|
||||||
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
|
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
|
||||||
@ -191,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:
|
||||||
@ -213,7 +222,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
self.count_changed()
|
self.count_changed()
|
||||||
|
|
||||||
def search(self, text, refinement, reset=True):
|
def search(self, text, reset=True):
|
||||||
try:
|
try:
|
||||||
self.db.search(text)
|
self.db.search(text)
|
||||||
except ParseException:
|
except ParseException:
|
||||||
@ -224,9 +233,10 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.clear_caches()
|
self.clear_caches()
|
||||||
self.reset()
|
self.reset()
|
||||||
if self.last_search:
|
if self.last_search:
|
||||||
|
# Do not issue search done for the null search. It is used to clear
|
||||||
|
# the search and count records for restrictions
|
||||||
self.searched.emit(True)
|
self.searched.emit(True)
|
||||||
|
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
if not self.db:
|
if not self.db:
|
||||||
return
|
return
|
||||||
@ -257,7 +267,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.sort(col, self.sorted_on[1], reset=reset)
|
self.sort(col, self.sorted_on[1], reset=reset)
|
||||||
|
|
||||||
def research(self, reset=True):
|
def research(self, reset=True):
|
||||||
self.search(self.last_search, False, reset=reset)
|
self.search(self.last_search, reset=reset)
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
if parent and parent.isValid():
|
if parent and parent.isValid():
|
||||||
@ -378,7 +388,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
else:
|
else:
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
def get_preferred_formats_from_ids(self, ids, formats, paths=False,
|
def get_preferred_formats_from_ids(self, ids, formats,
|
||||||
set_metadata=False, specific_format=None,
|
set_metadata=False, specific_format=None,
|
||||||
exclude_auto=False, mode='r+b'):
|
exclude_auto=False, mode='r+b'):
|
||||||
ans = []
|
ans = []
|
||||||
@ -403,12 +413,20 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
as_file=True)) as src:
|
as_file=True)) as src:
|
||||||
shutil.copyfileobj(src, pt)
|
shutil.copyfileobj(src, pt)
|
||||||
pt.flush()
|
pt.flush()
|
||||||
|
if getattr(src, 'name', None):
|
||||||
|
pt.orig_file_path = os.path.abspath(src.name)
|
||||||
pt.seek(0)
|
pt.seek(0)
|
||||||
if set_metadata:
|
if set_metadata:
|
||||||
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
|
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
|
||||||
format)
|
format)
|
||||||
pt.close() if paths else pt.seek(0)
|
pt.close()
|
||||||
ans.append(pt)
|
def to_uni(x):
|
||||||
|
if isbytestring(x):
|
||||||
|
x = x.decode(filesystem_encoding)
|
||||||
|
return x
|
||||||
|
name, op = map(to_uni, map(os.path.abspath, (pt.name,
|
||||||
|
pt.orig_file_path)))
|
||||||
|
ans.append(FormatPath(name, op))
|
||||||
else:
|
else:
|
||||||
need_auto.append(id)
|
need_auto.append(id)
|
||||||
if not exclude_auto:
|
if not exclude_auto:
|
||||||
@ -730,6 +748,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
|
|
||||||
def set_search_restriction(self, s):
|
def set_search_restriction(self, s):
|
||||||
self.db.data.set_search_restriction(s)
|
self.db.data.set_search_restriction(s)
|
||||||
|
self.search('')
|
||||||
|
return self.rowCount(None)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -862,6 +882,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
|
||||||
@ -874,7 +903,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
return flags
|
return flags
|
||||||
|
|
||||||
|
|
||||||
def search(self, text, refinement, reset=True):
|
def search(self, text, reset=True):
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
self.map = list(range(len(self.db)))
|
self.map = list(range(len(self.db)))
|
||||||
else:
|
else:
|
||||||
@ -1086,7 +1115,6 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
idx = self.map[row]
|
idx = self.map[row]
|
||||||
if cname == 'title' :
|
if cname == 'title' :
|
||||||
self.db[idx].title = val
|
self.db[idx].title = val
|
||||||
self.db[idx].title_sorter = val
|
|
||||||
elif cname == 'authors':
|
elif cname == 'authors':
|
||||||
self.db[idx].authors = string_to_authors(val)
|
self.db[idx].authors = string_to_authors(val)
|
||||||
elif cname == 'collections':
|
elif cname == 'collections':
|
||||||
|
@ -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
|
||||||
@ -426,6 +436,26 @@ class BooksView(QTableView): # {{{
|
|||||||
if dy != 0:
|
if dy != 0:
|
||||||
self.column_header.update()
|
self.column_header.update()
|
||||||
|
|
||||||
|
def scroll_to_row(self, row):
|
||||||
|
if row > -1:
|
||||||
|
h = self.horizontalHeader()
|
||||||
|
for i in range(h.count()):
|
||||||
|
if not h.isSectionHidden(i):
|
||||||
|
self.scrollTo(self.model().index(row, i))
|
||||||
|
break
|
||||||
|
|
||||||
|
def set_current_row(self, row, select=True):
|
||||||
|
if row > -1:
|
||||||
|
h = self.horizontalHeader()
|
||||||
|
for i in range(h.count()):
|
||||||
|
if not h.isSectionHidden(i):
|
||||||
|
index = self.model().index(row, i)
|
||||||
|
self.setCurrentIndex(index)
|
||||||
|
if select:
|
||||||
|
sm = self.selectionModel()
|
||||||
|
sm.select(index, sm.ClearAndSelect|sm.Rows)
|
||||||
|
break
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self._model.close()
|
self._model.close()
|
||||||
|
|
||||||
@ -437,10 +467,6 @@ class BooksView(QTableView): # {{{
|
|||||||
self._search_done = search_done
|
self._search_done = search_done
|
||||||
self._model.searched.connect(self.search_done)
|
self._model.searched.connect(self.search_done)
|
||||||
|
|
||||||
def connect_to_restriction_set(self, tv):
|
|
||||||
# must be synchronous (not queued)
|
|
||||||
tv.restriction_set.connect(self._model.set_search_restriction)
|
|
||||||
|
|
||||||
def connect_to_book_display(self, bd):
|
def connect_to_book_display(self, bd):
|
||||||
self._model.new_bookdisplay_data.connect(bd)
|
self._model.new_bookdisplay_data.connect(bd)
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.stack.setCurrentIndex(1)
|
self.stack.setCurrentIndex(1)
|
||||||
self.renderer.start()
|
self.renderer.start()
|
||||||
|
|
||||||
def find(self, search, refinement):
|
def find(self, search):
|
||||||
self.last_search = search
|
self.last_search = search
|
||||||
try:
|
try:
|
||||||
self.document.search(search)
|
self.document.search(search)
|
||||||
|
@ -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()
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="centralwidget">
|
<widget class="QWidget" name="centralwidget">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
<item>
|
<item>
|
||||||
@ -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,256 +310,6 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="Splitter" name="vertical_splitter">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>100</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<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_2">
|
|
||||||
<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="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>
|
|
||||||
</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>
|
|
||||||
<widget class="StatusBar" name="status_bar" native="true"/>
|
|
||||||
</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>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="tool_bar">
|
<widget class="QToolBar" name="tool_bar">
|
||||||
@ -790,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>
|
||||||
@ -820,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,8 @@ typedef long PFreal;
|
|||||||
|
|
||||||
typedef unsigned short QRgb565;
|
typedef unsigned short QRgb565;
|
||||||
|
|
||||||
|
#define FONT_SIZE 18
|
||||||
|
|
||||||
#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
|
||||||
@ -540,6 +542,8 @@ void PictureFlowPrivate::showSlide(int index)
|
|||||||
|
|
||||||
void PictureFlowPrivate::resize(int w, int h)
|
void PictureFlowPrivate::resize(int w, int h)
|
||||||
{
|
{
|
||||||
|
slideHeight = int(float(h)/2.);
|
||||||
|
slideWidth = int(float(slideHeight) * 2/3.);
|
||||||
recalc(w, h);
|
recalc(w, h);
|
||||||
resetSlides();
|
resetSlides();
|
||||||
triggerRender();
|
triggerRender();
|
||||||
@ -709,14 +713,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.setPointSize(FONT_SIZE);
|
||||||
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-FONT_SIZE*3),
|
||||||
Qt::AlignCenter, slideImages->caption(centerIndex));
|
Qt::AlignCenter, slideImages->caption(centerIndex));
|
||||||
|
|
||||||
painter.end();
|
painter.end();
|
||||||
@ -759,8 +764,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.setPointSize(FONT_SIZE);
|
||||||
painter.setFont(font);
|
painter.setFont(font);
|
||||||
|
|
||||||
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
|
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
|
||||||
@ -768,12 +774,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 - FONT_SIZE*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 - FONT_SIZE*3),
|
||||||
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
|
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
|
||||||
@ -57,7 +61,7 @@ class SearchBox2(QComboBox):
|
|||||||
INTERVAL = 1500 #: Time to wait before emitting search signal
|
INTERVAL = 1500 #: Time to wait before emitting search signal
|
||||||
MAX_COUNT = 25
|
MAX_COUNT = 25
|
||||||
|
|
||||||
search = pyqtSignal(object, object)
|
search = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QComboBox.__init__(self, parent)
|
QComboBox.__init__(self, parent)
|
||||||
@ -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,10 +100,16 @@ 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._in_a_search = False
|
self._in_a_search = False
|
||||||
self.setEditText(self.help_text)
|
self.setEditText(self.help_text)
|
||||||
|
if self.timer is not None: # Turn off any timers that got started in setEditText
|
||||||
|
self.killTimer(self.timer)
|
||||||
|
self.timer = None
|
||||||
self.line_edit.home(False)
|
self.line_edit.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)
|
||||||
@ -111,7 +120,6 @@ class SearchBox2(QComboBox):
|
|||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.clear_to_help()
|
self.clear_to_help()
|
||||||
self.search.emit('', False)
|
|
||||||
|
|
||||||
def search_done(self, ok):
|
def search_done(self, ok):
|
||||||
if not unicode(self.currentText()).strip():
|
if not unicode(self.currentText()).strip():
|
||||||
@ -155,9 +163,8 @@ class SearchBox2(QComboBox):
|
|||||||
if not text or text == self.help_text:
|
if not text or text == self.help_text:
|
||||||
return self.clear()
|
return self.clear()
|
||||||
self.help_state = False
|
self.help_state = False
|
||||||
refinement = text.startswith(self.prev_search) and ':' not in text
|
|
||||||
self.prev_search = text
|
self.prev_search = text
|
||||||
self.search.emit(text, refinement)
|
self.search.emit(text)
|
||||||
|
|
||||||
idx = self.findText(text, Qt.MatchFixedString)
|
idx = self.findText(text, Qt.MatchFixedString)
|
||||||
self.block_signals(True)
|
self.block_signals(True)
|
||||||
@ -187,12 +194,15 @@ class SearchBox2(QComboBox):
|
|||||||
self.set_search_string(joiner.join(tags))
|
self.set_search_string(joiner.join(tags))
|
||||||
|
|
||||||
def set_search_string(self, txt):
|
def set_search_string(self, txt):
|
||||||
|
if not txt:
|
||||||
|
self.clear_to_help()
|
||||||
|
return
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
self.setEditText(txt)
|
self.setEditText(txt)
|
||||||
if self.timer is not None: # Turn off any timers that got started in setEditText
|
if self.timer is not None: # Turn off any timers that got started in setEditText
|
||||||
self.killTimer(self.timer)
|
self.killTimer(self.timer)
|
||||||
self.timer = None
|
self.timer = None
|
||||||
self.search.emit(txt, False)
|
self.search.emit(txt)
|
||||||
self.line_edit.end(False)
|
self.line_edit.end(False)
|
||||||
self.initial_state = False
|
self.initial_state = False
|
||||||
|
|
||||||
@ -234,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
|
||||||
@ -326,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)
|
|
||||||
|
|
||||||
|
|
@ -52,10 +52,7 @@ class BookInfoDisplay(QWidget):
|
|||||||
QLabel.__init__(self)
|
QLabel.__init__(self)
|
||||||
self.setMaximumWidth(81)
|
self.setMaximumWidth(81)
|
||||||
self.setMaximumHeight(108)
|
self.setMaximumHeight(108)
|
||||||
self.default_pixmap = QPixmap(coverpath).scaled(self.maximumWidth(),
|
self.default_pixmap = QPixmap(coverpath)
|
||||||
self.maximumHeight(),
|
|
||||||
Qt.IgnoreAspectRatio,
|
|
||||||
Qt.SmoothTransformation)
|
|
||||||
self.setScaledContents(True)
|
self.setScaledContents(True)
|
||||||
self.statusbar_height = 120
|
self.statusbar_height = 120
|
||||||
self.setPixmap(self.default_pixmap)
|
self.setPixmap(self.default_pixmap)
|
||||||
@ -64,7 +61,7 @@ class BookInfoDisplay(QWidget):
|
|||||||
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)
|
||||||
@ -165,17 +162,48 @@ 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
|
||||||
|
|
||||||
|
def reset_info(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def show_data(self, data):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
|
||||||
|
|
||||||
|
files_dropped = pyqtSignal(object, object)
|
||||||
|
show_book_info = pyqtSignal()
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
@ -195,15 +223,6 @@ class StatusBar(QStatusBar):
|
|||||||
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,19 +10,22 @@ 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): # {{{
|
||||||
|
|
||||||
refresh_required = pyqtSignal()
|
refresh_required = pyqtSignal()
|
||||||
restriction_set = pyqtSignal(object)
|
|
||||||
tags_marked = pyqtSignal(object, object)
|
tags_marked = pyqtSignal(object, object)
|
||||||
user_category_edit = pyqtSignal(object)
|
user_category_edit = pyqtSignal(object)
|
||||||
tag_list_edit = pyqtSignal(object, object)
|
tag_list_edit = pyqtSignal(object, object)
|
||||||
@ -30,31 +33,34 @@ 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, restriction):
|
def set_database(self, db, tag_match, popularity):
|
||||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||||
self._model = TagsModel(db, parent=self,
|
self._model = TagsModel(db, parent=self,
|
||||||
hidden_categories=self.hidden_categories)
|
hidden_categories=self.hidden_categories,
|
||||||
|
search_restriction=None)
|
||||||
self.popularity = popularity
|
self.popularity = popularity
|
||||||
self.restriction = restriction
|
|
||||||
self.tag_match = tag_match
|
self.tag_match = tag_match
|
||||||
self.db = db
|
self.db = db
|
||||||
|
self.search_restriction = None
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.clicked.connect(self.toggle)
|
self.clicked.connect(self.toggle)
|
||||||
self.customContextMenuRequested.connect(self.show_context_menu)
|
self.customContextMenuRequested.connect(self.show_context_menu)
|
||||||
self.popularity.setChecked(config['sort_by_popularity'])
|
self.popularity.setChecked(config['sort_by_popularity'])
|
||||||
self.popularity.stateChanged.connect(self.sort_changed)
|
self.popularity.stateChanged.connect(self.sort_changed)
|
||||||
self.restriction.activated[str].connect(self.search_restriction_set)
|
|
||||||
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
|
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
|
||||||
db.add_listener(self.database_changed)
|
db.add_listener(self.database_changed)
|
||||||
self.saved_searches_changed(recount=False)
|
|
||||||
|
|
||||||
def database_changed(self, event, ids):
|
def database_changed(self, event, ids):
|
||||||
self.refresh_required.emit()
|
self.refresh_required.emit()
|
||||||
@ -65,19 +71,14 @@ class TagsView(QTreeView): # {{{
|
|||||||
|
|
||||||
def sort_changed(self, state):
|
def sort_changed(self, state):
|
||||||
config.set('sort_by_popularity', state == Qt.Checked)
|
config.set('sort_by_popularity', state == Qt.Checked)
|
||||||
self.model().refresh()
|
self.recount()
|
||||||
# self.search_restriction_set()
|
|
||||||
|
|
||||||
def search_restriction_set(self, s):
|
def set_search_restriction(self, s):
|
||||||
self.clear()
|
if s:
|
||||||
if len(s) == 0:
|
self.search_restriction = s
|
||||||
self.search_restriction = ''
|
|
||||||
else:
|
else:
|
||||||
self.search_restriction = 'search:"%s"' % unicode(s).strip()
|
self.search_restriction = None
|
||||||
self.model().set_search_restriction(self.search_restriction)
|
self.set_new_model()
|
||||||
self.restriction_set.emit(self.search_restriction)
|
|
||||||
self.recount() # Must happen after the emission of the restriction_set signal
|
|
||||||
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
def mouseReleaseEvent(self, event):
|
||||||
# Swallow everything except leftButton so context menus work correctly
|
# Swallow everything except leftButton so context menus work correctly
|
||||||
@ -144,7 +145,8 @@ class TagsView(QTreeView): # {{{
|
|||||||
# the possibility of renaming that item
|
# the possibility of renaming that item
|
||||||
if tag_name and \
|
if tag_name and \
|
||||||
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
|
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
|
||||||
self.db.field_metadata[key]['is_custom']):
|
self.db.field_metadata[key]['is_custom'] and \
|
||||||
|
self.db.field_metadata[key]['datatype'] != 'rating'):
|
||||||
self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
|
self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
|
||||||
partial(self.context_menu_handler, action='edit_item',
|
partial(self.context_menu_handler, action='edit_item',
|
||||||
category=tag_item, index=index))
|
category=tag_item, index=index))
|
||||||
@ -187,29 +189,24 @@ class TagsView(QTreeView): # {{{
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
if self.model():
|
||||||
self.model().clear_state()
|
self.model().clear_state()
|
||||||
|
|
||||||
def saved_searches_changed(self, recount=True):
|
def is_visible(self, idx):
|
||||||
p = prefs['saved_searches'].keys()
|
item = idx.internalPointer()
|
||||||
p.sort()
|
if getattr(item, 'type', None) == TagTreeItem.TAG:
|
||||||
t = self.restriction.currentText()
|
idx = idx.parent()
|
||||||
self.restriction.clear() # rebuild the restrictions combobox using current saved searches
|
return self.isExpanded(idx)
|
||||||
self.restriction.addItem('')
|
|
||||||
for s in p:
|
|
||||||
self.restriction.addItem(s)
|
|
||||||
if t in p: # redo the current restriction, if there was one
|
|
||||||
self.restriction.setCurrentIndex(self.restriction.findText(t))
|
|
||||||
self.search_restriction_set(t)
|
|
||||||
if recount:
|
|
||||||
self.recount()
|
|
||||||
|
|
||||||
def recount(self, *args):
|
def recount(self, *args):
|
||||||
ci = self.currentIndex()
|
ci = self.currentIndex()
|
||||||
if not ci.isValid():
|
if not ci.isValid():
|
||||||
ci = self.indexAt(QPoint(10, 10))
|
ci = self.indexAt(QPoint(10, 10))
|
||||||
path = self.model().path_for_index(ci)
|
path = self.model().path_for_index(ci) if self.is_visible(ci) else None
|
||||||
try:
|
try:
|
||||||
self.model().refresh()
|
if not self.model().refresh(): # categories changed!
|
||||||
|
self.set_new_model()
|
||||||
|
path = None
|
||||||
except: #Database connection could be closed if an integrity check is happening
|
except: #Database connection could be closed if an integrity check is happening
|
||||||
pass
|
pass
|
||||||
if path:
|
if path:
|
||||||
@ -222,9 +219,16 @@ class TagsView(QTreeView): # {{{
|
|||||||
# gone, or if columns have been hidden or restored, we must rebuild the
|
# gone, or if columns have been hidden or restored, we must rebuild the
|
||||||
# model. Reason: it is much easier than reconstructing the browser tree.
|
# model. Reason: it is much easier than reconstructing the browser tree.
|
||||||
def set_new_model(self):
|
def set_new_model(self):
|
||||||
|
try:
|
||||||
self._model = TagsModel(self.db, parent=self,
|
self._model = TagsModel(self.db, parent=self,
|
||||||
hidden_categories=self.hidden_categories)
|
hidden_categories=self.hidden_categories,
|
||||||
|
search_restriction=self.search_restriction)
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
|
except:
|
||||||
|
# The DB must be gone. Set the model to None and hope that someone
|
||||||
|
# will call set_database later. I don't know if this in fact works
|
||||||
|
self._model = None
|
||||||
|
self.setModel(None)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class TagTreeItem(object): # {{{
|
class TagTreeItem(object): # {{{
|
||||||
@ -311,7 +315,7 @@ class TagTreeItem(object): # {{{
|
|||||||
|
|
||||||
class TagsModel(QAbstractItemModel): # {{{
|
class TagsModel(QAbstractItemModel): # {{{
|
||||||
|
|
||||||
def __init__(self, db, parent, hidden_categories=None):
|
def __init__(self, db, parent, hidden_categories=None, search_restriction=None):
|
||||||
QAbstractItemModel.__init__(self, parent)
|
QAbstractItemModel.__init__(self, parent)
|
||||||
|
|
||||||
# must do this here because 'QPixmap: Must construct a QApplication
|
# must do this here because 'QPixmap: Must construct a QApplication
|
||||||
@ -333,20 +337,10 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.tags_view = parent
|
self.tags_view = parent
|
||||||
self.hidden_categories = hidden_categories
|
self.hidden_categories = hidden_categories
|
||||||
self.search_restriction = ''
|
self.search_restriction = search_restriction
|
||||||
self.ignore_next_search = 0
|
self.row_map = []
|
||||||
|
|
||||||
# Reconstruct the user categories, putting them into metadata
|
|
||||||
tb_cats = self.db.field_metadata
|
|
||||||
for k in tb_cats.keys():
|
|
||||||
if tb_cats[k]['kind'] in ['user', 'search']:
|
|
||||||
del tb_cats[k]
|
|
||||||
for user_cat in sorted(prefs['user_categories'].keys()):
|
|
||||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
|
||||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
|
||||||
if len(saved_searches.names()):
|
|
||||||
tb_cats.add_search_category(label='search', name=_('Searches'))
|
|
||||||
|
|
||||||
|
# get_node_tree cannot return None here, because row_map is empty
|
||||||
data = self.get_node_tree(config['sort_by_popularity'])
|
data = self.get_node_tree(config['sort_by_popularity'])
|
||||||
self.root_item = TagTreeItem()
|
self.root_item = TagTreeItem()
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
@ -367,29 +361,44 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.search_restriction = s
|
self.search_restriction = s
|
||||||
|
|
||||||
def get_node_tree(self, sort):
|
def get_node_tree(self, sort):
|
||||||
|
old_row_map = self.row_map[:]
|
||||||
self.row_map = []
|
self.row_map = []
|
||||||
self.categories = []
|
self.categories = []
|
||||||
|
|
||||||
if len(self.search_restriction):
|
# Reconstruct the user categories, putting them into metadata
|
||||||
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map,
|
tb_cats = self.db.field_metadata
|
||||||
ids=self.db.search(self.search_restriction, return_matches=True))
|
for k in tb_cats.keys():
|
||||||
|
if tb_cats[k]['kind'] in ['user', 'search']:
|
||||||
|
del tb_cats[k]
|
||||||
|
for user_cat in sorted(prefs['user_categories'].keys()):
|
||||||
|
cat_name = user_cat+':' # add the ':' to avoid name collision
|
||||||
|
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||||
|
if len(saved_searches.names()):
|
||||||
|
tb_cats.add_search_category(label='search', name=_('Searches'))
|
||||||
|
|
||||||
|
# Now get the categories
|
||||||
|
if self.search_restriction:
|
||||||
|
data = self.db.get_categories(sort_on_count=sort,
|
||||||
|
icon_map=self.category_icon_map,
|
||||||
|
ids=self.db.search('', return_matches=True))
|
||||||
else:
|
else:
|
||||||
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
|
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
|
||||||
|
|
||||||
tb_categories = self.db.field_metadata
|
tb_categories = self.db.field_metadata
|
||||||
self.category_items = {}
|
|
||||||
for category in tb_categories:
|
for category in tb_categories:
|
||||||
if category in data: # They should always be there, but ...
|
if category in data: # The search category can come and go
|
||||||
# make a map of sets of names per category for duplicate
|
|
||||||
# checking when editing
|
|
||||||
self.category_items[category] = set([tag.name for tag in data[category]])
|
|
||||||
self.row_map.append(category)
|
self.row_map.append(category)
|
||||||
self.categories.append(tb_categories[category]['name'])
|
self.categories.append(tb_categories[category]['name'])
|
||||||
|
if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map):
|
||||||
|
# A category has been added or removed. We must force a rebuild of
|
||||||
|
# the model
|
||||||
|
return None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
data = self.get_node_tree(config['sort_by_popularity']) # get category data
|
data = self.get_node_tree(config['sort_by_popularity']) # get category data
|
||||||
|
if data is None:
|
||||||
|
return False
|
||||||
row_index = -1
|
row_index = -1
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
@ -411,6 +420,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
tag.state = state_map.get(tag.name, 0)
|
tag.state = state_map.get(tag.name, 0)
|
||||||
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
|
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
|
||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
|
return True
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
return 1
|
return 1
|
||||||
@ -424,6 +434,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
def setData(self, index, value, role=Qt.EditRole):
|
def setData(self, index, value, role=Qt.EditRole):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return NONE
|
return NONE
|
||||||
|
# set up to position at the category label
|
||||||
|
path = self.path_for_index(self.parent(index))
|
||||||
val = unicode(value.toString())
|
val = unicode(value.toString())
|
||||||
if not val:
|
if not val:
|
||||||
error_dialog(self.tags_view, _('Item is blank'),
|
error_dialog(self.tags_view, _('Item is blank'),
|
||||||
@ -431,15 +443,14 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
return False
|
return False
|
||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
key = item.parent.category_key
|
key = item.parent.category_key
|
||||||
# make certain we know about the category
|
# make certain we know about the item's category
|
||||||
if key not in self.db.field_metadata:
|
if key not in self.db.field_metadata:
|
||||||
return
|
return
|
||||||
if val in self.category_items[key]:
|
|
||||||
error_dialog(self.tags_view, 'Duplicate item',
|
|
||||||
_('The name %s is already used.')%val).exec_()
|
|
||||||
return False
|
|
||||||
oldval = item.tag.name
|
|
||||||
if key == 'search':
|
if key == 'search':
|
||||||
|
if val in saved_searches.names():
|
||||||
|
error_dialog(self.tags_view, _('Duplicate search name'),
|
||||||
|
_('The saved search name %s is already used.')%val).exec_()
|
||||||
|
return False
|
||||||
saved_searches.rename(unicode(item.data(role).toString()), val)
|
saved_searches.rename(unicode(item.data(role).toString()), val)
|
||||||
self.tags_view.search_item_renamed.emit()
|
self.tags_view.search_item_renamed.emit()
|
||||||
else:
|
else:
|
||||||
@ -456,10 +467,12 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
label=self.db.field_metadata[key]['label'])
|
label=self.db.field_metadata[key]['label'])
|
||||||
self.tags_view.tag_item_renamed.emit()
|
self.tags_view.tag_item_renamed.emit()
|
||||||
item.tag.name = val
|
item.tag.name = val
|
||||||
self.dataChanged.emit(index, index)
|
self.refresh() # Should work, because no categories can have disappeared
|
||||||
# replace the old value in the duplicate detection map with the new one
|
if path:
|
||||||
self.category_items[key].discard(oldval)
|
idx = self.index_for_path(path)
|
||||||
self.category_items[key].add(val)
|
if idx.isValid():
|
||||||
|
self.tags_view.setCurrentIndex(idx)
|
||||||
|
self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def headerData(self, *args):
|
def headerData(self, *args):
|
||||||
@ -544,12 +557,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
def clear_state(self):
|
def clear_state(self):
|
||||||
self.reset_all_states()
|
self.reset_all_states()
|
||||||
|
|
||||||
def reinit(self, *args, **kwargs):
|
|
||||||
if self.ignore_next_search == 0:
|
|
||||||
self.reset_all_states()
|
|
||||||
else:
|
|
||||||
self.ignore_next_search -= 1
|
|
||||||
|
|
||||||
def toggle(self, index, exclusive):
|
def toggle(self, index, exclusive):
|
||||||
if not index.isValid(): return False
|
if not index.isValid(): return False
|
||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
@ -557,7 +564,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
item.toggle()
|
item.toggle()
|
||||||
if exclusive:
|
if exclusive:
|
||||||
self.reset_all_states(except_=item.tag)
|
self.reset_all_states(except_=item.tag)
|
||||||
self.ignore_next_search = 2
|
|
||||||
self.dataChanged.emit(index, index)
|
self.dataChanged.emit(index, index)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -590,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)
|
||||||
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -16,9 +16,9 @@ from threading import Thread
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \
|
from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \
|
||||||
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
||||||
QToolButton, QDialog, QDesktopServices, \
|
QDialog, QDesktopServices, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||||
QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\
|
QMessageBox, QHelpEvent, QInputDialog,\
|
||||||
QThread, pyqtSignal
|
QThread, pyqtSignal
|
||||||
from PyQt4.QtSvg import QSvgRenderer
|
from PyQt4.QtSvg import QSvgRenderer
|
||||||
|
|
||||||
@ -29,30 +29,27 @@ from calibre.utils.filenames import ascii_filename
|
|||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.utils.search_query_parser import saved_searches
|
|
||||||
from calibre.devices.errors import UserFeedback
|
from calibre.devices.errors import UserFeedback
|
||||||
from calibre.gui2 import warning_dialog, choose_files, error_dialog, \
|
from calibre.gui2 import warning_dialog, choose_files, error_dialog, \
|
||||||
question_dialog,\
|
question_dialog,\
|
||||||
pixmap_to_data, choose_dir, \
|
pixmap_to_data, choose_dir, \
|
||||||
Dispatcher, gprefs, \
|
Dispatcher, gprefs, \
|
||||||
available_height, \
|
|
||||||
max_available_height, config, info_dialog, \
|
max_available_height, config, info_dialog, \
|
||||||
available_width, GetMetadata
|
GetMetadata
|
||||||
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||||
from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS
|
from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS
|
||||||
from calibre.gui2.wizard import move_library
|
from calibre.gui2.wizard import move_library
|
||||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||||
from calibre.gui2.update import CheckForUpdates
|
from calibre.gui2.update import CheckForUpdates
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
from calibre.gui2.main_ui import Ui_MainWindow
|
from calibre.gui2.main_ui import Ui_MainWindow
|
||||||
from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceGUI, Emailer
|
from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceMixin, Emailer
|
||||||
from calibre.gui2.jobs import JobManager, JobsDialog
|
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
||||||
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
|
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
|
||||||
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
||||||
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
|
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
|
||||||
fetch_scheduled_recipe, generate_catalog
|
fetch_scheduled_recipe, generate_catalog
|
||||||
from calibre.gui2.dialogs.config import ConfigDialog
|
from calibre.gui2.dialogs.config import ConfigDialog
|
||||||
from calibre.gui2.dialogs.search import SearchDialog
|
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.gui2.dialogs.book_info import BookInfo
|
from calibre.gui2.dialogs.book_info import BookInfo
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
@ -60,24 +57,12 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
|||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.library.caches import CoverCache
|
from calibre.library.caches import CoverCache
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.dialogs.tag_categories import TagCategories
|
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
|
||||||
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||||
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||||
|
from calibre.gui2.tag_view import TagBrowserMixin
|
||||||
|
|
||||||
class SaveMenu(QMenu):
|
class Listener(Thread): # {{{
|
||||||
|
|
||||||
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))
|
|
||||||
self.connect(action, SIGNAL('triggered(bool)'),
|
|
||||||
getattr(self, 'do_'+ext))
|
|
||||||
|
|
||||||
def do(self, ext, *args):
|
|
||||||
self.emit(SIGNAL('save_fmt(PyQt_PyObject)'), ext)
|
|
||||||
|
|
||||||
class Listener(Thread):
|
|
||||||
|
|
||||||
def __init__(self, listener):
|
def __init__(self, listener):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
@ -102,7 +87,9 @@ class Listener(Thread):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class SystemTrayIcon(QSystemTrayIcon):
|
# }}}
|
||||||
|
|
||||||
|
class SystemTrayIcon(QSystemTrayIcon): # {{{
|
||||||
|
|
||||||
def __init__(self, icon, parent):
|
def __init__(self, icon, parent):
|
||||||
QSystemTrayIcon.__init__(self, icon, parent)
|
QSystemTrayIcon.__init__(self, icon, parent)
|
||||||
@ -115,7 +102,11 @@ class SystemTrayIcon(QSystemTrayIcon):
|
|||||||
return True
|
return True
|
||||||
return QSystemTrayIcon.event(self, ev)
|
return QSystemTrayIcon.event(self, ev)
|
||||||
|
|
||||||
class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
# }}}
|
||||||
|
|
||||||
|
class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin,
|
||||||
|
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||||
|
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin):
|
||||||
'The main GUI'
|
'The main GUI'
|
||||||
|
|
||||||
def set_default_thumbnail(self, height):
|
def set_default_thumbnail(self, height):
|
||||||
@ -152,35 +143,25 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.check_messages_timer.start(1000)
|
self.check_messages_timer.start(1000)
|
||||||
|
|
||||||
Ui_MainWindow.__init__(self)
|
Ui_MainWindow.__init__(self)
|
||||||
self.setupUi(self)
|
|
||||||
self.setWindowTitle(__appname__)
|
# Jobs Button {{{
|
||||||
|
self.job_manager = JobManager()
|
||||||
|
self.jobs_dialog = JobsDialog(self, self.job_manager)
|
||||||
|
self.jobs_button = JobsButton()
|
||||||
|
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
LayoutMixin.__init__(self)
|
||||||
|
|
||||||
self.restriction_count_of_books_in_view = 0
|
self.restriction_count_of_books_in_view = 0
|
||||||
self.restriction_count_of_books_in_library = 0
|
self.restriction_count_of_books_in_library = 0
|
||||||
self.restriction_in_effect = False
|
self.restriction_in_effect = False
|
||||||
self.search.initialize('main_search_history', colorize=True,
|
|
||||||
help_text=_('Search (For Advanced Search click the button to the left)'))
|
|
||||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear)
|
|
||||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
|
|
||||||
self.search_clear()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
self.progress_indicator = ProgressIndicator(self)
|
self.progress_indicator = ProgressIndicator(self)
|
||||||
self.verbose = opts.verbose
|
self.verbose = opts.verbose
|
||||||
self.get_metadata = GetMetadata()
|
self.get_metadata = GetMetadata()
|
||||||
self.read_settings()
|
|
||||||
self.job_manager = JobManager()
|
|
||||||
self.emailer = Emailer()
|
self.emailer = Emailer()
|
||||||
self.emailer.start()
|
self.emailer.start()
|
||||||
self.jobs_dialog = JobsDialog(self, self.job_manager)
|
|
||||||
self.upload_memory = {}
|
self.upload_memory = {}
|
||||||
self.delete_memory = {}
|
self.delete_memory = {}
|
||||||
self.conversion_jobs = {}
|
self.conversion_jobs = {}
|
||||||
@ -234,14 +215,15 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.connect(self.system_tray_icon,
|
self.connect(self.system_tray_icon,
|
||||||
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
|
||||||
self.system_tray_icon_activated)
|
self.system_tray_icon_activated)
|
||||||
self.tool_bar.contextMenuEvent = self.no_op
|
|
||||||
|
DeviceMixin.__init__(self)
|
||||||
|
|
||||||
####################### Start spare job server ########################
|
####################### Start spare job server ########################
|
||||||
QTimer.singleShot(1000, self.add_spare_server)
|
QTimer.singleShot(1000, self.add_spare_server)
|
||||||
|
|
||||||
####################### Setup device detection ########################
|
####################### Setup device detection ########################
|
||||||
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
||||||
self.job_manager, Dispatcher(self.status_bar.showMessage))
|
self.job_manager, Dispatcher(self.status_bar.show_message))
|
||||||
self.device_manager.start()
|
self.device_manager.start()
|
||||||
|
|
||||||
|
|
||||||
@ -271,298 +253,31 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.update_checker.update_found.connect(self.update_found,
|
self.update_checker.update_found.connect(self.update_found,
|
||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
self.update_checker.start()
|
self.update_checker.start()
|
||||||
|
|
||||||
####################### Status Bar #####################
|
####################### Status Bar #####################
|
||||||
self.status_bar.initialize(self.system_tray_icon)
|
self.status_bar.initialize(self.system_tray_icon)
|
||||||
self.status_bar.show_book_info.connect(self.show_book_info)
|
self.book_details.show_book_info.connect(self.show_book_info)
|
||||||
self.status_bar.files_dropped.connect(self.files_dropped_on_book)
|
self.book_details.files_dropped.connect(self.files_dropped_on_book)
|
||||||
|
|
||||||
####################### Setup Toolbar #####################
|
####################### Setup Toolbar #####################
|
||||||
md = QMenu()
|
ToolbarMixin.__init__(self)
|
||||||
md.addAction(_('Edit metadata individually'))
|
|
||||||
md.addSeparator()
|
|
||||||
md.addAction(_('Edit metadata in bulk'))
|
|
||||||
md.addSeparator()
|
|
||||||
md.addAction(_('Download metadata and covers'))
|
|
||||||
md.addAction(_('Download only metadata'))
|
|
||||||
md.addAction(_('Download only covers'))
|
|
||||||
md.addAction(_('Download only social metadata'))
|
|
||||||
self.metadata_menu = md
|
|
||||||
|
|
||||||
mb = QMenu()
|
####################### Search boxes ########################
|
||||||
mb.addAction(_('Merge into first selected book - delete others'))
|
SavedSearchBoxMixin.__init__(self)
|
||||||
mb.addSeparator()
|
SearchBoxMixin.__init__(self)
|
||||||
mb.addAction(_('Merge into first selected book - keep others'))
|
|
||||||
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_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_menu.addAction(_('Add books from directories, including '
|
|
||||||
'sub directories (Multiple books per directory, assumes every '
|
|
||||||
'ebook file is a different book)'))
|
|
||||||
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
|
|
||||||
'formats)'))
|
|
||||||
self.action_add.setMenu(self.add_menu)
|
|
||||||
QObject.connect(self.action_add, SIGNAL("triggered(bool)"),
|
|
||||||
self.add_books)
|
|
||||||
QObject.connect(self.add_menu.actions()[0], SIGNAL("triggered(bool)"),
|
|
||||||
self.add_books)
|
|
||||||
QObject.connect(self.add_menu.actions()[1], SIGNAL("triggered(bool)"),
|
|
||||||
self.add_recursive_single)
|
|
||||||
QObject.connect(self.add_menu.actions()[2], SIGNAL("triggered(bool)"),
|
|
||||||
self.add_recursive_multiple)
|
|
||||||
QObject.connect(self.add_menu.actions()[3], SIGNAL('triggered(bool)'),
|
|
||||||
self.add_empty)
|
|
||||||
QObject.connect(self.action_del, SIGNAL("triggered(bool)"),
|
|
||||||
self.delete_books)
|
|
||||||
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"),
|
|
||||||
self.edit_metadata)
|
|
||||||
self.__em1__ = partial(self.edit_metadata, bulk=False)
|
|
||||||
QObject.connect(md.actions()[0], SIGNAL('triggered(bool)'),
|
|
||||||
self.__em1__)
|
|
||||||
self.__em2__ = partial(self.edit_metadata, bulk=True)
|
|
||||||
QObject.connect(md.actions()[2], SIGNAL('triggered(bool)'),
|
|
||||||
self.__em2__)
|
|
||||||
self.__em3__ = partial(self.download_metadata, covers=True)
|
|
||||||
QObject.connect(md.actions()[4], SIGNAL('triggered(bool)'),
|
|
||||||
self.__em3__)
|
|
||||||
self.__em4__ = partial(self.download_metadata, covers=False)
|
|
||||||
QObject.connect(md.actions()[5], SIGNAL('triggered(bool)'),
|
|
||||||
self.__em4__)
|
|
||||||
self.__em5__ = partial(self.download_metadata, covers=True,
|
|
||||||
set_metadata=False, set_social_metadata=False)
|
|
||||||
QObject.connect(md.actions()[6], SIGNAL('triggered(bool)'),
|
|
||||||
self.__em5__)
|
|
||||||
self.__em6__ = partial(self.download_metadata, covers=False,
|
|
||||||
set_metadata=False, set_social_metadata=True)
|
|
||||||
QObject.connect(md.actions()[7], SIGNAL('triggered(bool)'),
|
|
||||||
self.__em6__)
|
|
||||||
|
|
||||||
QObject.connect(self.action_merge, SIGNAL("triggered(bool)"),
|
|
||||||
self.merge_books)
|
|
||||||
QObject.connect(mb.actions()[0], SIGNAL('triggered(bool)'),
|
|
||||||
self.merge_books)
|
|
||||||
self.__mb1__ = partial(self.merge_books, safe_merge=True)
|
|
||||||
QObject.connect(mb.actions()[2], SIGNAL('triggered(bool)'),
|
|
||||||
self.__mb1__)
|
|
||||||
|
|
||||||
self.save_menu = QMenu()
|
|
||||||
self.save_menu.addAction(_('Save to disk'))
|
|
||||||
self.save_menu.addAction(_('Save to disk in a single directory'))
|
|
||||||
self.save_menu.addAction(_('Save only %s format to disk')%
|
|
||||||
prefs['output_format'].upper())
|
|
||||||
self.save_menu.addAction(
|
|
||||||
_('Save only %s format to disk in a single directory')%
|
|
||||||
prefs['output_format'].upper())
|
|
||||||
|
|
||||||
self.save_sub_menu = SaveMenu(self)
|
|
||||||
self.save_menu.addMenu(self.save_sub_menu)
|
|
||||||
self.connect(self.save_sub_menu, SIGNAL('save_fmt(PyQt_PyObject)'),
|
|
||||||
self.save_specific_format_disk)
|
|
||||||
|
|
||||||
self.view_menu = QMenu()
|
|
||||||
self.view_menu.addAction(_('View'))
|
|
||||||
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)
|
|
||||||
|
|
||||||
self.delete_menu = QMenu()
|
|
||||||
self.delete_menu.addAction(_('Remove selected books'))
|
|
||||||
self.delete_menu.addAction(
|
|
||||||
_('Remove files of a specific format from selected books..'))
|
|
||||||
self.delete_menu.addAction(
|
|
||||||
_('Remove all formats from selected books, except...'))
|
|
||||||
self.delete_menu.addAction(
|
|
||||||
_('Remove covers from selected books'))
|
|
||||||
self.action_del.setMenu(self.delete_menu)
|
|
||||||
QObject.connect(self.action_save, SIGNAL("triggered(bool)"),
|
|
||||||
self.save_to_disk)
|
|
||||||
QObject.connect(self.save_menu.actions()[0], SIGNAL("triggered(bool)"),
|
|
||||||
self.save_to_disk)
|
|
||||||
QObject.connect(self.save_menu.actions()[1], SIGNAL("triggered(bool)"),
|
|
||||||
self.save_to_single_dir)
|
|
||||||
QObject.connect(self.save_menu.actions()[2], SIGNAL("triggered(bool)"),
|
|
||||||
self.save_single_format_to_disk)
|
|
||||||
QObject.connect(self.save_menu.actions()[3], SIGNAL("triggered(bool)"),
|
|
||||||
self.save_single_fmt_to_single_dir)
|
|
||||||
QObject.connect(self.action_view, SIGNAL("triggered(bool)"),
|
|
||||||
self.view_book)
|
|
||||||
QObject.connect(self.view_menu.actions()[0],
|
|
||||||
SIGNAL("triggered(bool)"), self.view_book)
|
|
||||||
QObject.connect(self.view_menu.actions()[1],
|
|
||||||
SIGNAL("triggered(bool)"), self.view_specific_format,
|
|
||||||
Qt.QueuedConnection)
|
|
||||||
self.connect(self.action_open_containing_folder,
|
|
||||||
SIGNAL('triggered(bool)'), self.view_folder)
|
|
||||||
|
|
||||||
self.delete_menu.actions()[0].triggered.connect(self.delete_books)
|
|
||||||
self.delete_menu.actions()[1].triggered.connect(self.delete_selected_formats)
|
|
||||||
self.delete_menu.actions()[2].triggered.connect(self.delete_all_but_selected_formats)
|
|
||||||
self.delete_menu.actions()[3].triggered.connect(self.delete_covers)
|
|
||||||
|
|
||||||
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
|
||||||
self.addAction(self.action_open_containing_folder)
|
|
||||||
self.action_sync.setShortcut(Qt.Key_D)
|
|
||||||
self.action_sync.setEnabled(True)
|
|
||||||
self.create_device_menu()
|
|
||||||
self.connect(self.action_sync, SIGNAL('triggered(bool)'),
|
|
||||||
self._sync_action_triggered)
|
|
||||||
|
|
||||||
self.action_edit.setMenu(md)
|
|
||||||
self.action_save.setMenu(self.save_menu)
|
|
||||||
|
|
||||||
cm = QMenu()
|
|
||||||
cm.addAction(_('Convert individually'))
|
|
||||||
cm.addAction(_('Bulk convert'))
|
|
||||||
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._convert_single_hook = partial(self.convert_ebook, bulk=False)
|
|
||||||
QObject.connect(cm.actions()[0],
|
|
||||||
SIGNAL('triggered(bool)'), self._convert_single_hook)
|
|
||||||
self._convert_bulk_hook = partial(self.convert_ebook, bulk=True)
|
|
||||||
QObject.connect(cm.actions()[1],
|
|
||||||
SIGNAL('triggered(bool)'), self._convert_bulk_hook)
|
|
||||||
QObject.connect(self.action_convert,
|
|
||||||
SIGNAL('triggered(bool)'), self.convert_ebook)
|
|
||||||
self.convert_menu = cm
|
|
||||||
|
|
||||||
pm = QMenu()
|
|
||||||
ap = self.action_preferences
|
|
||||||
pm.addAction(ap.icon(), ap.text())
|
|
||||||
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'))
|
|
||||||
self.connect(pm.actions()[0], SIGNAL('triggered(bool)'),
|
|
||||||
self.do_config)
|
|
||||||
self.connect(pm.actions()[1], SIGNAL('triggered(bool)'),
|
|
||||||
self.run_wizard)
|
|
||||||
self.action_preferences.setMenu(pm)
|
|
||||||
self.preferences_menu = pm
|
|
||||||
|
|
||||||
self.tool_bar.widgetForAction(self.action_news).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.widgetForAction(self.action_edit).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.widgetForAction(self.action_sync).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.widgetForAction(self.action_convert).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.widgetForAction(self.action_save).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.widgetForAction(self.action_add).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.widgetForAction(self.action_view).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.widgetForAction(self.action_del).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.widgetForAction(self.action_preferences).\
|
|
||||||
setPopupMode(QToolButton.MenuButtonPopup)
|
|
||||||
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
|
|
||||||
|
|
||||||
self.connect(self.preferences_action, SIGNAL('triggered(bool)'),
|
|
||||||
self.do_config)
|
|
||||||
self.connect(self.action_preferences, SIGNAL('triggered(bool)'),
|
|
||||||
self.do_config)
|
|
||||||
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
|
|
||||||
self.do_advanced_search)
|
|
||||||
|
|
||||||
for ch in self.tool_bar.children():
|
|
||||||
if isinstance(ch, QToolButton):
|
|
||||||
ch.setCursor(Qt.PointingHandCursor)
|
|
||||||
|
|
||||||
####################### Library view ########################
|
####################### Library view ########################
|
||||||
similar_menu = QMenu(_('Similar books...'))
|
LibraryViewMixin.__init__(self, db)
|
||||||
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.connect(self.action_books_by_same_author, SIGNAL('triggered()'),
|
|
||||||
lambda : self.show_similar_books('author'))
|
|
||||||
self.connect(self.action_books_in_this_series, SIGNAL('triggered()'),
|
|
||||||
lambda : self.show_similar_books('series'))
|
|
||||||
self.connect(self.action_books_with_the_same_tags,
|
|
||||||
SIGNAL('triggered()'),
|
|
||||||
lambda : self.show_similar_books('tag'))
|
|
||||||
self.connect(self.action_books_by_this_publisher, SIGNAL('triggered()'),
|
|
||||||
lambda : 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.status_bar.book_info.show_data,)),
|
|
||||||
('connect_to_restriction_set',
|
|
||||||
(self.tags_view,)),
|
|
||||||
]:
|
|
||||||
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.show()
|
self.show()
|
||||||
|
|
||||||
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||||
self.hide_windows()
|
self.hide_windows()
|
||||||
self.stack.setCurrentIndex(0)
|
|
||||||
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
|
|
||||||
self.search.setFocus(Qt.OtherFocusReason)
|
|
||||||
self.cover_cache = CoverCache(self.library_path)
|
self.cover_cache = CoverCache(self.library_path)
|
||||||
self.cover_cache.start()
|
self.cover_cache.start()
|
||||||
self.library_view.model().cover_cache = self.cover_cache
|
self.library_view.model().cover_cache = self.cover_cache
|
||||||
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
|
self.library_view.model().count_changed_signal.connect \
|
||||||
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
|
(self.location_view.count_changed)
|
||||||
self.tags_view.tags_marked.connect(self.search.search_from_tags)
|
|
||||||
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
|
|
||||||
self.tags_view.restriction_set.connect(x)
|
|
||||||
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.search.search.connect(self.tags_view.model().reinit)
|
|
||||||
for x in (self.location_view.count_changed, self.tags_view.recount,
|
|
||||||
self.restriction_count_changed):
|
|
||||||
self.library_view.model().count_changed_signal.connect(x)
|
|
||||||
|
|
||||||
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
|
||||||
self.connect(self.saved_search, SIGNAL('changed()'),
|
|
||||||
self.tags_view.saved_searches_changed, Qt.QueuedConnection)
|
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||||
@ -582,48 +297,18 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
|
|
||||||
########################### Tags Browser ##############################
|
########################### Tags Browser ##############################
|
||||||
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
|
TagBrowserMixin.__init__(self, db)
|
||||||
self.search_restriction.setMinimumContentsLength(10)
|
|
||||||
|
|
||||||
|
######################### Search Restriction ##########################
|
||||||
|
SearchRestrictionMixin.__init__(self)
|
||||||
|
|
||||||
########################### Cover Flow ################################
|
########################### Cover Flow ################################
|
||||||
self.cover_flow = None
|
|
||||||
if CoverFlow is not None:
|
CoverFlowMixin.__init__(self)
|
||||||
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
|
|
||||||
text_height = 40 if config['separate_cover_flow'] else 25
|
|
||||||
ah = available_height()
|
|
||||||
cfh = ah-100
|
|
||||||
cfh = 3./5 * cfh - text_height
|
|
||||||
if not config['separate_cover_flow']:
|
|
||||||
cfh = 220 if ah > 950 else 170 if ah > 850 else 140
|
|
||||||
self.cover_flow = CoverFlow(height=cfh, text_height=text_height)
|
|
||||||
self.cover_flow.setVisible(False)
|
|
||||||
if not config['separate_cover_flow']:
|
|
||||||
self.library.layout().addWidget(self.cover_flow)
|
|
||||||
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)
|
|
||||||
|
|
||||||
self._calculated_available_height = min(max_available_height()-15,
|
self._calculated_available_height = min(max_available_height()-15,
|
||||||
self.height())
|
self.height())
|
||||||
self.resize(self.width(), self._calculated_available_height)
|
self.resize(self.width(), self._calculated_available_height)
|
||||||
self.search.setMaximumWidth(self.width()-150)
|
|
||||||
|
|
||||||
####################### Side Bar ###############################
|
|
||||||
|
|
||||||
self.sidebar.initialize(self.jobs_dialog, self.cover_flow,
|
|
||||||
self.toggle_cover_flow, pictureflowerror,
|
|
||||||
self.vertical_splitter, self.horizontal_splitter)
|
|
||||||
QObject.connect(self.job_manager, SIGNAL('job_added(int)'),
|
|
||||||
self.sidebar.job_added, Qt.QueuedConnection)
|
|
||||||
QObject.connect(self.job_manager, SIGNAL('job_done(int)'),
|
|
||||||
self.sidebar.job_done, Qt.QueuedConnection)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if config['autolaunch_server']:
|
if config['autolaunch_server']:
|
||||||
@ -644,47 +329,14 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.connect(self.scheduler,
|
self.connect(self.scheduler,
|
||||||
SIGNAL('start_recipe_fetch(PyQt_PyObject)'),
|
SIGNAL('start_recipe_fetch(PyQt_PyObject)'),
|
||||||
self.download_scheduled_recipe, Qt.QueuedConnection)
|
self.download_scheduled_recipe, Qt.QueuedConnection)
|
||||||
self.library_view.verticalHeader().sectionClicked.connect(self.view_specific_book)
|
|
||||||
|
|
||||||
for view in ('library', 'memory', 'card_a', 'card_b'):
|
|
||||||
view = getattr(self, view+'_view')
|
|
||||||
view.verticalHeader().sectionDoubleClicked.connect(self.view_specific_book)
|
|
||||||
|
|
||||||
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
||||||
|
|
||||||
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
|
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
|
||||||
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
def do_user_categories_edit(self, on_category=None):
|
self.read_settings()
|
||||||
d = TagCategories(self, self.library_view.model().db, on_category)
|
self.finalize_layout()
|
||||||
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()
|
|
||||||
|
|
||||||
def do_saved_search_edit(self, search):
|
|
||||||
d = SavedSearchEditor(self, search)
|
|
||||||
d.exec_()
|
|
||||||
if d.result() == d.Accepted:
|
|
||||||
self.tags_view.saved_searches_changed(recount=True)
|
|
||||||
self.saved_search.clear_to_help()
|
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
MainWindow.resizeEvent(self, ev)
|
MainWindow.resizeEvent(self, ev)
|
||||||
@ -767,162 +419,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
error_dialog(self, _('Failed to start content server'),
|
error_dialog(self, _('Failed to start content server'),
|
||||||
unicode(self.content_server.exception)).exec_()
|
unicode(self.content_server.exception)).exec_()
|
||||||
|
|
||||||
def show_similar_books(self, type):
|
|
||||||
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 == 'author':
|
|
||||||
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 toggle_cover_flow(self, show):
|
|
||||||
if config['separate_cover_flow']:
|
|
||||||
if show:
|
|
||||||
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
|
|
||||||
d = QDialog(self)
|
|
||||||
ah, aw = available_height(), available_width()
|
|
||||||
d.resize(int(aw/2.), 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)
|
|
||||||
self.library_view.scrollTo(self.library_view.currentIndex())
|
|
||||||
d.show()
|
|
||||||
d.finished.connect(self.sidebar.external_cover_flow_finished)
|
|
||||||
self.cf_dialog = d
|
|
||||||
self.cover_flow_sync_timer.start(500)
|
|
||||||
else:
|
|
||||||
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)
|
|
||||||
cfd = getattr(self, 'cf_dialog', None)
|
|
||||||
if cfd is not None:
|
|
||||||
self.cover_flow.setVisible(False)
|
|
||||||
cfd.hide()
|
|
||||||
self.cf_dialog = None
|
|
||||||
else:
|
|
||||||
if show:
|
|
||||||
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
|
|
||||||
self.library_view.setCurrentIndex(
|
|
||||||
self.library_view.currentIndex())
|
|
||||||
self.cover_flow.setVisible(True)
|
|
||||||
self.cover_flow.setFocus(Qt.OtherFocusReason)
|
|
||||||
self.library_view.scrollTo(self.library_view.currentIndex())
|
|
||||||
self.cover_flow_sync_timer.start(500)
|
|
||||||
else:
|
|
||||||
self.cover_flow_sync_timer.stop()
|
|
||||||
self.cover_flow.setVisible(False)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Handling of the count of books in a restricted view requires that
|
|
||||||
we capture the count after the initial restriction search. To so this,
|
|
||||||
we require that the restriction_set signal be issued before the search signal,
|
|
||||||
so that when the search_done happens and the count is displayed,
|
|
||||||
we can grab the count. This works because the search box is cleared
|
|
||||||
when a restriction is set, so that first search will find all books.
|
|
||||||
|
|
||||||
Adding and deleting books creates another complexity. When added, they are
|
|
||||||
displayed regardless of whether they match the 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(compute_count=False)
|
|
||||||
|
|
||||||
def mark_restriction_set(self, r):
|
|
||||||
self.restriction_in_effect = False if r is None or not r else True
|
|
||||||
|
|
||||||
def set_number_of_books_shown(self, compute_count):
|
|
||||||
if self.current_view() == self.library_view and self.restriction_in_effect:
|
|
||||||
if compute_count:
|
|
||||||
self.restriction_count_of_books_in_view = self.current_view().row_count()
|
|
||||||
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)
|
|
||||||
|
|
||||||
def search_box_cleared(self):
|
|
||||||
self.set_number_of_books_shown(compute_count=True)
|
|
||||||
self.tags_view.clear()
|
|
||||||
self.saved_search.clear_to_help()
|
|
||||||
|
|
||||||
def search_clear(self):
|
|
||||||
self.set_number_of_books_shown(compute_count=True)
|
|
||||||
self.search.clear()
|
|
||||||
|
|
||||||
def search_done(self, view, ok):
|
|
||||||
if view is self.current_view():
|
|
||||||
self.search.search_done(ok)
|
|
||||||
self.set_number_of_books_shown(compute_count=False)
|
|
||||||
|
|
||||||
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
|
|
||||||
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 another_instance_wants_to_talk(self):
|
def another_instance_wants_to_talk(self):
|
||||||
try:
|
try:
|
||||||
@ -961,8 +457,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
def booklists(self):
|
def booklists(self):
|
||||||
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
########################## Connect to device ##############################
|
########################## Connect to device ##############################
|
||||||
|
|
||||||
def save_device_view_settings(self):
|
def save_device_view_settings(self):
|
||||||
@ -988,7 +482,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
Dispatcher(self.info_read))
|
Dispatcher(self.info_read))
|
||||||
self.set_default_thumbnail(\
|
self.set_default_thumbnail(\
|
||||||
self.device_manager.device.THUMBNAIL_HEIGHT)
|
self.device_manager.device.THUMBNAIL_HEIGHT)
|
||||||
self.status_bar.showMessage(_('Device: ')+\
|
self.status_bar.show_message(_('Device: ')+\
|
||||||
self.device_manager.device.__class__.get_gui_name()+\
|
self.device_manager.device.__class__.get_gui_name()+\
|
||||||
_(' detected.'), 3000)
|
_(' detected.'), 3000)
|
||||||
self.device_connected = 'device' if not is_folder_device else 'folder'
|
self.device_connected = 'device' if not is_folder_device else 'folder'
|
||||||
@ -1009,7 +503,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
dict(version=self.latest_version, device=' '))
|
dict(version=self.latest_version, device=' '))
|
||||||
self.device_info = ' '
|
self.device_info = ' '
|
||||||
if self.current_view() != self.library_view:
|
if self.current_view() != self.library_view:
|
||||||
self.status_bar.reset_info()
|
self.book_details.reset_info()
|
||||||
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
||||||
self.eject_action.setEnabled(False)
|
self.eject_action.setEnabled(False)
|
||||||
self.refresh_ondevice_info (device_connected = False)
|
self.refresh_ondevice_info (device_connected = False)
|
||||||
@ -1300,21 +794,21 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
Dispatcher(self._files_added), spare_server=self.spare_server)
|
Dispatcher(self._files_added), spare_server=self.spare_server)
|
||||||
self._adder.add_recursive(root, single)
|
self._adder.add_recursive(root, single)
|
||||||
|
|
||||||
def add_recursive_single(self, checked):
|
def add_recursive_single(self, *args):
|
||||||
'''
|
'''
|
||||||
Add books from the local filesystem to either the library or the device
|
Add books from the local filesystem to either the library or the device
|
||||||
recursively assuming one book per folder.
|
recursively assuming one book per folder.
|
||||||
'''
|
'''
|
||||||
self.add_recursive(True)
|
self.add_recursive(True)
|
||||||
|
|
||||||
def add_recursive_multiple(self, checked):
|
def add_recursive_multiple(self, *args):
|
||||||
'''
|
'''
|
||||||
Add books from the local filesystem to either the library or the device
|
Add books from the local filesystem to either the library or the device
|
||||||
recursively assuming multiple books per folder.
|
recursively assuming multiple books per folder.
|
||||||
'''
|
'''
|
||||||
self.add_recursive(False)
|
self.add_recursive(False)
|
||||||
|
|
||||||
def add_empty(self, checked):
|
def add_empty(self, *args):
|
||||||
'''
|
'''
|
||||||
Add an empty book item to the library. This does not import any formats
|
Add an empty book item to the library. This does not import any formats
|
||||||
from a book file.
|
from a book file.
|
||||||
@ -1367,14 +861,14 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
to_device = allow_device and self.stack.currentIndex() != 0
|
to_device = allow_device and self.stack.currentIndex() != 0
|
||||||
self._add_books(books, to_device)
|
self._add_books(books, to_device)
|
||||||
if to_device:
|
if to_device:
|
||||||
self.status_bar.showMessage(\
|
self.status_bar.show_message(\
|
||||||
_('Uploading books to device.'), 2000)
|
_('Uploading books to device.'), 2000)
|
||||||
|
|
||||||
|
|
||||||
def add_filesystem_book(self, paths, allow_device=True):
|
def add_filesystem_book(self, paths, allow_device=True):
|
||||||
self._add_filesystem_book(paths, allow_device=allow_device)
|
self._add_filesystem_book(paths, allow_device=allow_device)
|
||||||
|
|
||||||
def add_books(self, checked):
|
def add_books(self, *args):
|
||||||
'''
|
'''
|
||||||
Add books from the local filesystem to either the library or the device.
|
Add books from the local filesystem to either the library or the device.
|
||||||
'''
|
'''
|
||||||
@ -1418,7 +912,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.upload_books(paths,
|
self.upload_books(paths,
|
||||||
list(map(ascii_filename, names)),
|
list(map(ascii_filename, names)),
|
||||||
infos, on_card=on_card)
|
infos, on_card=on_card)
|
||||||
self.status_bar.showMessage(
|
self.status_bar.show_message(
|
||||||
_('Uploading books to device.'), 2000)
|
_('Uploading books to device.'), 2000)
|
||||||
if getattr(self._adder, 'number_of_books_added', 0) > 0:
|
if getattr(self._adder, 'number_of_books_added', 0) > 0:
|
||||||
self.library_view.model().books_added(self._adder.number_of_books_added)
|
self.library_view.model().books_added(self._adder.number_of_books_added)
|
||||||
@ -1537,7 +1031,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
row = None
|
row = None
|
||||||
if ci.isValid():
|
if ci.isValid():
|
||||||
row = ci.row()
|
row = ci.row()
|
||||||
view.model().delete_books(rows)
|
ids_deleted = view.model().delete_books(rows)
|
||||||
|
for v in (self.memory_view, self.card_a_view, self.card_b_view):
|
||||||
|
if v is None:
|
||||||
|
continue
|
||||||
|
v.model().clear_ondevice(ids_deleted)
|
||||||
if row is not None:
|
if row is not None:
|
||||||
ci = view.model().index(row, 0)
|
ci = view.model().index(row, 0)
|
||||||
if ci.isValid():
|
if ci.isValid():
|
||||||
@ -1548,7 +1046,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
if not confirm('<p>'+_('The selected books will be '
|
if not confirm('<p>'+_('The selected books will be '
|
||||||
'<b>permanently deleted</b> '
|
'<b>permanently deleted</b> '
|
||||||
'from your device. Are you sure?')
|
'from your device. Are you sure?')
|
||||||
+'</p>', 'library_delete_books', self):
|
+'</p>', 'device_delete_books', self):
|
||||||
return
|
return
|
||||||
if self.stack.currentIndex() == 1:
|
if self.stack.currentIndex() == 1:
|
||||||
view = self.memory_view
|
view = self.memory_view
|
||||||
@ -1560,7 +1058,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
job = self.remove_paths(paths)
|
job = self.remove_paths(paths)
|
||||||
self.delete_memory[job] = (paths, view.model())
|
self.delete_memory[job] = (paths, view.model())
|
||||||
view.model().mark_for_deletion(job, rows)
|
view.model().mark_for_deletion(job, rows)
|
||||||
self.status_bar.showMessage(_('Deleting books from device.'), 1000)
|
self.status_bar.show_message(_('Deleting books from device.'), 1000)
|
||||||
|
|
||||||
def remove_paths(self, paths):
|
def remove_paths(self, paths):
|
||||||
return self.device_manager.delete_books(\
|
return self.device_manager.delete_books(\
|
||||||
@ -1582,6 +1080,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.booklists())
|
self.booklists())
|
||||||
model.paths_deleted(paths)
|
model.paths_deleted(paths)
|
||||||
self.upload_booklists()
|
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()
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
@ -1921,7 +1424,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
job.catalog_file_path = out
|
job.catalog_file_path = out
|
||||||
job.fmt = fmt
|
job.fmt = fmt
|
||||||
job.catalog_sync, job.catalog_title = sync, title
|
job.catalog_sync, job.catalog_title = sync, title
|
||||||
self.status_bar.showMessage(_('Generating %s catalog...')%fmt)
|
self.status_bar.show_message(_('Generating %s catalog...')%fmt)
|
||||||
|
|
||||||
def catalog_generated(self, job):
|
def catalog_generated(self, job):
|
||||||
if job.result:
|
if job.result:
|
||||||
@ -1937,7 +1440,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
sync = dynamic.get('catalogs_to_be_synced', set([]))
|
sync = dynamic.get('catalogs_to_be_synced', set([]))
|
||||||
sync.add(id)
|
sync.add(id)
|
||||||
dynamic.set('catalogs_to_be_synced', sync)
|
dynamic.set('catalogs_to_be_synced', sync)
|
||||||
self.status_bar.showMessage(_('Catalog generated.'), 3000)
|
self.status_bar.show_message(_('Catalog generated.'), 3000)
|
||||||
self.sync_catalogs()
|
self.sync_catalogs()
|
||||||
if job.fmt not in ['EPUB','MOBI']:
|
if job.fmt not in ['EPUB','MOBI']:
|
||||||
export_dir = choose_dir(self, _('Export Catalog Directory'),
|
export_dir = choose_dir(self, _('Export Catalog Directory'),
|
||||||
@ -1955,7 +1458,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
Dispatcher(self.scheduled_recipe_fetched), func, args=args,
|
Dispatcher(self.scheduled_recipe_fetched), func, args=args,
|
||||||
description=desc)
|
description=desc)
|
||||||
self.conversion_jobs[job] = (temp_files, fmt, arg)
|
self.conversion_jobs[job] = (temp_files, fmt, arg)
|
||||||
self.status_bar.showMessage(_('Fetching news from ')+arg['title'], 2000)
|
self.status_bar.show_message(_('Fetching news from ')+arg['title'], 2000)
|
||||||
|
|
||||||
def scheduled_recipe_fetched(self, job):
|
def scheduled_recipe_fetched(self, job):
|
||||||
temp_files, fmt, arg = self.conversion_jobs.pop(job)
|
temp_files, fmt, arg = self.conversion_jobs.pop(job)
|
||||||
@ -1969,7 +1472,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
sync.add(id)
|
sync.add(id)
|
||||||
dynamic.set('news_to_be_synced', sync)
|
dynamic.set('news_to_be_synced', sync)
|
||||||
self.scheduler.recipe_downloaded(arg)
|
self.scheduler.recipe_downloaded(arg)
|
||||||
self.status_bar.showMessage(arg['title'] + _(' fetched.'), 3000)
|
self.status_bar.show_message(arg['title'] + _(' fetched.'), 3000)
|
||||||
self.email_news(id)
|
self.email_news(id)
|
||||||
self.sync_news()
|
self.sync_news()
|
||||||
|
|
||||||
@ -2049,7 +1552,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
num = len(jobs)
|
num = len(jobs)
|
||||||
|
|
||||||
if num > 0:
|
if num > 0:
|
||||||
self.status_bar.showMessage(_('Starting conversion of %d book(s)') %
|
self.status_bar.show_message(_('Starting conversion of %d book(s)') %
|
||||||
num, 2000)
|
num, 2000)
|
||||||
|
|
||||||
def queue_convert_jobs(self, jobs, changed, bad, rows, previous,
|
def queue_convert_jobs(self, jobs, changed, bad, rows, previous,
|
||||||
@ -2096,7 +1599,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.library_view.model().db.add_format(book_id, \
|
self.library_view.model().db.add_format(book_id, \
|
||||||
fmt, data, index_is_id=True)
|
fmt, data, index_is_id=True)
|
||||||
data.close()
|
data.close()
|
||||||
self.status_bar.showMessage(job.description + \
|
self.status_bar.show_message(job.description + \
|
||||||
(' completed'), 2000)
|
(' completed'), 2000)
|
||||||
finally:
|
finally:
|
||||||
for f in temp_files:
|
for f in temp_files:
|
||||||
@ -2252,13 +1755,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
########################### Do advanced search #############################
|
|
||||||
|
|
||||||
def do_advanced_search(self, *args):
|
|
||||||
d = SearchDialog(self)
|
|
||||||
if d.exec_() == QDialog.Accepted:
|
|
||||||
self.search.set_search_string(d.search_string())
|
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
############################### Do config ##################################
|
############################### Do config ##################################
|
||||||
@ -2280,12 +1776,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
d.exec_()
|
d.exec_()
|
||||||
self.content_server = d.server
|
self.content_server = d.server
|
||||||
if d.result() == d.Accepted:
|
if d.result() == d.Accepted:
|
||||||
self.tool_bar.setIconSize(config['toolbar_icon_size'])
|
self.read_toolbar_settings()
|
||||||
self.search.search_as_you_type(config['search_as_you_type'])
|
self.search.search_as_you_type(config['search_as_you_type'])
|
||||||
self.tool_bar.setToolButtonStyle(
|
|
||||||
Qt.ToolButtonTextUnderIcon if \
|
|
||||||
config['show_text_in_toolbar'] else \
|
|
||||||
Qt.ToolButtonIconOnly)
|
|
||||||
self.save_menu.actions()[2].setText(
|
self.save_menu.actions()[2].setText(
|
||||||
_('Save only %s format to disk')%
|
_('Save only %s format to disk')%
|
||||||
prefs['output_format'].upper())
|
prefs['output_format'].upper())
|
||||||
@ -2304,14 +1796,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
def library_moved(self, newloc):
|
def library_moved(self, newloc):
|
||||||
if newloc is None: return
|
if newloc is None: return
|
||||||
db = LibraryDatabase2(newloc)
|
db = LibraryDatabase2(newloc)
|
||||||
|
self.library_path = newloc
|
||||||
self.book_on_device(None, reset=True)
|
self.book_on_device(None, reset=True)
|
||||||
db.set_book_on_device_func(self.book_on_device)
|
db.set_book_on_device_func(self.book_on_device)
|
||||||
self.library_view.set_database(db)
|
self.library_view.set_database(db)
|
||||||
|
self.tags_view.set_database(db, self.tag_match, self.popularity)
|
||||||
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
||||||
self.status_bar.clearMessage()
|
self.status_bar.clear_message()
|
||||||
self.search.clear_to_help()
|
self.search.clear_to_help()
|
||||||
self.status_bar.reset_info()
|
self.book_details.reset_info()
|
||||||
self.library_view.model().count_changed()
|
self.library_view.model().count_changed()
|
||||||
|
prefs['library_path'] = self.library_path
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
@ -2336,8 +1831,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
'''
|
'''
|
||||||
page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
|
page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
|
||||||
self.stack.setCurrentIndex(page)
|
self.stack.setCurrentIndex(page)
|
||||||
self.status_bar.reset_info()
|
self.book_details.reset_info()
|
||||||
self.sidebar.location_changed(location)
|
for x in ('tb', 'cb'):
|
||||||
|
splitter = getattr(self, x+'_splitter')
|
||||||
|
splitter.button.setEnabled(location == 'library')
|
||||||
if location == 'library':
|
if location == 'library':
|
||||||
self.action_edit.setEnabled(True)
|
self.action_edit.setEnabled(True)
|
||||||
self.action_merge.setEnabled(True)
|
self.action_merge.setEnabled(True)
|
||||||
@ -2358,7 +1855,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.search_restriction.setEnabled(False)
|
self.search_restriction.setEnabled(False)
|
||||||
for action in list(self.delete_menu.actions())[1:]:
|
for action in list(self.delete_menu.actions())[1:]:
|
||||||
action.setEnabled(False)
|
action.setEnabled(False)
|
||||||
self.set_number_of_books_shown(compute_count=False)
|
self.set_number_of_books_shown()
|
||||||
|
|
||||||
|
|
||||||
def device_job_exception(self, job):
|
def device_job_exception(self, job):
|
||||||
@ -2439,20 +1936,13 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
geometry = config['main_window_geometry']
|
geometry = config['main_window_geometry']
|
||||||
if geometry is not None:
|
if geometry is not None:
|
||||||
self.restoreGeometry(geometry)
|
self.restoreGeometry(geometry)
|
||||||
self.tool_bar.setIconSize(config['toolbar_icon_size'])
|
self.read_toolbar_settings()
|
||||||
self.tool_bar.setToolButtonStyle(
|
self.read_layout_settings()
|
||||||
Qt.ToolButtonTextUnderIcon if \
|
|
||||||
config['show_text_in_toolbar'] else \
|
|
||||||
Qt.ToolButtonIconOnly)
|
|
||||||
|
|
||||||
|
|
||||||
def write_settings(self):
|
def write_settings(self):
|
||||||
config.set('main_window_geometry', self.saveGeometry())
|
config.set('main_window_geometry', self.saveGeometry())
|
||||||
dynamic.set('sort_history', self.library_view.model().sort_history)
|
dynamic.set('sort_history', self.library_view.model().sort_history)
|
||||||
self.sidebar.save_state()
|
self.save_layout_state()
|
||||||
for view in ('library_view', 'memory_view', 'card_a_view',
|
|
||||||
'card_b_view'):
|
|
||||||
getattr(self, view).save_state()
|
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
self.quit(restart=True)
|
self.quit(restart=True)
|
||||||
|
@ -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)'),
|
||||||
@ -424,7 +424,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.set_bookmarks(self.iterator.bookmarks)
|
self.set_bookmarks(self.iterator.bookmarks)
|
||||||
|
|
||||||
|
|
||||||
def find(self, text, refinement, repeat=False, backwards=False):
|
def find(self, text, repeat=False, backwards=False):
|
||||||
if not text:
|
if not text:
|
||||||
self.view.search('')
|
self.view.search('')
|
||||||
return self.search.search_done(False)
|
return self.search.search_done(False)
|
||||||
|
@ -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,115 @@ 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:
|
||||||
|
print self.save_name, 'side:', self.side_index_size, 'other:',
|
||||||
|
print list(self.sizes())[self.other_index]
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def side_index_size(self):
|
||||||
|
def fget(self):
|
||||||
|
if self.count() < 2: return 0
|
||||||
|
return self.sizes()[self.side_index]
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
if self.count() < 2: return
|
||||||
|
if val == 0 and not self.is_side_index_hidden:
|
||||||
|
self.save_state()
|
||||||
sizes = list(self.sizes())
|
sizes = list(self.sizes())
|
||||||
if 0 in sizes:
|
for i in range(len(sizes)):
|
||||||
idx = sizes.index(0)
|
sizes[i] = val if i == self.side_index else 10
|
||||||
sizes[idx] = 80
|
|
||||||
else:
|
|
||||||
sizes[self.side_index] = 0
|
|
||||||
|
|
||||||
if visible:
|
|
||||||
dynamic.set(self._name + '_last_open_state', str(self.saveState()))
|
|
||||||
self.setSizes(sizes)
|
self.setSizes(sizes)
|
||||||
else:
|
total = sum(self.sizes())
|
||||||
state = dynamic.get(self._name+ '_last_open_state', None)
|
sizes = list(self.sizes())
|
||||||
if state is not None:
|
for i in range(len(sizes)):
|
||||||
self.restoreState(state)
|
sizes[i] = val if i == self.side_index else total-val
|
||||||
else:
|
|
||||||
self.setSizes(sizes)
|
self.setSizes(sizes)
|
||||||
self.initialize()
|
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:
|
||||||
|
self.side_index_size = 0
|
||||||
|
self.desired_show = state[0]
|
||||||
|
|
||||||
|
def default_state(self):
|
||||||
|
return (self.initial_show, self.initial_side_size)
|
||||||
|
|
||||||
|
# Public API {{{
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
if self.count() > 1:
|
||||||
|
gprefs[self.save_name+'_state'] = self.get_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def other_index(self):
|
||||||
|
return (self.side_index+1)%2
|
||||||
|
|
||||||
|
def restore_state(self):
|
||||||
|
if self.count() > 1:
|
||||||
|
state = gprefs.get(self.save_name+'_state',
|
||||||
|
self.default_state())
|
||||||
|
self.apply_state(state, save_desired=False)
|
||||||
|
self.desired_side_size = state[1]
|
||||||
|
|
||||||
|
def toggle_side_pane(self, hide=None):
|
||||||
|
if hide is None:
|
||||||
|
action = 'show' if self.is_side_index_hidden else 'hide'
|
||||||
|
else:
|
||||||
|
action = 'hide' if hide else 'show'
|
||||||
|
getattr(self, action+'_side_pane')()
|
||||||
|
|
||||||
|
def show_side_pane(self):
|
||||||
|
if self.count() < 2 or not self.is_side_index_hidden:
|
||||||
|
return
|
||||||
|
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'
|
||||||
|
|
||||||
|
@ -241,6 +241,24 @@ class ResultCache(SearchQueryParser):
|
|||||||
matches = set([])
|
matches = set([])
|
||||||
if len(query) < 2:
|
if len(query) < 2:
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
if location == 'date':
|
||||||
|
location = 'timestamp'
|
||||||
|
loc = self.field_metadata[location]['rec_index']
|
||||||
|
|
||||||
|
if query == 'false':
|
||||||
|
for item in self._data:
|
||||||
|
if item is None: continue
|
||||||
|
if item[loc] is None or item[loc] == UNDEFINED_DATE:
|
||||||
|
matches.add(item[0])
|
||||||
|
return matches
|
||||||
|
if query == 'true':
|
||||||
|
for item in self._data:
|
||||||
|
if item is None: continue
|
||||||
|
if item[loc] is not None and item[loc] != UNDEFINED_DATE:
|
||||||
|
matches.add(item[0])
|
||||||
|
return matches
|
||||||
|
|
||||||
relop = None
|
relop = None
|
||||||
for k in self.date_search_relops.keys():
|
for k in self.date_search_relops.keys():
|
||||||
if query.startswith(k):
|
if query.startswith(k):
|
||||||
@ -249,10 +267,6 @@ class ResultCache(SearchQueryParser):
|
|||||||
if relop is None:
|
if relop is None:
|
||||||
(p, relop) = self.date_search_relops['=']
|
(p, relop) = self.date_search_relops['=']
|
||||||
|
|
||||||
if location == 'date':
|
|
||||||
location = 'timestamp'
|
|
||||||
loc = self.field_metadata[location]['rec_index']
|
|
||||||
|
|
||||||
if query == _('today'):
|
if query == _('today'):
|
||||||
qd = now()
|
qd = now()
|
||||||
field_count = 3
|
field_count = 3
|
||||||
@ -301,7 +315,7 @@ class ResultCache(SearchQueryParser):
|
|||||||
if query == 'false':
|
if query == 'false':
|
||||||
query = '0'
|
query = '0'
|
||||||
elif query == 'true':
|
elif query == 'true':
|
||||||
query = '>0'
|
query = '!=0'
|
||||||
relop = None
|
relop = None
|
||||||
for k in self.numeric_search_relops.keys():
|
for k in self.numeric_search_relops.keys():
|
||||||
if query.startswith(k):
|
if query.startswith(k):
|
||||||
@ -605,9 +619,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)
|
||||||
|
@ -183,14 +183,29 @@ class CustomColumns(object):
|
|||||||
ans = self.conn.get('SELECT id, value FROM %s'%table)
|
ans = self.conn.get('SELECT id, value FROM %s'%table)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def rename_custom_item(self, id, new_name, label=None, num=None):
|
def rename_custom_item(self, old_id, new_name, label=None, num=None):
|
||||||
if id:
|
|
||||||
if label is not None:
|
if label is not None:
|
||||||
data = self.custom_column_label_map[label]
|
data = self.custom_column_label_map[label]
|
||||||
if num is not None:
|
if num is not None:
|
||||||
data = self.custom_column_num_map[num]
|
data = self.custom_column_num_map[num]
|
||||||
table,lt = self.custom_table_names(data['num'])
|
table,lt = self.custom_table_names(data['num'])
|
||||||
self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, id))
|
# check if item exists
|
||||||
|
new_id = self.conn.get(
|
||||||
|
'SELECT id FROM %s WHERE value=?'%table, (new_name,), all=False)
|
||||||
|
if new_id is None or old_id == new_id:
|
||||||
|
self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, old_id))
|
||||||
|
else:
|
||||||
|
# New id exists. If the column is_multiple, then process like
|
||||||
|
# tags, otherwise process like publishers (see database2)
|
||||||
|
if data['is_multiple']:
|
||||||
|
books = self.conn.get('''SELECT book from %s
|
||||||
|
WHERE value=?'''%lt, (old_id,))
|
||||||
|
for (book_id,) in books:
|
||||||
|
self.conn.execute('''DELETE FROM %s
|
||||||
|
WHERE book=? and value=?'''%lt, (book_id, new_id))
|
||||||
|
self.conn.execute('''UPDATE %s SET value=?
|
||||||
|
WHERE value=?'''%lt, (new_id, old_id,))
|
||||||
|
self.conn.execute('DELETE FROM %s WHERE id=?'%table, (old_id,))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def delete_custom_item_using_id(self, id, label=None, num=None):
|
def delete_custom_item_using_id(self, id, label=None, num=None):
|
||||||
|
@ -9,12 +9,6 @@ The database used to store ebook metadata
|
|||||||
import os, sys, shutil, cStringIO, glob,functools, traceback
|
import os, sys, shutil, cStringIO, glob,functools, traceback
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from math import floor
|
from math import floor
|
||||||
try:
|
|
||||||
from PIL import Image as PILImage
|
|
||||||
PILImage
|
|
||||||
except ImportError:
|
|
||||||
import Image as PILImage
|
|
||||||
|
|
||||||
|
|
||||||
from PyQt4.QtGui import QImage
|
from PyQt4.QtGui import QImage
|
||||||
|
|
||||||
@ -34,10 +28,10 @@ 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
|
||||||
|
|
||||||
if iswindows:
|
if iswindows:
|
||||||
import calibre.utils.winshell as winshell
|
import calibre.utils.winshell as winshell
|
||||||
@ -475,11 +469,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if callable(getattr(data, 'save', None)):
|
if callable(getattr(data, 'save', None)):
|
||||||
data.save(path)
|
data.save(path)
|
||||||
else:
|
else:
|
||||||
f = data
|
if callable(getattr(data, 'read', None)):
|
||||||
if not callable(getattr(data, 'read', None)):
|
data = data.read()
|
||||||
f = cStringIO.StringIO(data)
|
save_cover_data_to(data, path)
|
||||||
im = PILImage.open(f)
|
|
||||||
im.convert('RGB').save(path, 'JPEG')
|
|
||||||
|
|
||||||
def book_on_device(self, id):
|
def book_on_device(self, id):
|
||||||
if callable(self.book_on_device_func):
|
if callable(self.book_on_device_func):
|
||||||
@ -744,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
|
||||||
@ -958,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)
|
||||||
|
if tweaks['title_series_sorting'] == 'library_order':
|
||||||
self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True)
|
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:
|
||||||
@ -1007,13 +1006,35 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return []
|
return []
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def rename_tag(self, id, new_name):
|
def rename_tag(self, old_id, new_name):
|
||||||
if id:
|
new_id = self.conn.get(
|
||||||
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id))
|
'''SELECT id from tags
|
||||||
|
WHERE name=?''', (new_name,), all=False)
|
||||||
|
if new_id is None or old_id == new_id:
|
||||||
|
# easy cases. Simply rename the tag. Do it even if equal, in case
|
||||||
|
# there is a change of case
|
||||||
|
self.conn.execute('''UPDATE tags SET name=?
|
||||||
|
WHERE id=?''', (new_name, old_id))
|
||||||
|
else:
|
||||||
|
# It is possible that by renaming a tag, the tag will appear
|
||||||
|
# twice on a book. This will throw an integrity error, aborting
|
||||||
|
# all the changes. To get around this, we first delete any links
|
||||||
|
# to the new_id from books referencing the old_id, so that
|
||||||
|
# renaming old_id to new_id will be unique on the book
|
||||||
|
books = self.conn.get('''SELECT book from books_tags_link
|
||||||
|
WHERE tag=?''', (old_id,))
|
||||||
|
for (book_id,) in books:
|
||||||
|
self.conn.execute('''DELETE FROM books_tags_link
|
||||||
|
WHERE book=? and tag=?''', (book_id, new_id))
|
||||||
|
|
||||||
|
# Change the link table to point at the new tag
|
||||||
|
self.conn.execute('''UPDATE books_tags_link SET tag=?
|
||||||
|
WHERE tag=?''',(new_id, old_id,))
|
||||||
|
# Get rid of the no-longer used publisher
|
||||||
|
self.conn.execute('DELETE FROM tags WHERE id=?', (old_id,))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def delete_tag_using_id(self, id):
|
def delete_tag_using_id(self, id):
|
||||||
if id:
|
|
||||||
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,))
|
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,))
|
||||||
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
|
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
@ -1024,13 +1045,38 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return []
|
return []
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def rename_series(self, id, new_name):
|
def rename_series(self, old_id, new_name):
|
||||||
if id:
|
new_id = self.conn.get(
|
||||||
self.conn.execute('UPDATE series SET name=? WHERE id=?', (new_name, id))
|
'''SELECT id from series
|
||||||
|
WHERE name=?''', (new_name,), all=False)
|
||||||
|
if new_id is None or old_id == new_id:
|
||||||
|
self.conn.execute('UPDATE series SET name=? WHERE id=?',
|
||||||
|
(new_name, old_id))
|
||||||
|
else:
|
||||||
|
# New series exists. Must update the link, then assign a
|
||||||
|
# new series index to each of the books.
|
||||||
|
|
||||||
|
# Get the list of books where we must update the series index
|
||||||
|
books = self.conn.get('''SELECT books.id
|
||||||
|
FROM books, books_series_link as lt
|
||||||
|
WHERE books.id = lt.book AND lt.series=?
|
||||||
|
ORDER BY books.series_index''', (old_id,))
|
||||||
|
# Get the next series index
|
||||||
|
index = self.get_next_series_num_for(new_name)
|
||||||
|
# Now update the link table
|
||||||
|
self.conn.execute('''UPDATE books_series_link
|
||||||
|
SET series=?
|
||||||
|
WHERE series=?''',(new_id, old_id,))
|
||||||
|
# Now set the indices
|
||||||
|
for (book_id,) in books:
|
||||||
|
self.conn.execute('''UPDATE books
|
||||||
|
SET series_index=?
|
||||||
|
WHERE id=?''',(index, book_id,))
|
||||||
|
index = index + 1
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def delete_series_using_id(self, id):
|
def delete_series_using_id(self, id):
|
||||||
if id:
|
|
||||||
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
|
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
|
||||||
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
|
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
|
||||||
self.conn.execute('DELETE FROM series WHERE id=?', (id,))
|
self.conn.execute('DELETE FROM series WHERE id=?', (id,))
|
||||||
@ -1044,28 +1090,95 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return []
|
return []
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def rename_publisher(self, id, new_name):
|
def rename_publisher(self, old_id, new_name):
|
||||||
if id:
|
new_id = self.conn.get(
|
||||||
self.conn.execute('UPDATE publishers SET name=? WHERE id=?', (new_name, id))
|
'''SELECT id from publishers
|
||||||
|
WHERE name=?''', (new_name,), all=False)
|
||||||
|
if new_id is None or old_id == new_id:
|
||||||
|
# New name doesn't exist. Simply change the old name
|
||||||
|
self.conn.execute('UPDATE publishers SET name=? WHERE id=?', \
|
||||||
|
(new_name, old_id))
|
||||||
|
else:
|
||||||
|
# Change the link table to point at the new one
|
||||||
|
self.conn.execute('''UPDATE books_publishers_link
|
||||||
|
SET publisher=?
|
||||||
|
WHERE publisher=?''',(new_id, old_id,))
|
||||||
|
# Get rid of the no-longer used publisher
|
||||||
|
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def delete_publisher_using_id(self, id):
|
def delete_publisher_using_id(self, old_id):
|
||||||
if id:
|
self.conn.execute('''DELETE FROM books_publishers_link
|
||||||
self.conn.execute('DELETE FROM books_publishers_link WHERE publisher=?', (id,))
|
WHERE publisher=?''', (old_id,))
|
||||||
self.conn.execute('DELETE FROM publishers WHERE id=?', (id,))
|
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
# There is no editor for author, so we do not need get_authors_with_ids or
|
# There is no editor for author, so we do not need get_authors_with_ids or
|
||||||
# delete_author_using_id.
|
# delete_author_using_id.
|
||||||
def rename_author(self, id, new_name):
|
|
||||||
if id:
|
def rename_author(self, old_id, new_name):
|
||||||
# Make sure that any commas in new_name are changed to '|'!
|
# Make sure that any commas in new_name are changed to '|'!
|
||||||
new_name = new_name.replace(',', '|')
|
new_name = new_name.replace(',', '|')
|
||||||
self.conn.execute('UPDATE authors SET name=? WHERE id=?', (new_name, id))
|
|
||||||
|
# Get the list of books we must fix up, one way or the other
|
||||||
|
# Save the list so we can use it twice
|
||||||
|
bks = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,))
|
||||||
|
books = []
|
||||||
|
for (book_id,) in bks:
|
||||||
|
books.append(book_id)
|
||||||
|
|
||||||
|
# check if the new author already exists
|
||||||
|
new_id = self.conn.get('SELECT id from authors WHERE name=?',
|
||||||
|
(new_name,), all=False)
|
||||||
|
if new_id is None or old_id == new_id:
|
||||||
|
# No name clash. Go ahead and update the author's name
|
||||||
|
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
|
||||||
|
(new_name, old_id))
|
||||||
|
else:
|
||||||
|
# First check for the degenerate case -- changing a value to itself.
|
||||||
|
# Update it in case there is a change of case, but do nothing else
|
||||||
|
if old_id == new_id:
|
||||||
|
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
|
||||||
|
(new_name, old_id))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
# now must fix up the books
|
return
|
||||||
books = self.conn.get('SELECT book from books_authors_link WHERE author=?', (id,))
|
# Author exists. To fix this, we must replace all the authors
|
||||||
for (book_id,) in books:
|
# instead of replacing the one. Reason: db integrity checks can stop
|
||||||
|
# the rename process, which would leave everything half-done. We
|
||||||
|
# can't do it the same way as tags (delete and add) because author
|
||||||
|
# order is important.
|
||||||
|
|
||||||
|
for book_id in books:
|
||||||
|
# Get the existing list of authors
|
||||||
|
authors = self.conn.get('''
|
||||||
|
SELECT author from books_authors_link
|
||||||
|
WHERE book=?
|
||||||
|
ORDER BY id''',(book_id,))
|
||||||
|
|
||||||
|
# unpack the double-list structure, replacing the old author
|
||||||
|
# with the new one while we are at it
|
||||||
|
for i,aut in enumerate(authors):
|
||||||
|
authors[i] = aut[0] if aut[0] != old_id else new_id
|
||||||
|
# Delete the existing authors list
|
||||||
|
self.conn.execute('''DELETE FROM books_authors_link
|
||||||
|
WHERE book=?''',(book_id,))
|
||||||
|
# Change the authors to the new list
|
||||||
|
for aid in authors:
|
||||||
|
try:
|
||||||
|
self.conn.execute('''
|
||||||
|
INSERT INTO books_authors_link(book, author)
|
||||||
|
VALUES (?,?)''', (book_id, aid))
|
||||||
|
except IntegrityError:
|
||||||
|
# Sometimes books specify the same author twice in their
|
||||||
|
# metadata. Ignore it.
|
||||||
|
pass
|
||||||
|
# Now delete the old author from the DB
|
||||||
|
bks = self.conn.get('SELECT book FROM books_authors_link WHERE author=?', (old_id,))
|
||||||
|
self.conn.execute('DELETE FROM authors WHERE id=?', (old_id,))
|
||||||
|
self.conn.commit()
|
||||||
|
# the authors are now changed, either by changing the author's name
|
||||||
|
# or replacing the author in the list. Now must fix up the books.
|
||||||
|
for book_id in books:
|
||||||
# First, must refresh the cache to see the new authors
|
# First, must refresh the cache to see the new authors
|
||||||
self.data.refresh_ids(self, [book_id])
|
self.data.refresh_ids(self, [book_id])
|
||||||
# now fix the filesystem paths
|
# now fix the filesystem paths
|
||||||
@ -1075,12 +1188,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
SELECT authors.name
|
SELECT authors.name
|
||||||
FROM authors, books_authors_link as bl
|
FROM authors, books_authors_link as bl
|
||||||
WHERE bl.book = ? and bl.author = authors.id
|
WHERE bl.book = ? and bl.author = authors.id
|
||||||
|
ORDER BY bl.id
|
||||||
''' , (book_id,))
|
''' , (book_id,))
|
||||||
# unpack the double-list structure
|
# unpack the double-list structure
|
||||||
for i,aut in enumerate(authors):
|
for i,aut in enumerate(authors):
|
||||||
authors[i] = aut[0]
|
authors[i] = aut[0]
|
||||||
ss = authors_to_sort_string(authors)
|
ss = authors_to_sort_string(authors)
|
||||||
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id))
|
# Change the '|'s to ','
|
||||||
|
ss = ss.replace('|', ',')
|
||||||
|
self.conn.execute('''UPDATE books
|
||||||
|
SET author_sort=?
|
||||||
|
WHERE id=?''', (ss, book_id))
|
||||||
|
self.conn.commit()
|
||||||
|
# the caller will do a general refresh, so we don't need to
|
||||||
|
# do one here
|
||||||
|
|
||||||
# end convenience methods
|
# end convenience methods
|
||||||
|
|
||||||
@ -1183,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])
|
||||||
@ -1721,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))
|
||||||
|
@ -38,6 +38,12 @@ def server_config(defaults=None):
|
|||||||
c.add_opt('max_opds_items', ['--max-opds-items'], default=30,
|
c.add_opt('max_opds_items', ['--max-opds-items'], default=30,
|
||||||
help=_('The maximum number of matches to return per OPDS query. '
|
help=_('The maximum number of matches to return per OPDS query. '
|
||||||
'This affects Stanza, WordPlayer, etc. integration.'))
|
'This affects Stanza, WordPlayer, etc. integration.'))
|
||||||
|
c.add_opt('max_opds_ungrouped_items', ['--max-opds-ungrouped-items'],
|
||||||
|
default=100,
|
||||||
|
help=_('Group items in categories such as author/tags '
|
||||||
|
'by first letter when there are more than this number '
|
||||||
|
'of items. Default: %default. Set to a large number '
|
||||||
|
'to disable grouping.'))
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -445,7 +445,7 @@ class OPDSServer(object):
|
|||||||
|
|
||||||
id_ = 'calibre-category-feed:'+which
|
id_ = 'calibre-category-feed:'+which
|
||||||
|
|
||||||
MAX_ITEMS = 50
|
MAX_ITEMS = self.opts.max_opds_ungrouped_items
|
||||||
|
|
||||||
if len(items) <= MAX_ITEMS:
|
if len(items) <= MAX_ITEMS:
|
||||||
max_items = self.opts.max_opds_items
|
max_items = self.opts.max_opds_items
|
||||||
@ -459,8 +459,6 @@ class OPDSServer(object):
|
|||||||
self.text, self.count = text, count
|
self.text, self.count = text, count
|
||||||
|
|
||||||
starts = set([x.name[0] for x in items])
|
starts = set([x.name[0] for x in items])
|
||||||
if len(starts) > MAX_ITEMS:
|
|
||||||
starts = set([x.name[:2] for x in items])
|
|
||||||
category_groups = OrderedDict()
|
category_groups = OrderedDict()
|
||||||
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
|
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
|
||||||
category_groups[x] = len([y for y in items if
|
category_groups[x] = len([y for y in items if
|
||||||
|
@ -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)
|
||||||
|
if tweaks['title_series_sorting'] == 'library_order':
|
||||||
self.conn.create_function('title_sort', 1, title_sort)
|
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):
|
||||||
|
@ -8,16 +8,33 @@ Customizing |app|
|
|||||||
==================================
|
==================================
|
||||||
|
|
||||||
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
|
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
|
||||||
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn how to
|
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
|
||||||
use *plugins* to customize and control various aspects of |app|'s behavior.
|
first, how to use environment variables and *tweaks* to customize |app|'s behavior and then how to
|
||||||
|
use *plugins* to add funtionality to |app|.
|
||||||
Theer are different kinds of plugins, corresponding to different aspects of |app|. As more and more aspects of |app|
|
|
||||||
are modularized, new plugin types will be added.
|
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
:depth: 2
|
:depth: 2
|
||||||
:local:
|
:local:
|
||||||
|
|
||||||
|
Environment variables
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
* ``CALIBRE_CONFIG_DIRECTORY`` - sets the directory where configuration files are stored/read.
|
||||||
|
* ``CALIBRE_OVERRIDE_DATABASE_PATH`` - allows you to specify the full path to metadata.db. Using this variable you can have metadata.db be in a location other than the library folder. Useful if your library folder is on a networked drive that does not support file locking.
|
||||||
|
* ``CALIBRE_DEVELOP_FROM`` - Used to run from a calibre development environment. See :ref:`develop`.
|
||||||
|
* ``CALIBRE_OVERRIDE_LANG`` - Used to force the language used by the interface (ISO 639 language code)
|
||||||
|
* ``SYSFS_PATH`` - Use if sysfs is mounted somewhere other than /sys
|
||||||
|
* ``http_proxy`` - Used on linux to specify an HTTP proxy
|
||||||
|
|
||||||
|
Tweaks
|
||||||
|
------------
|
||||||
|
|
||||||
|
Tweaks are small changes that you can specify to control various aspects of |app|'s behavior. You specify them by editing the 2tweaks.py file in the config directory.
|
||||||
|
The default tweaks.py file is reproduced below
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../resources/default_tweaks.py
|
||||||
|
|
||||||
|
|
||||||
A Hello World plugin
|
A Hello World plugin
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
@ -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?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
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
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
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:
|
||||||
|
@ -171,8 +171,7 @@ def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
|
|||||||
border_color)
|
border_color)
|
||||||
compose_image(canvas, img, left, top)
|
compose_image(canvas, img, left, top)
|
||||||
p.DestroyMagickWand(img)
|
p.DestroyMagickWand(img)
|
||||||
with open(path_to_image, 'wb') as f:
|
p.MagickWriteImage(canvas,path_to_image)
|
||||||
p.MagickWriteImage(canvas, f)
|
|
||||||
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,
|
||||||
@ -212,6 +211,23 @@ def create_cover_page(top_lines, logo_path, width=590, height=750,
|
|||||||
p.DestroyMagickWand(canvas)
|
p.DestroyMagickWand(canvas)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def save_cover_data_to(data, path, bgcolor='white'):
|
||||||
|
'''
|
||||||
|
Saves image in data to path, in the format specified by the path
|
||||||
|
extension. Composes the image onto a blank cancas so as to
|
||||||
|
properly convert transparent images.
|
||||||
|
'''
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
with p.ImageMagick():
|
||||||
|
img = load_image(path)
|
||||||
|
canvas = create_canvas(p.MagickGetImageWidth(img),
|
||||||
|
p.MagickGetImageHeight(img), bgcolor)
|
||||||
|
compose_image(canvas, img, 0, 0)
|
||||||
|
p.MagickWriteImage(canvas, path)
|
||||||
|
p.DestroyMagickWand(img)
|
||||||
|
p.DestroyMagickWand(canvas)
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
import subprocess
|
import subprocess
|
||||||
with TemporaryFile('.png') as f:
|
with TemporaryFile('.png') as f:
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user