This commit is contained in:
GRiker 2012-07-27 04:57:02 -06:00
commit 5921a12390
67 changed files with 1795 additions and 745 deletions

View File

@ -19,6 +19,50 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.8.62
date: 2012-07-27
new features:
- title: "Book details panel: Allow right clicking on a format to delete it."
- title: "When errors occur in lots of background jobs, add an option to the error message to temporarily suppress subsequent error messages."
tickets: [886904]
- title: "E-book viewer full screen mode: Allow clicking in the left and right page margins to turn pages."
tickets: [1024819]
- title: "Drivers for various Android devices"
tickets: [1028690,1027431]
- title: "Advanced search dialog: When starting on the title/author/etc. tab, restore the previously used search kind as well."
tickets: [1029745]
- title: "When presenting the calibre must be restarted warning after installing a new plugin, add a restart now button so that the user can conveniently restart calibre. Currently only works when going vie Preferences->Plugins->Get new plugins"
bug fixes:
- title: "Fix main window layout state being saved incorrectly if calibre is killed without a proper shutdown"
- title: "Fix boolean and date searching in non english calibre installs."
- title: "Conversion: Ignore invalid chapter detection and level n ToC expressions instead of erroring out"
improved recipes:
- Psychology Today
- The Smithsonian
- The New Republic
- Various updated Polish news sources
- The Sun
- San Francisco Bay Guardian
- AnandTech
- Smashing Magazine
new recipes:
- title: Linux Journal and Conowego.pl
author: fenuks
- title: A list apart and .net magazine
author: Marc Busque
- version: 0.8.61 - version: 0.8.61
date: 2012-07-20 date: 2012-07-20

17
manual/develop.rst Executable file → Normal file
View File

@ -151,25 +151,20 @@ calibre is the directory that contains the src and resources sub-directories. En
The next step is to create a bash script that will set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path of the src directory when running calibre in debug mode. The next step is to create a bash script that will set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path of the src directory when running calibre in debug mode.
Create a plain text file: Create a plain text file::
#!/bin/sh #!/bin/sh
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src" export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
calibre-debug -g calibre-debug -g
Save this file as ``/usr/bin/calibre-develop``, then set its permissions so that it can be run: Save this file as ``/usr/bin/calibre-develop``, then set its permissions so that it can be executed::
chmod +x /usr/bin/calibre-develop chmod +x /usr/bin/calibre-develop
Once you have done this, type Once you have done this, type::
calibre-develop calibre-develop
You should see some diagnostic information in the Terminal window as calibre starts up, and you should see an asterisk after the version number in the GUI window, indicating that you are running from source. You should see some diagnostic information in the Terminal window as calibre
starts up, and you should see an asterisk after the version number in the GUI
That's it! You are now ready to start hacking on the |app| code. For example, open the file :file:`src/calibre/__init__.py` window, indicating that you are running from source.
in your favorite editor and add the line::
print ("Hello, world!")
near the top of the file. Now run the command :command:`calibredb`. The very first line of output should be ``Hello, world!``.
Linux development environment Linux development environment
------------------------------ ------------------------------

View File

@ -21,8 +21,12 @@ class anan(BasicNewsRecipe):
remove_javascript = True remove_javascript = True
encoding = 'utf-8' encoding = 'utf-8'
remove_tags=[dict(name='a', attrs={'style':'width:110px; margin-top:0px;text-align:center;'}), remove_tags=[
dict(name='a', attrs={'style':'width:110px; margin-top:0px; margin-right:20px;text-align:center;'})] dict(name='a', attrs={'style':'width:110px; margin-top:0px;text-align:center;'}),
dict(name='a', attrs={'style':'width:110px; margin-top:0px; margin-right:20px;text-align:center;'}),
{'attrs':{'class':['article_links', 'header', 'body_right']}},
{'id':['crumbs']},
]
feeds = [ ('Anandtech', 'http://www.anandtech.com/rss/')] feeds = [ ('Anandtech', 'http://www.anandtech.com/rss/')]

View File

@ -1,6 +1,6 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
import re import re
class Benchmark_pl(BasicNewsRecipe): class BenchmarkPl(BasicNewsRecipe):
title = u'Benchmark.pl' title = u'Benchmark.pl'
__author__ = 'fenuks' __author__ = 'fenuks'
description = u'benchmark.pl -IT site' description = u'benchmark.pl -IT site'
@ -14,7 +14,7 @@ class Benchmark_pl(BasicNewsRecipe):
preprocess_regexps = [(re.compile(ur'<h3><span style="font-size: small;">&nbsp;Zobacz poprzednie <a href="http://www.benchmark.pl/news/zestawienie/grupa_id/135">Opinie dnia:</a></span>.*</body>', re.DOTALL|re.IGNORECASE), lambda match: '</body>'), (re.compile(ur'Więcej o .*?</ul>', re.DOTALL|re.IGNORECASE), lambda match: '')] preprocess_regexps = [(re.compile(ur'<h3><span style="font-size: small;">&nbsp;Zobacz poprzednie <a href="http://www.benchmark.pl/news/zestawienie/grupa_id/135">Opinie dnia:</a></span>.*</body>', re.DOTALL|re.IGNORECASE), lambda match: '</body>'), (re.compile(ur'Więcej o .*?</ul>', re.DOTALL|re.IGNORECASE), lambda match: '')]
keep_only_tags=[dict(name='div', attrs={'class':['m_zwykly', 'gallery']})] keep_only_tags=[dict(name='div', attrs={'class':['m_zwykly', 'gallery']})]
remove_tags_after=dict(name='div', attrs={'class':'body'}) remove_tags_after=dict(name='div', attrs={'class':'body'})
remove_tags=[dict(name='div', attrs={'class':['kategoria', 'socialize', 'thumb', 'panelOcenaObserwowane', 'categoryNextToSocializeGallery']}), dict(name='table', attrs={'background':'http://www.benchmark.pl/uploads/backend_img/a/fotki_newsy/opinie_dnia/bg.png'}), dict(name='table', attrs={'width':'210', 'cellspacing':'1', 'cellpadding':'4', 'border':'0', 'align':'right'})] remove_tags=[dict(name='div', attrs={'class':['kategoria', 'socialize', 'thumb', 'panelOcenaObserwowane', 'categoryNextToSocializeGallery', 'breadcrumb']}), dict(name='table', attrs={'background':'http://www.benchmark.pl/uploads/backend_img/a/fotki_newsy/opinie_dnia/bg.png'}), dict(name='table', attrs={'width':'210', 'cellspacing':'1', 'cellpadding':'4', 'border':'0', 'align':'right'})]
INDEX= 'http://www.benchmark.pl' INDEX= 'http://www.benchmark.pl'
feeds = [(u'Aktualności', u'http://www.benchmark.pl/rss/aktualnosci-pliki.xml'), feeds = [(u'Aktualności', u'http://www.benchmark.pl/rss/aktualnosci-pliki.xml'),
(u'Testy i recenzje', u'http://www.benchmark.pl/rss/testy-recenzje-minirecenzje.xml')] (u'Testy i recenzje', u'http://www.benchmark.pl/rss/testy-recenzje-minirecenzje.xml')]

38
recipes/conowego_pl.recipe Executable file
View File

@ -0,0 +1,38 @@
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class CoNowegoPl(BasicNewsRecipe):
title = u'conowego.pl'
__author__ = 'fenuks'
description = u'Nowy wortal technologiczny oraz gazeta internetowa. Testy najnowszych produktów, fachowe porady i recenzje. U nas znajdziesz wszystko o elektronice użytkowej !'
cover_url = 'http://www.conowego.pl/fileadmin/templates/main/images/logo_top.png'
category = 'IT, news'
language = 'pl'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
remove_empty_feeds = True
use_embedded_content = False
keep_only_tags = [dict(name='div', attrs={'class':'news_list single_view'})]
remove_tags = [dict(name='div', attrs={'class':['ni_bottom', 'ni_rank', 'ni_date']})]
feeds = [(u'Aktualno\u015bci', u'http://www.conowego.pl/rss/aktualnosci-5/?type=100'), (u'Gaming', u'http://www.conowego.pl/rss/gaming-6/?type=100'), (u'Porady', u'http://www.conowego.pl/rss/porady-3/?type=100'), (u'Testy', u'http://www.conowego.pl/rss/testy-2/?type=100')]
def preprocess_html(self, soup):
for i in soup.findAll('img'):
i.parent.insert(0, BeautifulSoup('<br />'))
i.insert(len(i), BeautifulSoup('<br />'))
self.append_page(soup, soup.body)
return soup
def append_page(self, soup, appendtag):
tag = appendtag.find('div', attrs={'class':'pages'})
if tag:
nexturls=tag.findAll('a')
for nexturl in nexturls[:-1]:
soup2 = self.index_to_soup('http://www.conowego.pl/' + nexturl['href'])
pagetext = soup2.find(attrs={'class':'ni_content'})
pos = len(appendtag.contents)
appendtag.insert(pos, pagetext)
for r in appendtag.findAll(attrs={'class':['pages', 'paginationWrap']}):
r.extract()

32
recipes/dot_net.recipe Normal file
View File

@ -0,0 +1,32 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from calibre.web.feeds.news import BasicNewsRecipe
import re
class NetMagazineRecipe (BasicNewsRecipe):
__author__ = u'Marc Busqué <marc@lamarciana.com>'
__url__ = 'http://www.lamarciana.com'
__version__ = '1.0'
__license__ = 'GPL v3'
__copyright__ = u'2012, Marc Busqué <marc@lamarciana.com>'
title = u'.net magazine'
description = u'net is the worlds best-selling magazine for web designers and developers, featuring tutorials from leading agencies, interviews with the webs biggest names, and agenda-setting features on the hottest issues affecting the internet today.'
language = 'en'
tags = 'web development, software'
oldest_article = 7
remove_empty_feeds = True
no_stylesheets = True
cover_url = u'http://media.netmagazine.futurecdn.net/sites/all/themes/netmag/logo.png'
keep_only_tags = [
dict(name='article', attrs={'class': re.compile('^node.*$', re.IGNORECASE)})
]
remove_tags = [
dict(name='span', attrs={'class': 'comment-count'}),
dict(name='div', attrs={'class': 'item-list share-links'}),
dict(name='footer'),
]
remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height', 'style']
extra_css = 'img {max-width: 100%; display: block; margin: auto;} .captioned-image div {text-align: center; font-style: italic;}'
feeds = [
(u'.net', u'http://feeds.feedburner.com/net/topstories'),
]

View File

@ -1,6 +1,7 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
import re
class Filmweb_pl(BasicNewsRecipe): from calibre.ebooks.BeautifulSoup import BeautifulSoup
class FilmWebPl(BasicNewsRecipe):
title = u'FilmWeb' title = u'FilmWeb'
__author__ = 'fenuks' __author__ = 'fenuks'
description = 'FilmWeb - biggest polish movie site' description = 'FilmWeb - biggest polish movie site'
@ -12,8 +13,9 @@ class Filmweb_pl(BasicNewsRecipe):
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets= True no_stylesheets= True
remove_empty_feeds=True remove_empty_feeds=True
preprocess_regexps = [(re.compile(u'\(kliknij\,\ aby powiększyć\)', re.IGNORECASE), lambda m: ''), ]#(re.compile(ur' | ', re.IGNORECASE), lambda m: '')]
extra_css = '.hdrBig {font-size:22px;} ul {list-style-type:none; padding: 0; margin: 0;}' extra_css = '.hdrBig {font-size:22px;} ul {list-style-type:none; padding: 0; margin: 0;}'
remove_tags= [dict(name='div', attrs={'class':['recommendOthers']}), dict(name='ul', attrs={'class':'fontSizeSet'})] remove_tags= [dict(name='div', attrs={'class':['recommendOthers']}), dict(name='ul', attrs={'class':'fontSizeSet'}), dict(attrs={'class':'userSurname anno'})]
keep_only_tags= [dict(name='h1', attrs={'class':['hdrBig', 'hdrEntity']}), dict(name='div', attrs={'class':['newsInfo', 'newsInfoSmall', 'reviewContent description']})] keep_only_tags= [dict(name='h1', attrs={'class':['hdrBig', 'hdrEntity']}), dict(name='div', attrs={'class':['newsInfo', 'newsInfoSmall', 'reviewContent description']})]
feeds = [(u'Wszystkie newsy', u'http://www.filmweb.pl/feed/news/latest'), feeds = [(u'Wszystkie newsy', u'http://www.filmweb.pl/feed/news/latest'),
(u'News / Filmy w produkcji', 'http://www.filmweb.pl/feed/news/category/filminproduction'), (u'News / Filmy w produkcji', 'http://www.filmweb.pl/feed/news/category/filminproduction'),
@ -31,13 +33,12 @@ class Filmweb_pl(BasicNewsRecipe):
(u'News / Kino polskie', u'http://www.filmweb.pl/feed/news/category/polish.cinema'), (u'News / Kino polskie', u'http://www.filmweb.pl/feed/news/category/polish.cinema'),
(u'News / Telewizja', u'http://www.filmweb.pl/feed/news/category/tv'), (u'News / Telewizja', u'http://www.filmweb.pl/feed/news/category/tv'),
(u'Recenzje redakcji', u'http://www.filmweb.pl/feed/reviews/latest'), (u'Recenzje redakcji', u'http://www.filmweb.pl/feed/reviews/latest'),
(u'Recenzje użytkowników', u'http://www.filmweb.pl/feed/user-reviews/latest')] (u'Recenzje użytkowników', u'http://www.filmweb.pl/feed/user-reviews/latest')
]
def skip_ad_pages(self, soup): def skip_ad_pages(self, soup):
skip_tag = soup.find('a', attrs={'class':'welcomeScreenButton'}) skip_tag = soup.find('a', attrs={'class':'welcomeScreenButton'})
if skip_tag is not None: if skip_tag is not None:
self.log.warn('skip_tag')
self.log.warn(skip_tag)
return self.index_to_soup(skip_tag['href'], raw=True) return self.index_to_soup(skip_tag['href'], raw=True)
@ -45,4 +46,9 @@ class Filmweb_pl(BasicNewsRecipe):
for a in soup('a'): for a in soup('a'):
if a.has_key('href') and 'http://' not in a['href'] and 'https://' not in a['href']: if a.has_key('href') and 'http://' not in a['href'] and 'https://' not in a['href']:
a['href']=self.index + a['href'] a['href']=self.index + a['href']
for i in soup.findAll('a', attrs={'class':'fn'}):
i.insert(len(i), BeautifulSoup('<br />'))
for i in soup.findAll('sup'):
if not i.string or i.string.startswith('(kliknij'):
i.extract()
return soup return soup

View File

@ -1,6 +1,6 @@
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
class Gry_online_pl(BasicNewsRecipe): class GryOnlinePl(BasicNewsRecipe):
title = u'Gry-Online.pl' title = u'Gry-Online.pl'
__author__ = 'fenuks' __author__ = 'fenuks'
description = 'Gry-Online.pl - computer games' description = 'Gry-Online.pl - computer games'
@ -21,17 +21,18 @@ class Gry_online_pl(BasicNewsRecipe):
tag = appendtag.find('div', attrs={'class':'n5p'}) tag = appendtag.find('div', attrs={'class':'n5p'})
if tag: if tag:
nexturls=tag.findAll('a') nexturls=tag.findAll('a')
for nexturl in nexturls[1:]: url_part = soup.find('link', attrs={'rel':'canonical'})['href']
try: url_part = url_part[25:].rpartition('?')[0]
soup2 = self.index_to_soup('http://www.gry-online.pl/S020.asp'+ nexturl['href']) for nexturl in nexturls[1:-1]:
except: soup2 = self.index_to_soup('http://www.gry-online.pl/' + url_part + nexturl['href'])
soup2 = self.index_to_soup('http://www.gry-online.pl/S022.asp'+ nexturl['href'])
pagetext = soup2.find(attrs={'class':'gc660'}) pagetext = soup2.find(attrs={'class':'gc660'})
for r in pagetext.findAll(name='header'): for r in pagetext.findAll(name='header'):
r.extract() r.extract()
for r in pagetext.findAll(attrs={'itemprop':'description'}):
r.extract()
pos = len(appendtag.contents) pos = len(appendtag.contents)
appendtag.insert(pos, pagetext) appendtag.insert(pos, pagetext)
for r in appendtag.findAll(attrs={'class':['n5p', 'add-info', 'twitter-share-button']}): for r in appendtag.findAll(attrs={'class':['n5p', 'add-info', 'twitter-share-button', 'lista lista3 lista-gry']}):
r.extract() r.extract()

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

36
recipes/linux_journal.recipe Executable file
View File

@ -0,0 +1,36 @@
from calibre.web.feeds.news import BasicNewsRecipe
class LinuxJournal(BasicNewsRecipe):
title = u'Linux Journal'
__author__ = 'fenuks'
description = u'The monthly magazine of the Linux community, promoting the use of Linux worldwide.'
cover_url = 'http://www.linuxjournal.com/files/linuxjournal.com/ufiles/logo-lj.jpg'
category = 'IT, Linux'
language = 'en'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
remove_empty_feeds = True
keep_only_tags=[dict(id='content-inner')]
remove_tags_after= dict(attrs={'class':'user-signature clear-block'})
remove_tags=[dict(attrs={'class':['user-signature clear-block', 'breadcrumb', 'terms terms-inline']})]
feeds = [(u'Front Page', u'http://feeds.feedburner.com/linuxjournalcom'), (u'News', u'http://feeds.feedburner.com/LinuxJournal-BreakingNews'), (u'Blogs', u'http://www.linuxjournal.com/blog/feed'), (u'Audio/Video', u'http://www.linuxjournal.com/taxonomy/term/28/0/feed'), (u'Community', u'http://www.linuxjournal.com/taxonomy/term/18/0/feed'), (u'Education', u'http://www.linuxjournal.com/taxonomy/term/25/0/feed'), (u'Embedded', u'http://www.linuxjournal.com/taxonomy/term/27/0/feed'), (u'Hardware', u'http://www.linuxjournal.com/taxonomy/term/23/0/feed'), (u'HOWTOs', u'http://www.linuxjournal.com/taxonomy/term/19/0/feed'), (u'International', u'http://www.linuxjournal.com/taxonomy/term/30/0/feed'), (u'Security', u'http://www.linuxjournal.com/taxonomy/term/31/0/feed'), (u'Software', u'http://www.linuxjournal.com/taxonomy/term/17/0/feed'), (u'Sysadmin', u'http://www.linuxjournal.com/taxonomy/term/21/0/feed'), (u'Webmaster', u'http://www.linuxjournal.com/taxonomy/term/24/0/feed')]
def append_page(self, soup, appendtag):
next = appendtag.find('li', attrs={'class':'pager-next'})
while next:
nexturl = next.a['href']
appendtag.find('div', attrs={'class':'links'}).extract()
soup2 = self.index_to_soup('http://www.linuxjournal.com'+ nexturl)
pagetext = soup2.find(attrs={'class':'node-inner'}).find(attrs={'class':'content'})
next = appendtag.find('li', attrs={'class':'pager-next'})
pos = len(appendtag.contents)
appendtag.insert(pos, pagetext)
tag = appendtag.find('div', attrs={'class':'links'})
if tag:
tag.extract()
def preprocess_html(self, soup):
self.append_page(soup, soup.body)
return soup

33
recipes/list_apart.recipe Normal file
View File

@ -0,0 +1,33 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from calibre.web.feeds.news import BasicNewsRecipe
class AListApart (BasicNewsRecipe):
__author__ = u'Marc Busqué <marc@lamarciana.com>'
__url__ = 'http://www.lamarciana.com'
__version__ = '1.0'
__license__ = 'GPL v3'
__copyright__ = u'2012, Marc Busqué <marc@lamarciana.com>'
title = u'A List Apart'
description = u'A List Apart Magazine (ISSN: 1534-0295) explores the design, development, and meaning of web content, with a special focus on web standards and best practices.'
language = 'en'
tags = 'web development, software'
oldest_article = 120
remove_empty_feeds = True
no_stylesheets = True
encoding = 'utf8'
cover_url = u'http://alistapart.com/pix/alalogo.gif'
keep_only_tags = [
dict(name='div', attrs={'id': 'content'})
]
remove_tags = [
dict(name='ul', attrs={'id': 'metastuff'}),
dict(name='div', attrs={'class': 'discuss'}),
dict(name='div', attrs={'class': 'discuss'}),
dict(name='div', attrs={'id': 'learnmore'}),
]
remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height']
extra_css = u'img {max-width: 100%; display: block; margin: auto;} #authorbio img {float: left; margin-right: 2%;}'
feeds = [
(u'A List Apart', u'http://www.alistapart.com/site/rss'),
]

View File

@ -1,3 +1,4 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class NaTemat(BasicNewsRecipe): class NaTemat(BasicNewsRecipe):
@ -8,8 +9,9 @@ class NaTemat(BasicNewsRecipe):
description = u'informacje, komentarze, opinie' description = u'informacje, komentarze, opinie'
category = 'news' category = 'news'
language = 'pl' language = 'pl'
preprocess_regexps = [(re.compile(ur'Czytaj też\:.*?</a>', re.IGNORECASE), lambda m: ''), (re.compile(ur'Zobacz też\:.*?</a>', re.IGNORECASE), lambda m: ''), (re.compile(ur'Czytaj więcej\:.*?</a>', re.IGNORECASE), lambda m: ''), (re.compile(ur'Czytaj również\:.*?</a>', re.IGNORECASE), lambda m: '')]
cover_url= 'http://blog.plona.pl/wp-content/uploads/2012/05/natemat.png' cover_url= 'http://blog.plona.pl/wp-content/uploads/2012/05/natemat.png'
no_stylesheets = True no_stylesheets = True
keep_only_tags= [dict(id='main')] keep_only_tags= [dict(id='main')]
remove_tags= [dict(attrs={'class':['button', 'block-inside style_default', 'article-related']})] remove_tags= [dict(attrs={'class':['button', 'block-inside style_default', 'article-related', 'user-header', 'links']}), dict(name='img', attrs={'class':'indent'})]
feeds = [(u'Artyku\u0142y', u'http://natemat.pl/rss/wszystkie')] feeds = [(u'Artyku\u0142y', u'http://natemat.pl/rss/wszystkie')]

View File

@ -1,44 +1,79 @@
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
from calibre.ptempfile import PersistentTemporaryFile
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1275708473(BasicNewsRecipe): class PsychologyToday(BasicNewsRecipe):
title = u'Psychology Today'
_author__ = 'rty' title = 'Psychology Today'
publisher = u'www.psychologytoday.com' __author__ = 'Rick Shang'
category = u'Psychology'
max_articles_per_feed = 100 description = 'This magazine takes information from the latest research in the field of psychology and makes it useful to people in their everyday lives. Its coverage encompasses self-improvement, relationships, the mind-body connection, health, family, the workplace and culture.'
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'en' language = 'en'
temp_files = [] category = 'news'
articles_are_obfuscated = True encoding = 'UTF-8'
remove_tags = [ keep_only_tags = [dict(attrs={'class':['print-title', 'print-submitted', 'print-content', 'print-footer', 'print-source_url', 'print-links']})]
dict(name='div', attrs={'class':['print-source_url','field-items','print-footer']}), no_javascript = True
dict(name='span', attrs={'class':'print-footnote'}), no_stylesheets = True
]
remove_tags_before = dict(name='h1', attrs={'class':'print-title'})
remove_tags_after = dict(name='div', attrs={'class':['field-items','print-footer']})
feeds = [(u'Contents', u'http://www.psychologytoday.com/articles/index.rss')]
def get_article_url(self, article): def parse_index(self):
return article.get('link', None) articles = []
soup = self.index_to_soup('http://www.psychologytoday.com/magazine')
#Go to the main body
div = soup.find('div',attrs={'id':'content-content'})
#Find cover & date
cover_item = div.find('div', attrs={'class':'collections-header-image'})
cover = cover_item.find('img',src=True)
self.cover_url = cover['src']
date = self.tag_to_string(cover['title'])
self.timefmt = u' [%s]'%date
articles = []
for post in div.findAll('div', attrs={'class':'collections-node-feature-info'}):
title = self.tag_to_string(post.find('h2'))
author_item=post.find('div', attrs={'class':'collection-node-byline'})
author = re.sub(r'.*by\s',"",self.tag_to_string(author_item).strip())
title = title + u' (%s)'%author
article_page= self.index_to_soup('http://www.psychologytoday.com'+post.find('a', href=True)['href'])
print_page=article_page.find('li', attrs={'class':'print_html first'})
url='http://www.psychologytoday.com'+print_page.find('a',href=True)['href']
desc = self.tag_to_string(post.find('div', attrs={'class':'collection-node-description'})).strip()
self.log('Found article:', title)
self.log('\t', url)
self.log('\t', desc)
articles.append({'title':title, 'url':url, 'date':'','description':desc})
for post in div.findAll('div', attrs={'class':'collections-node-thumbnail-info'}):
title = self.tag_to_string(post.find('h2'))
author_item=post.find('div', attrs={'class':'collection-node-byline'})
article_page= self.index_to_soup('http://www.psychologytoday.com'+post.find('a', href=True)['href'])
print_page=article_page.find('li', attrs={'class':'print_html first'})
description = post.find('div', attrs={'class':'collection-node-description'})
author = re.sub(r'.*by\s',"",self.tag_to_string(description.nextSibling).strip())
desc = self.tag_to_string(description).strip()
url='http://www.psychologytoday.com'+print_page.find('a',href=True)['href']
title = title + u' (%s)'%author
self.log('Found article:', title)
self.log('\t', url)
self.log('\t', desc)
articles.append({'title':title, 'url':url, 'date':'','description':desc})
for post in div.findAll('li', attrs={'class':['collection-item-list-odd','collection-item-list-even']}):
title = self.tag_to_string(post.find('h2'))
author_item=post.find('div', attrs={'class':'collection-node-byline'})
author = re.sub(r'.*by\s',"",self.tag_to_string(author_item).strip())
title = title + u' (%s)'%author
article_page= self.index_to_soup('http://www.psychologytoday.com'+post.find('a', href=True)['href'])
print_page=article_page.find('li', attrs={'class':'print_html first'})
url='http://www.psychologytoday.com'+print_page.find('a',href=True)['href']
desc = self.tag_to_string(post.find('div', attrs={'class':'collection-node-description'})).strip()
self.log('Found article:', title)
self.log('\t', url)
self.log('\t', desc)
articles.append({'title':title, 'url':url, 'date':'','description':desc})
return [('Current Issue', articles)]
def get_obfuscated_article(self, url):
br = self.get_browser()
br.open(url)
response = br.follow_link(url_regex = r'/print/[0-9]+', nr = 0)
html = response.read()
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
self.temp_files[-1].write(html)
self.temp_files[-1].close()
return self.temp_files[-1].name
def get_cover_url(self):
index = 'http://www.psychologytoday.com/magazine/'
soup = self.index_to_soup(index)
for image in soup.findAll('img',{ "class" : "imagefield imagefield-field_magazine_cover" }):
return image['src'] + '.jpg'
return None

View File

@ -1,25 +1,35 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class SanFranciscoBayGuardian(BasicNewsRecipe): class SanFranciscoBayGuardian(BasicNewsRecipe):
title = u'San Francisco Bay Guardian' title = u'San Francisco Bay Guardian'
language = 'en' language = 'en'
__author__ = 'Krittika Goyal' __author__ = 'Krittika Goyal'
oldest_article = 31 #days oldest_article = 31 #days
max_articles_per_feed = 25 max_articles_per_feed = 25
#encoding = 'latin1'
no_stylesheets = True no_stylesheets = True
#remove_tags_before = dict(name='div', attrs={'id':'story_header'})
#remove_tags_after = dict(name='div', attrs={'id':'shirttail'})
remove_tags = [ remove_tags = [
dict(name='iframe'), dict(name='iframe'),
#dict(name='div', attrs={'class':'related-articles'}),
#dict(name='div', attrs={'id':['story_tools', 'toolbox', 'shirttail', 'comment_widget']}),
#dict(name='ul', attrs={'class':'article-tools'}),
#dict(name='ul', attrs={'id':'story_tabs'}),
] ]
feeds = [ feeds = [
('sfbg', 'http://www.sfbg.com/rss.xml'), ('sfbg', 'http://www.sfbg.com/rss.xml'),
('politics', 'http://www.sfbg.com/politics/rss.xml'),
('blogs', 'http://www.sfbg.com/blog/rss.xml'),
('pixel_vision', 'http://www.sfbg.com/pixel_vision/rss.xml'),
('bruce', 'http://www.sfbg.com/bruce/rss.xml'),
] ]
#def preprocess_html(self, soup):
#story = soup.find(name='div', attrs={'id':'story_body'})
#td = heading.findParent(name='td')
#td.extract()
#soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>')
#body = soup.find(name='body')
#body.insert(0, story)
#return soup

View File

@ -1,50 +1,24 @@
#!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
www.smashingmagazine.com
'''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class SmashingMagazine(BasicNewsRecipe): class SmashingMagazine (BasicNewsRecipe):
title = 'Smashing Magazine' __author__ = u'Marc Busqué <marc@lamarciana.com>'
__author__ = 'Darko Miletic' __url__ = 'http://www.lamarciana.com'
description = 'We smash you with the information that will make your life easier, really' __version__ = '1.0.1'
oldest_article = 20 __license__ = 'GPL v3'
language = 'en' __copyright__ = u'2012, Marc Busqué <marc@lamarciana.com>'
max_articles_per_feed = 100 title = u'Smashing Magazine'
no_stylesheets = True description = u'Founded in September 2006, Smashing Magazine delivers useful and innovative information to Web designers and developers. Our aim is to inform our readers about the latest trends and techniques in Web development. We try to persuade you not with the quantity but with the quality of the information we present. Smashing Magazine is and always has been independent.'
use_embedded_content = False language = 'en'
publisher = 'Smashing Magazine' tags = 'web development, software'
category = 'news, web, IT, css, javascript, html' oldest_article = 7
encoding = 'utf-8' remove_empty_feeds = True
no_stylesheets = True
encoding = 'utf8'
cover_url = u'http://media.smashingmagazine.com/themes/smashingv4/images/logo.png'
remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height', 'style']
extra_css = u'body div table:first-child {display: none;} img {max-width: 100%; display: block; margin: auto;}'
conversion_options = { feeds = [
'comments' : description (u'Smashing Magazine', u'http://rss1.smashingmagazine.com/feed/'),
,'tags' : category ]
,'publisher' : publisher
}
keep_only_tags = [dict(name='div', attrs={'id':'leftcolumn'})]
remove_tags_after = dict(name='ul',attrs={'class':'social'})
remove_tags = [
dict(name=['link','object'])
,dict(name='h1',attrs={'class':'logo'})
,dict(name='div',attrs={'id':'booklogosec'})
,dict(attrs={'src':'http://media2.smashingmagazine.com/wp-content/uploads/images/the-smashing-book/smbook6.gif'})
]
feeds = [(u'Articles', u'http://rss1.smashingmagazine.com/feed/')]
def preprocess_html(self, soup):
for iter in soup.findAll('div',attrs={'class':'leftframe'}):
it = iter.find('h1')
if it == None:
iter.extract()
for item in soup.findAll('img'):
oldParent = item.parent
if oldParent.name == 'a':
oldParent.name = 'div'
return soup

View File

@ -1,61 +1,67 @@
import re import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup from collections import OrderedDict
class SmithsonianMagazine(BasicNewsRecipe): class Smithsonian(BasicNewsRecipe):
title = u'Smithsonian Magazine'
language = 'en'
__author__ = 'Krittika Goyal and TerminalVeracity'
oldest_article = 31#days
max_articles_per_feed = 50
use_embedded_content = False
recursions = 1
cover_url = 'http://sphotos.xx.fbcdn.net/hphotos-snc7/431147_10150602715983253_764313347_n.jpg'
match_regexps = ['&page=[2-9]$']
preprocess_regexps = [
(re.compile(r'for more of Smithsonian\'s coverage on history, science and nature.', re.DOTALL), lambda m: '')
]
extra_css = """
h1{font-size: large; margin: .2em 0}
h2{font-size: medium; margin: .2em 0}
h3{font-size: medium; margin: .2em 0}
#byLine{margin: .2em 0}
.articleImageCaptionwide{font-style: italic}
.wp-caption-text{font-style: italic}
img{display: block}
"""
title = 'Smithsonian Magazine'
__author__ = 'Rick Shang'
remove_stylesheets = True description = 'This magazine chronicles the arts, environment, sciences and popular culture of the times. It is edited for modern, well-rounded individuals with diverse, general interests. With your order, you become a National Associate Member of the Smithsonian. Membership benefits include your subscription to Smithsonian magazine, a personalized membership card, discounts from the Smithsonian catalog, and more.'
remove_tags_after = dict(name='div', attrs={'class':['post','articlePaginationWrapper']}) language = 'en'
remove_tags = [ category = 'news'
dict(name='iframe'), encoding = 'UTF-8'
dict(name='div', attrs={'class':['article_sidebar_border','viewMorePhotos','addtoany_share_save_container','meta','social','OUTBRAIN','related-articles-inpage']}), keep_only_tags = [dict(attrs={'id':['articleTitle', 'subHead', 'byLine', 'articleImage', 'article-text']})]
dict(name='div', attrs={'id':['article_sidebar_border', 'most-popular_large', 'most-popular-body_large','comment_section','article-related']}), remove_tags = [dict(attrs={'class':['related-articles-inpage', 'viewMorePhotos']})]
dict(name='ul', attrs={'class':'cat-breadcrumb col three last'}), no_javascript = True
dict(name='h4', attrs={'id':'related-topics'}), no_stylesheets = True
dict(name='table'),
dict(name='a', attrs={'href':['/subArticleBottomWeb','/subArticleTopWeb','/subArticleTopMag','/subArticleBottomMag']}),
dict(name='a', attrs={'name':'comments_shaded'}),
]
def parse_index(self):
#Go to the issue
soup0 = self.index_to_soup('http://www.smithsonianmag.com/issue/archive/')
div = soup0.find('div',attrs={'id':'archives'})
issue = div.find('ul',attrs={'class':'clear-both'})
current_issue_url = issue.find('a', href=True)['href']
soup = self.index_to_soup(current_issue_url)
feeds = [ #Go to the main body
('History and Archeology', div = soup.find ('div', attrs={'id':'content-inset'})
'http://feeds.feedburner.com/smithsonianmag/history-archaeology'),
('People and Places', #Find date
'http://feeds.feedburner.com/smithsonianmag/people-places'), date = re.sub('.*\:\W*', "", self.tag_to_string(div.find('h2')).strip())
('Science and Nature', self.timefmt = u' [%s]'%date
'http://feeds.feedburner.com/smithsonianmag/science-nature'),
('Arts and Culture', #Find cover
'http://feeds.feedburner.com/smithsonianmag/arts-culture'), self.cover_url = div.find('img',src=True)['src']
('Travel',
'http://feeds.feedburner.com/smithsonianmag/travel'), feeds = OrderedDict()
] section_title = ''
subsection_title = ''
for post in div.findAll('div', attrs={'class':['plainModule', 'departments plainModule']}):
articles = []
prefix = ''
h3=post.find('h3')
if h3 is not None:
section_title = self.tag_to_string(h3)
else:
subsection=post.find('p',attrs={'class':'article-cat'})
link=post.find('a',href=True)
url=link['href']+'?c=y&story=fullstory'
if subsection is not None:
subsection_title = self.tag_to_string(subsection)
prefix = (subsection_title+': ')
description=self.tag_to_string(post('p', limit=2)[1]).strip()
else:
description=self.tag_to_string(post.find('p')).strip()
desc=re.sub('\sBy\s.*', '', description, re.DOTALL)
author=re.sub('.*By\s', '', description, re.DOTALL)
title=prefix + self.tag_to_string(link).strip()+ u' (%s)'%author
articles.append({'title':title, 'url':url, 'description':desc, 'date':''})
if articles:
if section_title not in feeds:
feeds[section_title] = []
feeds[section_title] += articles
ans = [(key, val) for key, val in feeds.iteritems()]
return ans
def preprocess_html(self, soup):
story = soup.find(name='div', attrs={'id':'article-body'})
soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>')
body = soup.find(name='body')
body.insert(0, story)
return soup

View File

@ -1,45 +1,64 @@
from calibre.web.feeds.news import BasicNewsRecipe import re
from calibre.web.feeds.recipes import BasicNewsRecipe
from collections import OrderedDict
class The_New_Republic(BasicNewsRecipe): class TNR(BasicNewsRecipe):
title = 'The New Republic'
__author__ = 'cix3' title = 'The New Republic'
__author__ = 'Rick Shang'
description = 'The New Republic is a journal of opinion with an emphasis on politics and domestic and international affairs. It carries feature articles by staff and contributing editors. The second half of each issue is devoted to book and the arts, theater, motion pictures, music and art.'
language = 'en' language = 'en'
description = 'Intelligent, stimulating and rigorous examination of American politics, foreign policy and culture' category = 'news'
timefmt = ' [%b %d, %Y]' encoding = 'UTF-8'
remove_tags = [dict(attrs={'class':['print-logo','print-site_name','print-hr']})]
oldest_article = 7 no_javascript = True
max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
remove_tags = [
dict(name='div', attrs={'class':['print-logo', 'print-site_name', 'img-left', 'print-source_url']}),
dict(name='hr', attrs={'class':'print-hr'}), dict(name='img')
]
feeds = [ def parse_index(self):
('Politics', 'http://www.tnr.com/rss/articles/Politics'),
('Books and Arts', 'http://www.tnr.com/rss/articles/Books-and-Arts'),
('Economy', 'http://www.tnr.com/rss/articles/Economy'),
('Environment and Energy', 'http://www.tnr.com/rss/articles/Environment-%2526-Energy'),
('Health Care', 'http://www.tnr.com/rss/articles/Health-Care'),
('Metro Policy', 'http://www.tnr.com/rss/articles/Metro-Policy'),
('World', 'http://www.tnr.com/rss/articles/World'),
('Film', 'http://www.tnr.com/rss/articles/Film'),
('Books', 'http://www.tnr.com/rss/articles/books'),
('The Book', 'http://www.tnr.com/rss/book'),
('Jonathan Chait', 'http://www.tnr.com/rss/blogs/Jonathan-Chait'),
('The Plank', 'http://www.tnr.com/rss/blogs/The-Plank'),
('The Treatment', 'http://www.tnr.com/rss/blogs/The-Treatment'),
('The Spine', 'http://www.tnr.com/rss/blogs/The-Spine'),
('The Vine', 'http://www.tnr.com/rss/blogs/The-Vine'),
('The Avenue', 'http://www.tnr.com/rss/blogs/The-Avenue'),
('William Galston', 'http://www.tnr.com/rss/blogs/William-Galston'),
('Simon Johnson', 'http://www.tnr.com/rss/blogs/Simon-Johnson'),
('Ed Kilgore', 'http://www.tnr.com/rss/blogs/Ed-Kilgore'),
('Damon Linker', 'http://www.tnr.com/rss/blogs/Damon-Linker'),
('John McWhorter', 'http://www.tnr.com/rss/blogs/John-McWhorter')
]
def print_version(self, url): #Go to the issue
return url.replace('http://www.tnr.com/', 'http://www.tnr.com/print/') soup0 = self.index_to_soup('http://www.tnr.com/magazine-issues')
issue = soup0.find('div',attrs={'id':'current_issue'})
#Find date
date = self.tag_to_string(issue.find('div',attrs={'class':'date'})).strip()
self.timefmt = u' [%s]'%date
#Go to the main body
current_issue_url = 'http://www.tnr.com' + issue.find('a', href=True)['href']
soup = self.index_to_soup(current_issue_url)
div = soup.find ('div', attrs={'class':'article_detail_body'})
#Find cover
self.cover_url = div.find('img',src=True)['src']
feeds = OrderedDict()
section_title = ''
subsection_title = ''
for post in div.findAll('p'):
articles = []
em=post.find('em')
b=post.find('b')
a=post.find('a',href=True)
if em is not None:
section_title = self.tag_to_string(em).strip()
subsection_title = ''
elif b is not None:
subsection_title=self.tag_to_string(b).strip()
elif a is not None:
prefix = (subsection_title+': ') if subsection_title else ''
url=re.sub('www.tnr.com','www.tnr.com/print', a['href'])
author=re.sub('.*by\s', '', self.tag_to_string(post), re.DOTALL)
title=prefix + self.tag_to_string(a).strip()+ u' (%s)'%author
articles.append({'title':title, 'url':url, 'description':'', 'date':''})
if articles:
if section_title not in feeds:
feeds[section_title] = []
feeds[section_title] += articles
ans = [(key, val) for key, val in feeds.iteritems()]
return ans

View File

@ -1,4 +1,4 @@
import re, random import random
from calibre import browser from calibre import browser
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
@ -8,45 +8,43 @@ class AdvancedUserRecipe1325006965(BasicNewsRecipe):
title = u'The Sun UK' title = u'The Sun UK'
description = 'Articles from The Sun tabloid UK' description = 'Articles from The Sun tabloid UK'
__author__ = 'Dave Asbury' __author__ = 'Dave Asbury'
# last updated 15/7/12 # last updated 25/7/12
language = 'en_GB' language = 'en_GB'
oldest_article = 1 oldest_article = 1
max_articles_per_feed = 15 max_articles_per_feed = 12
remove_empty_feeds = True remove_empty_feeds = True
no_stylesheets = True no_stylesheets = True
masthead_url = 'http://www.thesun.co.uk/sol/img/global/Sun-logo.gif' masthead_url = 'http://www.thesun.co.uk/sol/img/global/Sun-logo.gif'
encoding = 'UTF-8' encoding = 'UTF-8'
remove_empty_feeds = True
remove_javascript = True remove_javascript = True
no_stylesheets = True no_stylesheets = True
#preprocess_regexps = [
# (re.compile(r'<div class="foot-copyright".*?</div>', re.IGNORECASE | re.DOTALL), lambda match: '')]
extra_css = ''' extra_css = '''
body{ text-align: justify; font-family:Arial,Helvetica,sans-serif; font-size:11px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal;} body{ text-align: justify; font-family:Arial,Helvetica,sans-serif; font-size:11px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal;}
''' '''
preprocess_regexps = [
(re.compile(r'<div class="foot-copyright".*?</div>', re.IGNORECASE | re.DOTALL), lambda match: '')]
keep_only_tags = [ keep_only_tags = [
dict(name='h1'),dict(name='h2',attrs={'class' : ['large','large centered','medium centered','medium']}),dict(name='h3'), dict(name='div',attrs={'class' : 'intro'}),
dict(name='div',attrs={'class' : 'text-center'}), dict(name='h3'),
dict(name='div',attrs={'id' : 'bodyText'}) dict(name='div',attrs={'id' : 'articlebody'}),
# dict(name='p') #dict(attrs={'class' : ['right_col_branding','related-stories','mystery-meat-link','ltbx-container','ltbx-var ltbx-hbxpn','ltbx-var ltbx-nav-loop','ltbx-var ltbx-url']}),
] # dict(name='div',attrs={'class' : 'cf'}),
remove_tags=[ # dict(attrs={'title' : 'download flash'}),
#dict(name='head'), # dict(attrs={'style' : 'padding: 5px'})
dict(attrs={'class' : ['mystery-meat-link','ltbx-container','ltbx-var ltbx-hbxpn','ltbx-var ltbx-nav-loop','ltbx-var ltbx-url']}),
dict(name='div',attrs={'class' : 'cf'}),
dict(attrs={'title' : 'download flash'}),
dict(attrs={'style' : 'padding: 5px'})
] ]
remove_tags_after = [dict(id='bodyText')]
remove_tags=[
dict(name='li'),
dict(attrs={'class' : 'grid-4 right-hand-column'}),
]
feeds = [ feeds = [
(u'News', u'http://www.thesun.co.uk/sol/homepage/news/rss'), (u'News', u'http://www.thesun.co.uk/sol/homepage/news/rss'),

View File

@ -1,7 +1,7 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
import re import re
class AdvancedUserRecipe1312886443(BasicNewsRecipe): class WNP(BasicNewsRecipe):
title = u'WNP' title = u'WNP'
cover_url= 'http://k.wnp.pl/images/wnpLogo.gif' cover_url= 'http://k.wnp.pl/images/wnpLogo.gif'
__author__ = 'fenuks' __author__ = 'fenuks'
@ -12,7 +12,7 @@ class AdvancedUserRecipe1312886443(BasicNewsRecipe):
oldest_article = 8 oldest_article = 8
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets= True no_stylesheets= True
remove_tags=[dict(attrs={'class':'printF'})] remove_tags=[dict(attrs={'class':['printF', 'border3B2 clearfix', 'articleMenu clearfix']})]
feeds = [(u'Wiadomości gospodarcze', u'http://www.wnp.pl/rss/serwis_rss.xml'), feeds = [(u'Wiadomości gospodarcze', u'http://www.wnp.pl/rss/serwis_rss.xml'),
(u'Serwis Energetyka - Gaz', u'http://www.wnp.pl/rss/serwis_rss_1.xml'), (u'Serwis Energetyka - Gaz', u'http://www.wnp.pl/rss/serwis_rss_1.xml'),
(u'Serwis Nafta - Chemia', u'http://www.wnp.pl/rss/serwis_rss_2.xml'), (u'Serwis Nafta - Chemia', u'http://www.wnp.pl/rss/serwis_rss_2.xml'),

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -4,7 +4,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__ = u'calibre' __appname__ = u'calibre'
numeric_version = (0, 8, 61) numeric_version = (0, 8, 62)
__version__ = u'.'.join(map(unicode, numeric_version)) __version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>" __author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -10,7 +10,7 @@ import cStringIO
from calibre.devices.usbms.driver import USBMS from calibre.devices.usbms.driver import USBMS
HTC_BCDS = [0x100, 0x0222, 0x0226, 0x227, 0x228, 0x229] HTC_BCDS = [0x100, 0x0222, 0x0226, 0x227, 0x228, 0x229, 0x9999]
class ANDROID(USBMS): class ANDROID(USBMS):
@ -41,9 +41,10 @@ class ANDROID(USBMS):
0xca9 : HTC_BCDS, 0xca9 : HTC_BCDS,
0xcac : HTC_BCDS, 0xcac : HTC_BCDS,
0xccf : HTC_BCDS, 0xccf : HTC_BCDS,
0xcd6 : HTC_BCDS,
0xce5 : HTC_BCDS, 0xce5 : HTC_BCDS,
0x2910 : HTC_BCDS, 0x2910 : HTC_BCDS,
0xff9 : HTC_BCDS + [0x9999], 0xff9 : HTC_BCDS,
}, },
# Eken # Eken
@ -194,7 +195,7 @@ class ANDROID(USBMS):
'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON', 'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON',
'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP', 'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP',
'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD', 'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD',
'PMP5097C', 'MASS', 'NOVO7'] 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID',
@ -212,7 +213,7 @@ class ANDROID(USBMS):
'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER',
'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX',
'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE', 'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE',
'ADVANCED'] 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
@ -221,7 +222,8 @@ class ANDROID(USBMS):
'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD', 'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD',
'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC', 'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC',
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875',
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD'] 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
'USB_FLASH_DRIVER']
OSX_MAIN_MEM = 'Android Device Main Memory' OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -15,6 +15,8 @@ class DevicePlugin(Plugin):
#: Ordered list of supported formats #: Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"] FORMATS = ["lrf", "rtf", "pdf", "txt"]
# If True, the config dialog will not show the formats box
HIDE_FORMATS_CONFIG_BOX = False
#: VENDOR_ID can be either an integer, a list of integers or a dictionary #: VENDOR_ID can be either an integer, a list of integers or a dictionary
#: If it is a dictionary, it must be a dictionary of dictionaries, #: If it is a dictionary, it must be a dictionary of dictionaries,
@ -496,6 +498,92 @@ class DevicePlugin(Plugin):
''' '''
return paths return paths
def startup(self):
'''
Called when calibre is is starting the device. Do any initialization
required. Note that multiple instances of the class can be instantiated,
and thus __init__ can be called multiple times, but only one instance
will have this method called.
'''
pass
def shutdown(self):
'''
Called when calibre is shutting down, either for good or in preparation
to restart. Do any cleanup required.
'''
pass
# Dynamic control interface.
# The following methods are probably called on the GUI thread. Any driver
# that implements these methods must take pains to be thread safe, because
# the device_manager might be using the driver at the same time that one of
# these methods is called.
def is_dynamically_controllable(self):
'''
Called by the device manager when starting plugins. If this method returns
a string, then a) it supports the device manager's dynamic control
interface, and b) that name is to be used when talking to the plugin.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
return None
def start_plugin(self):
'''
This method is called to start the plugin. The plugin should begin
to accept device connections however it does that. If the plugin is
already accepting connections, then do nothing.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
pass
def stop_plugin(self):
'''
This method is called to stop the plugin. The plugin should no longer
accept connections, and should cleanup behind itself. It is likely that
this method should call shutdown. If the plugin is already not accepting
connections, then do nothing.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
pass
def get_option(self, opt_string, default=None):
'''
Return the value of the option indicated by opt_string. This method can
be called when the plugin is not started. Return None if the option does
not exist.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
return default
def set_option(self, opt_string, opt_value):
'''
Set the value of the option indicated by opt_string. This method can
be called when the plugin is not started.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
pass
def is_running(self):
'''
Return True if the plugin is started, otherwise false
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
return False
class BookList(list): class BookList(list):
''' '''
A list of books. Each Book object must have the fields A list of books. Each Book object must have the fields

View File

@ -376,6 +376,8 @@ class PRST1(USBMS):
# Record what the max id being used is as well. # Record what the max id being used is as well.
db_books = {} db_books = {}
for i, row in enumerate(cursor): for i, row in enumerate(cursor):
if row[0] is None:
continue
lpath = row[0].replace('\\', '/') lpath = row[0].replace('\\', '/')
db_books[lpath] = row[1] db_books[lpath] = row[1]
if row[1] < sequence_min: if row[1] < sequence_min:

View File

@ -198,11 +198,13 @@ class EPUBInput(InputFormatPlugin):
('application/vnd.adobe-page-template+xml','application/text'): ('application/vnd.adobe-page-template+xml','application/text'):
not_for_spine.add(id_) not_for_spine.add(id_)
seen = set()
for x in list(opf.iterspine()): for x in list(opf.iterspine()):
ref = x.get('idref', None) ref = x.get('idref', None)
if ref is None or ref in not_for_spine: if not ref or ref in not_for_spine or ref in seen:
x.getparent().remove(x) x.getparent().remove(x)
continue continue
seen.add(ref)
if len(list(opf.iterspine())) == 0: if len(list(opf.iterspine())) == 0:
raise ValueError('No valid entries in the spine of this EPUB') raise ValueError('No valid entries in the spine of this EPUB')

View File

@ -326,7 +326,7 @@ OptionRecommendation(name='page_breaks_before',
recommended_value="//*[name()='h1' or name()='h2']", recommended_value="//*[name()='h1' or name()='h2']",
level=OptionRecommendation.LOW, level=OptionRecommendation.LOW,
help=_('An XPath expression. Page breaks are inserted ' help=_('An XPath expression. Page breaks are inserted '
'before the specified elements.') 'before the specified elements. To disable use the expression: /')
), ),
OptionRecommendation(name='remove_fake_margins', OptionRecommendation(name='remove_fake_margins',

View File

@ -117,8 +117,8 @@ class JsonCodec(object):
def __init__(self): def __init__(self):
self.field_metadata = FieldMetadata() self.field_metadata = FieldMetadata()
def encode_to_file(self, file, booklist): def encode_to_file(self, file_, booklist):
file.write(json.dumps(self.encode_booklist_metadata(booklist), file_.write(json.dumps(self.encode_booklist_metadata(booklist),
indent=2, encoding='utf-8')) indent=2, encoding='utf-8'))
def encode_booklist_metadata(self, booklist): def encode_booklist_metadata(self, booklist):
@ -156,21 +156,28 @@ class JsonCodec(object):
else: else:
return object_to_unicode(value) return object_to_unicode(value)
def decode_from_file(self, file, booklist, book_class, prefix): def decode_from_file(self, file_, booklist, book_class, prefix):
js = [] js = []
try: try:
js = json.load(file, encoding='utf-8') js = json.load(file_, encoding='utf-8')
for item in js: for item in js:
book = book_class(prefix, item.get('lpath', None)) booklist.append(self.raw_to_book(item, book_class, prefix))
for key in item.keys(): except:
meta = self.decode_metadata(key, item[key]) print 'exception during JSON decode_from_file'
if key == 'user_metadata': traceback.print_exc()
book.set_all_user_metadata(meta)
else: def raw_to_book(self, json_book, book_class, prefix):
if key == 'classifiers': try:
key = 'identifiers' book = book_class(prefix, json_book.get('lpath', None))
setattr(book, key, meta) for key,val in json_book.iteritems():
booklist.append(book) meta = self.decode_metadata(key, val)
if key == 'user_metadata':
book.set_all_user_metadata(meta)
else:
if key == 'classifiers':
key = 'identifiers'
setattr(book, key, meta)
return book
except: except:
print 'exception during JSON decoding' print 'exception during JSON decoding'
traceback.print_exc() traceback.print_exc()

View File

@ -286,15 +286,17 @@ class Spine(ResourceCollection): # {{{
@staticmethod @staticmethod
def from_opf_spine_element(itemrefs, manifest): def from_opf_spine_element(itemrefs, manifest):
s = Spine(manifest) s = Spine(manifest)
seen = set()
for itemref in itemrefs: for itemref in itemrefs:
idref = itemref.get('idref', None) idref = itemref.get('idref', None)
if idref is not None: if idref is not None:
path = s.manifest.path_for_id(idref) path = s.manifest.path_for_id(idref)
if path: if path and path not in seen:
r = Spine.Item(lambda x:idref, path, is_path=True) r = Spine.Item(lambda x:idref, path, is_path=True)
r.is_linear = itemref.get('linear', 'yes') == 'yes' r.is_linear = itemref.get('linear', 'yes') == 'yes'
r.idref = idref r.idref = idref
s.append(r) s.append(r)
seen.add(path)
return s return s
@staticmethod @staticmethod

View File

@ -12,7 +12,7 @@ import re
from calibre.ebooks.oeb.base import (OEB_DOCS, XHTML, XHTML_NS, XML_NS, from calibre.ebooks.oeb.base import (OEB_DOCS, XHTML, XHTML_NS, XML_NS,
namespace, prefixname, urlnormalize) namespace, prefixname, urlnormalize)
from calibre.ebooks.mobi.mobiml import MBP_NS from calibre.ebooks.mobi.mobiml import MBP_NS
from calibre.ebooks.mobi.utils import is_guide_ref_start from calibre.ebooks.mobi.utils import is_guide_ref_start, utf8_text
from collections import defaultdict from collections import defaultdict
from urlparse import urldefrag from urlparse import urldefrag
@ -355,7 +355,7 @@ class Serializer(object):
text = text.replace(u'\u00AD', '') # Soft-hyphen text = text.replace(u'\u00AD', '') # Soft-hyphen
if quot: if quot:
text = text.replace('"', '&quot;') text = text.replace('"', '&quot;')
self.buf.write(text.encode('utf-8')) self.buf.write(utf8_text(text))
def fixup_links(self): def fixup_links(self):
''' '''

View File

@ -0,0 +1,65 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid at kovidgoyal.net>
Released under the GPLv3 License
###
log = window.calibre_utils.log
class FullScreen
# This class is a namespace to expose functions via the
# window.full_screen object. The most important functions are:
constructor: () ->
if not this instanceof arguments.callee
throw new Error('FullScreen constructor called as function')
this.in_full_screen = false
this.initial_left_margin = null
this.initial_right_margin = null
save_margins: () ->
bs = document.body.style
this.initial_left_margin = bs.marginLeft
this.initial_right_margin = bs.marginRight
on: (max_text_width, in_paged_mode) ->
if in_paged_mode
window.paged_display.max_col_width = max_text_width
else
s = document.body.style
s.maxWidth = max_text_width + 'px'
s.marginLeft = 'auto'
s.marginRight = 'auto'
window.addEventListener('click', this.handle_click, false)
off: (in_paged_mode) ->
window.removeEventListener('click', this.handle_click, false)
if in_paged_mode
window.paged_display.max_col_width = -1
else
s = document.body.style
s.maxWidth = 'none'
if this.initial_left_margin != null
s.marginLeft = this.initial_left_margin
if this.initial_right_margin != null
s.marginRight = this.initial_right_margin
handle_click: (event) ->
if event.target != document.documentElement or event.button != 0
return
res = null
if window.paged_display.in_paged_mode
res = window.paged_display.click_for_page_turn(event)
else
br = document.body.getBoundingClientRect()
if not (br.left <= event.clientX <= br.right)
res = event.clientX < br.left
if res != null
window.py_bridge.page_turn_requested(res)
if window?
window.full_screen = new FullScreen()

View File

@ -26,6 +26,7 @@ class PagedDisplay
this.current_margin_side = 0 this.current_margin_side = 0
this.is_full_screen_layout = false this.is_full_screen_layout = false
this.max_col_width = -1 this.max_col_width = -1
this.current_page_height = null
this.document_margins = null this.document_margins = null
this.use_document_margins = false this.use_document_margins = false
@ -74,25 +75,12 @@ class PagedDisplay
# start_time = new Date().getTime() # start_time = new Date().getTime()
body_style = window.getComputedStyle(document.body) body_style = window.getComputedStyle(document.body)
bs = document.body.style bs = document.body.style
# When laying body out in columns, webkit bleeds the top margin of the
# first block element out above the columns, leading to an extra top
# margin for the page. We compensate for that here. Computing the
# boundingrect of body is very expensive with column layout, so we do
# it before the column layout is applied.
first_layout = false first_layout = false
if not this.in_paged_mode if not this.in_paged_mode
bs.setProperty('margin-top', '0px')
extra_margin = document.body.getBoundingClientRect().top
if extra_margin <= this.margin_top
extra_margin = 0
margin_top = (this.margin_top - extra_margin) + 'px'
# Check if the current document is a full screen layout like # Check if the current document is a full screen layout like
# cover, if so we treat it specially. # cover, if so we treat it specially.
single_screen = (document.body.scrollWidth < window.innerWidth + 25 and document.body.scrollHeight < window.innerHeight + 25) single_screen = (document.body.scrollWidth < window.innerWidth + 25 and document.body.scrollHeight < window.innerHeight + 25)
first_layout = true first_layout = true
else
# resize event
margin_top = body_style.marginTop
ww = window.innerWidth ww = window.innerWidth
@ -116,16 +104,23 @@ class PagedDisplay
col_width = Math.max(100, ((ww - adjust)/n) - 2*sm) col_width = Math.max(100, ((ww - adjust)/n) - 2*sm)
this.page_width = col_width + 2*sm this.page_width = col_width + 2*sm
this.screen_width = this.page_width * this.cols_per_screen this.screen_width = this.page_width * this.cols_per_screen
this.current_page_height = window.innerHeight - this.margin_top - this.margin_bottom
fgcolor = body_style.getPropertyValue('color') fgcolor = body_style.getPropertyValue('color')
bs.setProperty('-webkit-column-gap', (2*sm)+'px') bs.setProperty('-webkit-column-gap', (2*sm)+'px')
bs.setProperty('-webkit-column-width', col_width+'px') bs.setProperty('-webkit-column-width', col_width+'px')
bs.setProperty('-webkit-column-rule-color', fgcolor) bs.setProperty('-webkit-column-rule-color', fgcolor)
# Without this, webkit bleeds the margin of the first block(s) of body
# above the columns, which causes them to effectively be added to the
# page margins (the margin collapse algorithm)
bs.setProperty('-webkit-margin-collapse', 'separate')
bs.setProperty('overflow', 'visible') bs.setProperty('overflow', 'visible')
bs.setProperty('height', (window.innerHeight - this.margin_top - this.margin_bottom) + 'px') bs.setProperty('height', (window.innerHeight - this.margin_top - this.margin_bottom) + 'px')
bs.setProperty('width', (window.innerWidth - 2*sm)+'px') bs.setProperty('width', (window.innerWidth - 2*sm)+'px')
bs.setProperty('margin-top', margin_top) bs.setProperty('margin-top', this.margin_top + 'px')
bs.setProperty('margin-bottom', this.margin_bottom+'px') bs.setProperty('margin-bottom', this.margin_bottom+'px')
bs.setProperty('margin-left', sm+'px') bs.setProperty('margin-left', sm+'px')
bs.setProperty('margin-right', sm+'px') bs.setProperty('margin-right', sm+'px')
@ -167,9 +162,15 @@ class PagedDisplay
# that this method use getBoundingClientRect() which means it will # that this method use getBoundingClientRect() which means it will
# force a relayout if the render tree is dirty. # force a relayout if the render tree is dirty.
images = [] images = []
vimages = []
maxh = this.current_page_height
for img in document.getElementsByTagName('img') for img in document.getElementsByTagName('img')
previously_limited = calibre_utils.retrieve(img, 'width-limited', false) previously_limited = calibre_utils.retrieve(img, 'width-limited', false)
data = calibre_utils.retrieve(img, 'img-data', null)
br = img.getBoundingClientRect() br = img.getBoundingClientRect()
if data == null
data = {'left':br.left, 'right':br.right, 'height':br.height, 'display': img.style.display}
calibre_utils.store(img, 'img-data', data)
left = calibre_utils.viewport_to_document(br.left, 0, doc=img.ownerDocument)[0] left = calibre_utils.viewport_to_document(br.left, 0, doc=img.ownerDocument)[0]
col = this.column_at(left) * this.page_width col = this.column_at(left) * this.page_width
rleft = left - col - this.current_margin_side rleft = left - col - this.current_margin_side
@ -178,23 +179,28 @@ class PagedDisplay
col_width = this.page_width - 2*this.current_margin_side col_width = this.page_width - 2*this.current_margin_side
if previously_limited or rright > col_width if previously_limited or rright > col_width
images.push([img, col_width - rleft]) images.push([img, col_width - rleft])
previously_limited = calibre_utils.retrieve(img, 'height-limited', false)
if previously_limited or br.height > maxh
vimages.push(img)
if previously_limited
img.style.setProperty('-webkit-column-break-before', 'auto')
img.style.setProperty('display', data.display)
img.style.setProperty('-webkit-column-break-inside', 'avoid')
for [img, max_width] in images for [img, max_width] in images
img.style.setProperty('max-width', max_width+'px') img.style.setProperty('max-width', max_width+'px')
calibre_utils.store(img, 'width-limited', true) calibre_utils.store(img, 'width-limited', true)
check_top_margin: () -> for img in vimages
# This is needed to handle the case when a descendant of body specifies data = calibre_utils.retrieve(img, 'img-data', null)
# a top margin as a percentage, which messes up the top margin img.style.setProperty('-webkit-column-break-before', 'always')
# calculations above img.style.setProperty('max-height', maxh+'px')
tm = document.body.getBoundingClientRect().top if data.height > maxh
if tm != this.margin_top # This is needed to force the image onto a new page, without
document.body.style.setProperty('margin-top', '0px') # it, the webkit algorithm may still decide to split the image
tm = document.body.getBoundingClientRect().top # by keeping it part of its parent block
if tm <= this.margin_top img.style.setProperty('display', 'block')
tm = 0 calibre_utils.store(img, 'height-limited', true)
m = this.margin_top - tm
document.body.style.setProperty('margin-top', m+'px')
scroll_to_pos: (frac) -> scroll_to_pos: (frac) ->
# Scroll to the position represented by frac (number between 0 and 1) # Scroll to the position represented by frac (number between 0 and 1)
@ -395,6 +401,18 @@ class PagedDisplay
log('Viewport cfi:', ans) log('Viewport cfi:', ans)
return ans return ans
click_for_page_turn: (event) ->
# Check if the click event event should generate a apge turn. Returns
# null if it should not, true if it is a backwards page turn, false if
# it is a forward apge turn.
left_boundary = this.current_margin_side
right_bondary = this.screen_width - this.current_margin_side
if left_boundary > event.clientX
return true
if right_bondary < event.clientX
return false
return null
if window? if window?
window.paged_display = new PagedDisplay() window.paged_display = new PagedDisplay()

View File

@ -82,10 +82,17 @@ class DetectStructure(object):
def detect_chapters(self): def detect_chapters(self):
self.detected_chapters = [] self.detected_chapters = []
def find_matches(expr, doc):
try:
return XPath(expr)(doc)
except:
self.log.warn('Invalid chapter expression, ignoring: %s'%expr)
return []
if self.opts.chapter: if self.opts.chapter:
chapter_xpath = XPath(self.opts.chapter)
for item in self.oeb.spine: for item in self.oeb.spine:
for x in chapter_xpath(item.data): for x in find_matches(self.opts.chapter, item.data):
self.detected_chapters.append((item, x)) self.detected_chapters.append((item, x))
chapter_mark = self.opts.chapter_mark chapter_mark = self.opts.chapter_mark
@ -164,11 +171,19 @@ class DetectStructure(object):
added = OrderedDict() added = OrderedDict()
added2 = OrderedDict() added2 = OrderedDict()
counter = 1 counter = 1
def find_matches(expr, doc):
try:
return XPath(expr)(doc)
except:
self.log.warn('Invalid ToC expression, ignoring: %s'%expr)
return []
for document in self.oeb.spine: for document in self.oeb.spine:
previous_level1 = list(added.itervalues())[-1] if added else None previous_level1 = list(added.itervalues())[-1] if added else None
previous_level2 = list(added2.itervalues())[-1] if added2 else None previous_level2 = list(added2.itervalues())[-1] if added2 else None
for elem in XPath(self.opts.level1_toc)(document.data): for elem in find_matches(self.opts.level1_toc, document.data):
text, _href = self.elem_to_link(document, elem, counter) text, _href = self.elem_to_link(document, elem, counter)
counter += 1 counter += 1
if text: if text:
@ -178,7 +193,7 @@ class DetectStructure(object):
#node.add(_('Top'), _href) #node.add(_('Top'), _href)
if self.opts.level2_toc is not None and added: if self.opts.level2_toc is not None and added:
for elem in XPath(self.opts.level2_toc)(document.data): for elem in find_matches(self.opts.level2_toc, document.data):
level1 = None level1 = None
for item in document.data.iterdescendants(): for item in document.data.iterdescendants():
if item in added: if item in added:
@ -196,7 +211,8 @@ class DetectStructure(object):
break break
if self.opts.level3_toc is not None and added2: if self.opts.level3_toc is not None and added2:
for elem in XPath(self.opts.level3_toc)(document.data): for elem in find_matches(self.opts.level3_toc,
document.data):
level2 = None level2 = None
for item in document.data.iterdescendants(): for item in document.data.iterdescendants():
if item in added2: if item in added2:

View File

@ -202,7 +202,6 @@ class PDFWriter(QObject): # {{{
paged_display.set_geometry(1, 0, 0, 0); paged_display.set_geometry(1, 0, 0, 0);
paged_display.layout(); paged_display.layout();
paged_display.fit_images(); paged_display.fit_images();
paged_display.check_top_margin();
''') ''')
mf = self.view.page().mainFrame() mf = self.view.page().mainFrame()
while True: while True:
@ -221,7 +220,7 @@ class PDFWriter(QObject): # {{{
self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts') self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts')
def insert_cover(self): def insert_cover(self):
if self.cover_data is None: if not isinstance(self.cover_data, bytes):
return return
item_path = os.path.join(self.tmp_path, 'cover.pdf') item_path = os.path.join(self.tmp_path, 'cover.pdf')
printer = get_pdf_printer(self.opts, output_file_name=item_path, printer = get_pdf_printer(self.opts, output_file_name=item_path,

View File

@ -139,6 +139,21 @@ class DeleteAction(InterfaceAction):
return set([]) return set([])
return set(map(self.gui.library_view.model().id, rows)) return set(map(self.gui.library_view.model().id, rows))
def remove_format_by_id(self, book_id, fmt):
title = self.gui.current_db.title(book_id, index_is_id=True)
if not confirm('<p>'+(_(
'The %(fmt)s format will be <b>permanently deleted</b> from '
'%(title)s. Are you sure?')%dict(fmt=fmt, title=title))
+'</p>', 'library_delete_specific_format', self.gui):
return
self.gui.library_view.model().db.remove_format(book_id, fmt,
index_is_id=True, notify=False)
self.gui.library_view.model().refresh_ids([book_id])
self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
self.gui.library_view.currentIndex())
self.gui.tags_view.recount()
def delete_selected_formats(self, *args): def delete_selected_formats(self, *args):
ids = self._get_selected_ids() ids = self._get_selected_ids()
if not ids: if not ids:

View File

@ -14,6 +14,7 @@ from calibre.utils.smtp import config as email_config
from calibre.constants import iswindows, isosx from calibre.constants import iswindows, isosx
from calibre.customize.ui import is_disabled from calibre.customize.ui import is_disabled
from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.bambook.driver import BAMBOOK
from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog
from calibre.gui2 import info_dialog from calibre.gui2 import info_dialog
class ShareConnMenu(QMenu): # {{{ class ShareConnMenu(QMenu): # {{{
@ -24,6 +25,7 @@ class ShareConnMenu(QMenu): # {{{
config_email = pyqtSignal() config_email = pyqtSignal()
toggle_server = pyqtSignal() toggle_server = pyqtSignal()
control_smartdevice = pyqtSignal()
dont_add_to = frozenset(['context-menu-device']) dont_add_to = frozenset(['context-menu-device'])
def __init__(self, parent=None): def __init__(self, parent=None):
@ -56,6 +58,11 @@ class ShareConnMenu(QMenu): # {{{
_('Start Content Server')) _('Start Content Server'))
self.toggle_server_action.triggered.connect(lambda x: self.toggle_server_action.triggered.connect(lambda x:
self.toggle_server.emit()) self.toggle_server.emit())
self.control_smartdevice_action = \
self.addAction(QIcon(I('dot_green.png')),
_('Control Smart Device Connections'))
self.control_smartdevice_action.triggered.connect(lambda x:
self.control_smartdevice.emit())
self.addSeparator() self.addSeparator()
self.email_actions = [] self.email_actions = []
@ -80,6 +87,9 @@ class ShareConnMenu(QMenu): # {{{
text = _('Stop Content Server') + ' [%s]'%get_external_ip() text = _('Stop Content Server') + ' [%s]'%get_external_ip()
self.toggle_server_action.setText(text) self.toggle_server_action.setText(text)
def hide_smartdevice_menus(self):
self.control_smartdevice_action.setVisible(False)
def build_email_entries(self, sync_menu): def build_email_entries(self, sync_menu):
from calibre.gui2.device import DeviceAction from calibre.gui2.device import DeviceAction
for ac in self.email_actions: for ac in self.email_actions:
@ -158,6 +168,7 @@ class ConnectShareAction(InterfaceAction):
def genesis(self): def genesis(self):
self.share_conn_menu = ShareConnMenu(self.gui) self.share_conn_menu = ShareConnMenu(self.gui)
self.share_conn_menu.toggle_server.connect(self.toggle_content_server) self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
self.share_conn_menu.control_smartdevice.connect(self.control_smartdevice)
self.share_conn_menu.config_email.connect(partial( self.share_conn_menu.config_email.connect(partial(
self.gui.iactions['Preferences'].do_config, self.gui.iactions['Preferences'].do_config,
initial_plugin=('Sharing', 'Email'))) initial_plugin=('Sharing', 'Email')))
@ -200,8 +211,21 @@ class ConnectShareAction(InterfaceAction):
if not self.stopping_msg.isVisible(): if not self.stopping_msg.isVisible():
self.stopping_msg.exec_() self.stopping_msg.exec_()
return return
self.gui.content_server = None self.gui.content_server = None
self.stopping_msg.accept() self.stopping_msg.accept()
def control_smartdevice(self):
sd_dialog = SmartdeviceDialog(self.gui)
sd_dialog.exec_()
self.set_smartdevice_icon()
def check_smartdevice_menus(self):
if not self.gui.device_manager.is_enabled('smartdevice'):
self.share_conn_menu.hide_smartdevice_menus()
def set_smartdevice_icon(self):
running = self.gui.device_manager.is_running('smartdevice')
if running:
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_green.png')))
else:
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_red.png')))

View File

@ -31,3 +31,5 @@ class PluginUpdaterAction(InterfaceAction):
d = PluginUpdaterDialog(self.gui, initial_filter=initial_filter) d = PluginUpdaterDialog(self.gui, initial_filter=initial_filter)
d.exec_() d.exec_()
if d.do_restart:
self.gui.quit(restart=True)

View File

@ -45,6 +45,8 @@ class PreferencesAction(InterfaceAction):
d = PluginUpdaterDialog(self.gui, d = PluginUpdaterDialog(self.gui,
initial_filter=FILTER_NOT_INSTALLED) initial_filter=FILTER_NOT_INSTALLED)
d.exec_() d.exec_()
if d.do_restart:
self.gui.quit(restart=True)
def do_config(self, checked=False, initial_plugin=None, def do_config(self, checked=False, initial_plugin=None,
close_after_initial=False): close_after_initial=False):

View File

@ -5,8 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, QIcon,
QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QAction,
QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu) QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu)
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
@ -382,6 +382,7 @@ class CoverView(QWidget): # {{{
class BookInfo(QWebView): class BookInfo(QWebView):
link_clicked = pyqtSignal(object) link_clicked = pyqtSignal(object)
remove_format = pyqtSignal(int, object)
def __init__(self, vertical, parent=None): def __init__(self, vertical, parent=None):
QWebView.__init__(self, parent) QWebView.__init__(self, parent)
@ -395,6 +396,16 @@ class BookInfo(QWebView):
palette.setBrush(QPalette.Base, Qt.transparent) palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette) self.page().setPalette(palette)
self.css = P('templates/book_details.css', data=True).decode('utf-8') self.css = P('templates/book_details.css', data=True).decode('utf-8')
self.remove_format_action = QAction(QIcon(I('trash.png')),
'', self)
self.remove_format_action.current_fmt = None
self.remove_format_action.triggered.connect(self.remove_format_triggerred)
def remove_format_triggerred(self):
f = self.remove_format_action.current_fmt
if f:
book_id, fmt = f
self.remove_format.emit(book_id, fmt)
def link_activated(self, link): def link_activated(self, link):
self._link_clicked = True self._link_clicked = True
@ -420,6 +431,32 @@ class BookInfo(QWebView):
else: else:
ev.ignore() ev.ignore()
def contextMenuEvent(self, ev):
p = self.page()
mf = p.mainFrame()
r = mf.hitTestContent(ev.pos())
url = unicode(r.linkUrl().toString()).strip()
menu = p.createStandardContextMenu()
ca = self.pageAction(p.Copy)
for action in list(menu.actions()):
if action is not ca:
menu.removeAction(action)
if not r.isNull() and url.startswith('format:'):
parts = url.split(':')
try:
book_id, fmt = int(parts[1]), parts[2]
except:
import traceback
traceback.print_exc()
else:
self.remove_format_action.current_fmt = (book_id, fmt)
self.remove_format_action.setText(_('Delete the %s format')%parts[
2])
menu.addAction(self.remove_format_action)
if len(menu.actions()) > 0:
menu.exec_(ev.globalPos())
# }}} # }}}
class DetailsLayout(QLayout): # {{{ class DetailsLayout(QLayout): # {{{
@ -513,6 +550,7 @@ class BookDetails(QWidget): # {{{
show_book_info = pyqtSignal() show_book_info = pyqtSignal()
open_containing_folder = pyqtSignal(int) open_containing_folder = pyqtSignal(int)
view_specific_format = pyqtSignal(int, object) view_specific_format = pyqtSignal(int, object)
remove_specific_format = pyqtSignal(int, object)
remote_file_dropped = pyqtSignal(object, object) remote_file_dropped = pyqtSignal(object, object)
files_dropped = pyqtSignal(object, object) files_dropped = pyqtSignal(object, object)
cover_changed = pyqtSignal(object, object) cover_changed = pyqtSignal(object, object)
@ -579,6 +617,7 @@ class BookDetails(QWidget): # {{{
self.book_info = BookInfo(vertical, self) self.book_info = BookInfo(vertical, self)
self._layout.addWidget(self.book_info) self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self.handle_click) self.book_info.link_clicked.connect(self.handle_click)
self.book_info.remove_format.connect(self.remove_specific_format)
self.setCursor(Qt.PointingHandCursor) self.setCursor(Qt.PointingHandCursor)
def handle_click(self, link): def handle_click(self, link):

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
# Imports {{{ # Imports {{{
import os, traceback, Queue, time, cStringIO, re, sys import os, traceback, Queue, time, cStringIO, re, sys
from threading import Thread from threading import Thread, Event
from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL, from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL,
Qt, pyqtSignal, QDialog, QObject, QVBoxLayout, Qt, pyqtSignal, QDialog, QObject, QVBoxLayout,
@ -144,6 +144,9 @@ class DeviceManager(Thread): # {{{
self.open_feedback_msg = open_feedback_msg self.open_feedback_msg = open_feedback_msg
self._device_information = None self._device_information = None
self.current_library_uuid = None self.current_library_uuid = None
self.call_shutdown_on_disconnect = False
self.devices_initialized = Event()
self.dynamic_plugins = {}
def report_progress(self, *args): def report_progress(self, *args):
pass pass
@ -197,6 +200,13 @@ class DeviceManager(Thread): # {{{
self.ejected_devices.remove(self.connected_device) self.ejected_devices.remove(self.connected_device)
else: else:
self.connected_slot(False, self.connected_device_kind) self.connected_slot(False, self.connected_device_kind)
if self.call_shutdown_on_disconnect:
# The current device is an instance of a plugin class instantiated
# to handle this connection, probably as a mounted device. We are
# now abandoning the instance that we created, so we tell it that it
# is being shut down.
self.connected_device.shutdown()
self.call_shutdown_on_disconnect = False
self.connected_device = None self.connected_device = None
self._device_information = None self._device_information = None
@ -265,7 +275,24 @@ class DeviceManager(Thread): # {{{
except Queue.Empty: except Queue.Empty:
pass pass
def run_startup(self, dev):
name = 'unknown'
try:
name = dev.__class__.__name__
dev.startup()
except:
prints('Startup method for device %s threw exception'%name)
traceback.print_exc()
def run(self): def run(self):
# Do any device-specific startup processing.
for d in self.devices:
self.run_startup(d)
n = d.is_dynamically_controllable()
if n:
self.dynamic_plugins[n] = d
self.devices_initialized.set()
while self.keep_going: while self.keep_going:
kls = None kls = None
while True: while True:
@ -277,15 +304,23 @@ class DeviceManager(Thread): # {{{
if kls is not None: if kls is not None:
try: try:
dev = kls(folder_path) dev = kls(folder_path)
# We just created a new device instance. Call its startup
# method and set the flag to call the shutdown method when
# it disconnects.
self.run_startup(dev)
self.call_shutdown_on_disconnect = True
self.do_connect([[dev, None],], device_kind=device_kind) self.do_connect([[dev, None],], device_kind=device_kind)
except: except:
prints('Unable to open %s as device (%s)'%(device_kind, folder_path)) prints('Unable to open %s as device (%s)'%(device_kind, folder_path))
traceback.print_exc() traceback.print_exc()
else: else:
self.detect_device() self.detect_device()
do_sleep = True
while True: while True:
job = self.next() job = self.next()
if job is not None: if job is not None:
do_sleep = False
self.current_job = job self.current_job = job
if self.device is not None: if self.device is not None:
self.device.set_progress_reporter(job.report_progress) self.device.set_progress_reporter(job.report_progress)
@ -293,7 +328,15 @@ class DeviceManager(Thread): # {{{
self.current_job = None self.current_job = None
else: else:
break break
time.sleep(self.sleep_time) if do_sleep:
time.sleep(self.sleep_time)
# We are exiting. Call the shutdown method for each plugin
for p in self.devices:
try:
p.shutdown()
except:
pass
def create_job_step(self, func, done, description, to_job, args=[], kwargs={}): def create_job_step(self, func, done, description, to_job, args=[], kwargs={}):
job = DeviceJob(func, done, self.job_manager, job = DeviceJob(func, done, self.job_manager,
@ -475,6 +518,44 @@ class DeviceManager(Thread): # {{{
if self.connected_device: if self.connected_device:
self.connected_device.set_driveinfo_name(location_code, name) self.connected_device.set_driveinfo_name(location_code, name)
# dynamic plugin interface
# This is a helper function that handles queueing with the device manager
def _call_request(self, name, method, *args, **kwargs):
d = self.dynamic_plugins.get(name, None)
if d:
return getattr(d, method)(*args, **kwargs)
return kwargs.get('default', None)
# The dynamic plugin methods below must be called on the GUI thread. They
# will switch to the device thread before calling the plugin.
def start_plugin(self, name):
self._call_request(name, 'start_plugin')
def stop_plugin(self, name):
self._call_request(name, 'stop_plugin')
def get_option(self, name, opt_string, default=None):
return self._call_request(name, 'get_option', opt_string, default=default)
def set_option(self, name, opt_string, opt_value):
self._call_request(name, 'set_option', opt_string, opt_value)
def is_running(self, name):
if self._call_request(name, 'is_running'):
return True
return False
def is_enabled(self, name):
try:
d = self.dynamic_plugins.get(name, None)
if d:
return True
except:
pass
return False
# }}} # }}}
class DeviceAction(QAction): # {{{ class DeviceAction(QAction): # {{{
@ -675,6 +756,7 @@ class DeviceMixin(object): # {{{
self.job_manager, Dispatcher(self.status_bar.show_message), self.job_manager, Dispatcher(self.status_bar.show_message),
Dispatcher(self.show_open_feedback)) Dispatcher(self.show_open_feedback))
self.device_manager.start() self.device_manager.start()
self.device_manager.devices_initialized.wait()
if tweaks['auto_connect_to_folder']: if tweaks['auto_connect_to_folder']:
self.connect_to_folder_named(tweaks['auto_connect_to_folder']) self.connect_to_folder_named(tweaks['auto_connect_to_folder'])

View File

@ -43,6 +43,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column) self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column) self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
if device.HIDE_FORMATS_CONFIG_BOX:
self.groupBox.hide()
if supports_subdirs: if supports_subdirs:
self.opt_use_subdirs.setChecked(self.settings.use_subdirs) self.opt_use_subdirs.setChecked(self.settings.use_subdirs)
else: else:

View File

@ -103,6 +103,19 @@
<item row="6" column="0"> <item row="6" column="0">
<layout class="QGridLayout" name="extra_layout"/> <layout class="QGridLayout" name="extra_layout"/>
</item> </item>
<item row="7" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">

View File

@ -1,7 +1,8 @@
<ui version="4.0" > <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class> <class>Dialog</class>
<widget class="QDialog" name="Dialog" > <widget class="QDialog" name="Dialog">
<property name="geometry" > <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
@ -9,51 +10,63 @@
<height>300</height> <height>300</height>
</rect> </rect>
</property> </property>
<property name="windowTitle" > <property name="windowTitle">
<string>Are you sure?</string> <string>Are you sure?</string>
</property> </property>
<property name="windowIcon" > <property name="windowIcon">
<iconset resource="../../../../resources/images.qrc" > <iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/dialog_warning.png</normaloff>:/images/dialog_warning.png</iconset> <normaloff>:/images/dialog_warning.png</normaloff>:/images/dialog_warning.png</iconset>
</property> </property>
<layout class="QGridLayout" name="gridLayout" > <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" > <item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout" > <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QLabel" name="label" > <widget class="QLabel" name="label">
<property name="pixmap" > <property name="pixmap">
<pixmap resource="../../../../resources/images.qrc" >:/images/dialog_warning.png</pixmap> <pixmap resource="../../../../resources/images.qrc">:/images/dialog_warning.png</pixmap>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="msg" > <widget class="QLabel" name="msg">
<property name="text" > <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>TextLabel</string> <string>TextLabel</string>
</property> </property>
<property name="wordWrap" > <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="0" > <item row="1" column="0">
<widget class="QCheckBox" name="again" > <widget class="QCheckBox" name="again">
<property name="text" > <property name="text">
<string>&amp;Show this warning again</string> <string>&amp;Show this warning again</string>
</property> </property>
<property name="checked" > <property name="checked">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" > <item row="2" column="0">
<widget class="QDialogButtonBox" name="buttonBox" > <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation" > <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="standardButtons" > <property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property> </property>
</widget> </widget>
@ -61,7 +74,7 @@
</layout> </layout>
</widget> </widget>
<resources> <resources>
<include location="../../../../resources/images.qrc" /> <include location="../../../../resources/images.qrc"/>
</resources> </resources>
<connections> <connections>
<connection> <connection>
@ -70,11 +83,11 @@
<receiver>Dialog</receiver> <receiver>Dialog</receiver>
<slot>accept()</slot> <slot>accept()</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel">
<x>248</x> <x>248</x>
<y>254</y> <y>254</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel">
<x>157</x> <x>157</x>
<y>274</y> <y>274</y>
</hint> </hint>
@ -86,11 +99,11 @@
<receiver>Dialog</receiver> <receiver>Dialog</receiver>
<slot>reject()</slot> <slot>reject()</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel">
<x>316</x> <x>316</x>
<y>260</y> <y>260</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel">
<x>286</x> <x>286</x>
<y>274</y> <y>274</y>
</hint> </hint>

View File

@ -9,7 +9,7 @@ import sys
from PyQt4.Qt import (QDialog, QIcon, QApplication, QSize, QKeySequence, from PyQt4.Qt import (QDialog, QIcon, QApplication, QSize, QKeySequence,
QAction, Qt, QTextBrowser, QDialogButtonBox, QVBoxLayout, QGridLayout, QAction, Qt, QTextBrowser, QDialogButtonBox, QVBoxLayout, QGridLayout,
QLabel, QPlainTextEdit, QTextDocument) QLabel, QPlainTextEdit, QTextDocument, QCheckBox, pyqtSignal)
from calibre.constants import __version__, isfrozen from calibre.constants import __version__, isfrozen
from calibre.gui2.dialogs.message_box_ui import Ui_Dialog from calibre.gui2.dialogs.message_box_ui import Ui_Dialog
@ -270,21 +270,23 @@ class ErrorNotification(MessageBox): # {{{
class JobError(QDialog): # {{{ class JobError(QDialog): # {{{
WIDTH = 600 WIDTH = 600
do_pop = pyqtSignal()
def __init__(self, gui): def __init__(self, parent):
QDialog.__init__(self, gui) QDialog.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose, False) self.setAttribute(Qt.WA_DeleteOnClose, False)
self.gui = gui
self.queue = [] self.queue = []
self.do_pop.connect(self.pop, type=Qt.QueuedConnection)
self._layout = l = QGridLayout() self._layout = l = QGridLayout()
self.setLayout(l) self.setLayout(l)
self.icon = QIcon(I('dialog_error.png')) self.icon = QIcon(I('dialog_error.png'))
self.setWindowIcon(self.icon) self.setWindowIcon(self.icon)
self.icon_label = QLabel() self.icon_label = QLabel()
self.icon_label.setPixmap(self.icon.pixmap(128, 128)) self.icon_label.setPixmap(self.icon.pixmap(68, 68))
self.icon_label.setMaximumSize(QSize(128, 128)) self.icon_label.setMaximumSize(QSize(68, 68))
self.msg_label = QLabel('<p>&nbsp;') self.msg_label = QLabel('<p>&nbsp;')
self.msg_label.setStyleSheet('QLabel { margin-top: 1ex; }')
self.msg_label.setWordWrap(True) self.msg_label.setWordWrap(True)
self.msg_label.setTextFormat(Qt.RichText) self.msg_label.setTextFormat(Qt.RichText)
self.det_msg = QPlainTextEdit(self) self.det_msg = QPlainTextEdit(self)
@ -302,15 +304,23 @@ class JobError(QDialog): # {{{
self.det_msg_toggle.clicked.connect(self.toggle_det_msg) self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
self.det_msg_toggle.setToolTip( self.det_msg_toggle.setToolTip(
_('Show detailed information about this error')) _('Show detailed information about this error'))
self.suppress = QCheckBox(self)
l.addWidget(self.icon_label, 0, 0, 1, 1) l.addWidget(self.icon_label, 0, 0, 1, 1)
l.addWidget(self.msg_label, 0, 1, 1, 1, Qt.AlignLeft|Qt.AlignTop) l.addWidget(self.msg_label, 0, 1, 1, 1)
l.addWidget(self.det_msg, 1, 0, 1, 2) l.addWidget(self.det_msg, 1, 0, 1, 2)
l.addWidget(self.suppress, 2, 0, 1, 2, Qt.AlignLeft|Qt.AlignBottom)
l.addWidget(self.bb, 2, 0, 1, 2, Qt.AlignRight|Qt.AlignBottom) l.addWidget(self.bb, 3, 0, 1, 2, Qt.AlignRight|Qt.AlignBottom)
l.setColumnStretch(1, 100)
self.setModal(False) self.setModal(False)
self.base_height = max(200, self.sizeHint().height() + 20) self.suppress.setVisible(False)
self.do_resize()
def update_suppress_state(self):
self.suppress.setText(_(
'Hide the remaining %d error messages'%len(self.queue)))
self.suppress.setVisible(len(self.queue) > 3)
self.do_resize() self.do_resize()
def copy_to_clipboard(self, *args): def copy_to_clipboard(self, *args):
@ -332,9 +342,11 @@ class JobError(QDialog): # {{{
self.do_resize() self.do_resize()
def do_resize(self): def do_resize(self):
h = self.base_height h = self.sizeHint().height()
if self.det_msg.isVisible(): self.setMinimumHeight(0) # Needed as this gets set if det_msg is shown
h += 250 # Needed otherwise re-showing the box after showing det_msg causes the box
# to not reduce in height
self.setMaximumHeight(h)
self.resize(QSize(self.WIDTH, h)) self.resize(QSize(self.WIDTH, h))
def showEvent(self, ev): def showEvent(self, ev):
@ -342,16 +354,50 @@ class JobError(QDialog): # {{{
self.bb.button(self.bb.Close).setFocus(Qt.OtherFocusReason) self.bb.button(self.bb.Close).setFocus(Qt.OtherFocusReason)
return ret return ret
def show_error(self, title, msg, det_msg=u''):
self.queue.append((title, msg, det_msg))
self.update_suppress_state()
self.pop()
def pop(self):
if not self.queue or self.isVisible(): return
title, msg, det_msg = self.queue.pop(0)
self.setWindowTitle(title)
self.msg_label.setText(msg)
self.det_msg.setPlainText(det_msg)
self.det_msg.setVisible(False)
self.det_msg_toggle.setText(self.show_det_msg)
self.det_msg_toggle.setVisible(True)
self.suppress.setChecked(False)
self.update_suppress_state()
if not det_msg:
self.det_msg_toggle.setVisible(False)
self.do_resize()
self.show()
def done(self, r):
if self.suppress.isChecked():
self.queue = []
QDialog.done(self, r)
self.do_pop.emit()
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) app = QApplication([])
from calibre.gui2.preferences import init_gui d = JobError(None)
gui = init_gui() d.show_error('test title', 'some long meaningless test message', 'det msg')
d = JobError(gui) d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show() d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show_error('test title', 'some long meaningless test message', 'det msg')
app.setQuitOnLastWindowClosed(False)
def checkd():
if not d.queue:
app.quit()
app.lastWindowClosed.connect(checkd)
app.exec_() app.exec_()
gui.shutdown()
# if __name__ == '__main__': # if __name__ == '__main__':
# app = QApplication([]) # app = QApplication([])

View File

@ -456,6 +456,7 @@ class PluginUpdaterDialog(SizePersistedDialog):
self.gui = gui self.gui = gui
self.forum_link = None self.forum_link = None
self.model = None self.model = None
self.do_restart = False
self._initialize_controls() self._initialize_controls()
self._create_context_menu() self._create_context_menu()
@ -720,6 +721,7 @@ class PluginUpdaterDialog(SizePersistedDialog):
prints('Installing plugin: ', zip_path) prints('Installing plugin: ', zip_path)
self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path) self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path)
do_restart = False
try: try:
try: try:
plugin = add_plugin(zip_path) plugin = add_plugin(zip_path)
@ -731,11 +733,21 @@ class PluginUpdaterDialog(SizePersistedDialog):
widget.gui = self.gui widget.gui = self.gui
widget.check_for_add_to_toolbars(plugin) widget.check_for_add_to_toolbars(plugin)
self.gui.status_bar.showMessage(_('Plugin installed: %s') % display_plugin.name) self.gui.status_bar.showMessage(_('Plugin installed: %s') % display_plugin.name)
info_dialog(self.gui, _('Success'), d = info_dialog(self.gui, _('Success'),
_('Plugin <b>{0}</b> successfully installed under <b>' _('Plugin <b>{0}</b> successfully installed under <b>'
' {1} plugins</b>. You may have to restart calibre ' ' {1} plugins</b>. You may have to restart calibre '
'for the plugin to take effect.').format(plugin.name, plugin.type), 'for the plugin to take effect.').format(plugin.name, plugin.type),
show=True, show_copy_button=False) show_copy_button=False)
b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole)
b.setIcon(QIcon(I('lt.png')))
d.do_restart = False
def rf():
d.do_restart = True
b.clicked.connect(rf)
d.set_details('')
d.exec_()
b.clicked.disconnect()
do_restart = d.do_restart
display_plugin.plugin = plugin display_plugin.plugin = plugin
# We cannot read the 'actual' version information as the plugin will not be loaded yet # We cannot read the 'actual' version information as the plugin will not be loaded yet
@ -762,6 +774,9 @@ class PluginUpdaterDialog(SizePersistedDialog):
else: else:
self.model.refresh_plugin(display_plugin) self.model.refresh_plugin(display_plugin)
self._select_and_focus_view(change_selection=False) self._select_and_focus_view(change_selection=False)
if do_restart:
self.do_restart = True
self.accept()
def _history_clicked(self): def _history_clicked(self):
display_plugin = self._selected_display_plugin() display_plugin = self._selected_display_plugin()

View File

@ -12,6 +12,7 @@ from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
box_values = {} box_values = {}
last_matchkind = CONTAINS_MATCH
class SearchDialog(QDialog, Ui_Dialog): class SearchDialog(QDialog, Ui_Dialog):
@ -57,6 +58,9 @@ class SearchDialog(QDialog, Ui_Dialog):
current_tab = gprefs.get('advanced search dialog current tab', 0) current_tab = gprefs.get('advanced search dialog current tab', 0)
self.tabWidget.setCurrentIndex(current_tab) self.tabWidget.setCurrentIndex(current_tab)
if current_tab == 1:
self.matchkind.setCurrentIndex(last_matchkind)
self.tabWidget.currentChanged[int].connect(self.tab_changed) self.tabWidget.currentChanged[int].connect(self.tab_changed)
self.tab_changed(current_tab) self.tab_changed(current_tab)
@ -173,7 +177,9 @@ class SearchDialog(QDialog, Ui_Dialog):
general_index = unicode(self.general_combo.currentText()) general_index = unicode(self.general_combo.currentText())
self.box_last_values['general_index'] = general_index self.box_last_values['general_index'] = general_index
global box_values global box_values
global last_matchkind
box_values = copy.deepcopy(self.box_last_values) box_values = copy.deepcopy(self.box_last_values)
last_matchkind = mk
if general: if general:
ans.append(unicode(self.general_combo.currentText()) + ':"' + ans.append(unicode(self.general_combo.currentText()) + ':"' +
self.mc + general + '"') self.mc + general + '"')

View File

@ -0,0 +1,80 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtGui import QDialog, QLineEdit
from PyQt4.QtCore import SIGNAL, Qt
from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog
class SmartdeviceDialog(QDialog, Ui_Dialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
Ui_Dialog.__init__(self)
self.setupUi(self)
self.msg.setText(
_('This dialog starts and stops the smart device app interface. '
'When you start the interface, you might see some messages from '
'your computer\'s firewall or anti-virus manager asking you '
'if it is OK for calibre to connect to the network. <B>Please '
'answer yes</b>. If you do not, the app will not work. It will '
'be unable to connect to calibre.'))
self.password_box.setToolTip('<p>' +
_('Use a password if calibre is running on a network that '
'is not secure. For example, if you run calibre on a laptop, '
'use that laptop in an airport, and want to connect your '
'smart device to calibre, you should use a password.') + '</p>')
self.run_box.setToolTip('<p>' +
_('Check this box to allow calibre to accept connections from the '
'smart device. Uncheck the box to prevent connections.') + '</p>')
self.autostart_box.setToolTip('<p>' +
_('Check this box if you want calibre to automatically start the '
'smart device interface when calibre starts. You should not do '
'this if you are using a network that is not secure and you '
'are not setting a password.') + '</p>')
self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password)
self.autostart_box.stateChanged.connect(self.autostart_changed)
self.device_manager = parent.device_manager
if self.device_manager.is_running('smartdevice'):
self.run_box.setChecked(True)
else:
self.run_box.setChecked(False)
if self.device_manager.get_option('smartdevice', 'autostart'):
self.autostart_box.setChecked(True)
self.run_box.setChecked(True)
self.run_box.setEnabled(False)
pw = self.device_manager.get_option('smartdevice', 'password')
if pw:
self.password_box.setText(pw)
def autostart_changed(self):
if self.autostart_box.isChecked():
self.run_box.setChecked(True)
self.run_box.setEnabled(False)
else:
self.run_box.setEnabled(True)
def toggle_password(self, state):
if state == Qt.Unchecked:
self.password_box.setEchoMode(QLineEdit.Password)
else:
self.password_box.setEchoMode(QLineEdit.Normal)
def accept(self):
self.device_manager.set_option('smartdevice', 'password',
unicode(self.password_box.text()))
self.device_manager.set_option('smartdevice', 'autostart',
self.autostart_box.isChecked())
if self.run_box.isChecked():
self.device_manager.start_plugin('smartdevice')
else:
self.device_manager.stop_plugin('smartdevice')
QDialog.accept(self)

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>209</height>
</rect>
</property>
<property name="windowTitle">
<string>Smart device control</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
</property>
<layout class="QGridLayout">
<item row="4" column="1">
<widget class="QCheckBox" name="autostart_box">
<property name="text">
<string>&amp;Automatically allow connections at startup</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="label_43">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Optional password for security</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="run_box">
<property name="text">
<string>&amp;Allow connections</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="msg">
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Password:</string>
</property>
<property name="buddy">
<cstring>password_box</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="show_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -265,6 +265,8 @@ class LayoutMixin(object): # {{{
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id) self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id)
self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id) self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)
self.book_details.remove_specific_format.connect(
self.iactions['Remove Books'].remove_format_by_id)
m = self.library_view.model() m = self.library_view.model()
if m.rowCount(None) > 0: if m.rowCount(None) > 0:

View File

@ -135,7 +135,8 @@ class GuiRunner(QObject):
main = Main(self.opts, gui_debug=self.gui_debug) main = Main(self.opts, gui_debug=self.gui_debug)
if self.splash_screen is not None: if self.splash_screen is not None:
self.splash_screen.showMessage(_('Initializing user interface...')) self.splash_screen.showMessage(_('Initializing user interface...'))
main.initialize(self.library_path, db, self.listener, self.actions) with gprefs: # Only write gui.json after initialization is complete
main.initialize(self.library_path, db, self.listener, self.actions)
if self.splash_screen is not None: if self.splash_screen is not None:
self.splash_screen.finish(main) self.splash_screen.finish(main)
if DEBUG: if DEBUG:

View File

@ -236,6 +236,7 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
''' '''
changed_signal = pyqtSignal() changed_signal = pyqtSignal()
restart_now = pyqtSignal()
supports_restoring_to_defaults = True supports_restoring_to_defaults = True
restart_critical = False restart_critical = False

View File

@ -290,6 +290,7 @@ class Preferences(QMainWindow):
self.apply_action.setEnabled(False) self.apply_action.setEnabled(False)
self.showing_widget.changed_signal.connect(lambda : self.showing_widget.changed_signal.connect(lambda :
self.apply_action.setEnabled(True)) self.apply_action.setEnabled(True))
self.showing_widget.restart_now.connect(self.restart_now)
self.restore_action.setEnabled(self.showing_widget.supports_restoring_to_defaults) self.restore_action.setEnabled(self.showing_widget.supports_restoring_to_defaults)
tt = self.showing_widget.restore_defaults_desc tt = self.showing_widget.restore_defaults_desc
if not self.restore_action.isEnabled(): if not self.restore_action.isEnabled():
@ -319,6 +320,15 @@ class Preferences(QMainWindow):
elif self.stack.currentIndex() == 0: elif self.stack.currentIndex() == 0:
self.close() self.close()
def restart_now(self):
try:
self.showing_widget.commit()
except AbortCommit:
return
self.hide_plugin()
self.close()
self.gui.quit(restart=True)
def commit(self, *args): def commit(self, *args):
try: try:
must_restart = self.showing_widget.commit() must_restart = self.showing_widget.commit()

View File

@ -384,6 +384,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self._plugin_model.populate() self._plugin_model.populate()
self._plugin_model.reset() self._plugin_model.reset()
self.changed_signal.emit() self.changed_signal.emit()
if d.do_restart:
self.restart_now.emit()
def reload_store_plugins(self): def reload_store_plugins(self):
self.gui.load_store_plugins() self.gui.load_store_plugins()

View File

@ -44,6 +44,7 @@ from calibre.gui2.keyboard import Manager
from calibre.gui2.auto_add import AutoAdder from calibre.gui2.auto_add import AutoAdder
from calibre.library.sqlite import sqlite, DatabaseException from calibre.library.sqlite import sqlite, DatabaseException
from calibre.gui2.proceed import ProceedQuestion from calibre.gui2.proceed import ProceedQuestion
from calibre.gui2.dialogs.message_box import JobError
class Listener(Thread): # {{{ class Listener(Thread): # {{{
@ -111,6 +112,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.proceed_requested.connect(self.do_proceed, self.proceed_requested.connect(self.do_proceed,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.proceed_question = ProceedQuestion(self) self.proceed_question = ProceedQuestion(self)
self.job_error_dialog = JobError(self)
self.keyboard = Manager(self) self.keyboard = Manager(self)
_gui = self _gui = self
self.opts = opts self.opts = opts
@ -337,6 +339,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if config['autolaunch_server']: if config['autolaunch_server']:
self.start_content_server() self.start_content_server()
smartdevice_actions = self.iactions['Connect Share']
smartdevice_actions.check_smartdevice_menus()
if self.device_manager.get_option('smartdevice', 'autostart'):
try:
self.device_manager.start_plugin('smartdevice')
except:
pass
smartdevice_actions.set_smartdevice_icon()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
self.read_settings() self.read_settings()
@ -358,6 +369,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.keyboard.finalize() self.keyboard.finalize()
self.auto_adder = AutoAdder(gprefs['auto_add_path'], self) self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)
self.save_layout_state()
# Collect cycles now # Collect cycles now
gc.collect() gc.collect()
@ -679,12 +692,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except: except:
pass pass
if not minz: if not minz:
d = error_dialog(self, dialog_title, self.job_error_dialog.show_error(dialog_title,
_('<b>Failed</b>')+': '+unicode(job.description), _('<b>Failed</b>')+': '+unicode(job.description),
det_msg=job.details) det_msg=job.details)
d.setModal(False)
d.show()
self._modeless_dialogs.append(d)
def read_settings(self): def read_settings(self):
geometry = config['main_window_geometry'] geometry = config['main_window_geometry']
@ -693,9 +703,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.read_layout_settings() self.read_layout_settings()
def write_settings(self): def write_settings(self):
config.set('main_window_geometry', self.saveGeometry()) with gprefs: # Only write to gprefs once
dynamic.set('sort_history', self.library_view.model().sort_history) config.set('main_window_geometry', self.saveGeometry())
self.save_layout_state() dynamic.set('sort_history', self.library_view.model().sort_history)
self.save_layout_state()
def quit(self, checked=True, restart=False, debug_on_restart=False, def quit(self, checked=True, restart=False, debug_on_restart=False,
confirm_quit=True): confirm_quit=True):

View File

@ -175,6 +175,8 @@ class UpdateMixin(object):
d = PluginUpdaterDialog(self, d = PluginUpdaterDialog(self,
initial_filter=FILTER_UPDATE_AVAILABLE) initial_filter=FILTER_UPDATE_AVAILABLE)
d.exec_() d.exec_()
if d.do_restart:
self.quit(restart=True)
def plugin_update_found(self, number_of_updates): def plugin_update_found(self, number_of_updates):
# Change the plugin icon to indicate there are updates available # Change the plugin icon to indicate there are updates available

View File

@ -11,7 +11,7 @@ from functools import partial
from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, pyqtProperty, from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, pyqtProperty,
QPainter, QPalette, QBrush, QFontDatabase, QDialog, QColor, QPoint, QPainter, QPalette, QBrush, QFontDatabase, QDialog, QColor, QPoint,
QImage, QRegion, QIcon, pyqtSignature, QAction, QMenu, QString, QImage, QRegion, QIcon, pyqtSignature, QAction, QMenu, QString,
pyqtSignal, QSwipeGesture, QApplication) pyqtSignal, QSwipeGesture, QApplication, pyqtSlot)
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from calibre.gui2.viewer.flip import SlideFlip from calibre.gui2.viewer.flip import SlideFlip
@ -34,6 +34,8 @@ def load_builtin_fonts():
class Document(QWebPage): # {{{ class Document(QWebPage): # {{{
page_turn = pyqtSignal(object)
def set_font_settings(self): def set_font_settings(self):
opts = config().parse() opts = config().parse()
settings = self.settings() settings = self.settings()
@ -73,7 +75,6 @@ class Document(QWebPage): # {{{
self.loaded_javascript = False self.loaded_javascript = False
self.js_loader = JavaScriptLoader( self.js_loader = JavaScriptLoader(
dynamic_coffeescript=self.debug_javascript) dynamic_coffeescript=self.debug_javascript)
self.initial_left_margin = self.initial_right_margin = u''
self.in_fullscreen_mode = False self.in_fullscreen_mode = False
self.setLinkDelegationPolicy(self.DelegateAllLinks) self.setLinkDelegationPolicy(self.DelegateAllLinks)
@ -172,6 +173,10 @@ class Document(QWebPage): # {{{
if not isxp and self.hyphenate and getattr(self, 'loaded_lang', ''): if not isxp and self.hyphenate and getattr(self, 'loaded_lang', ''):
self.javascript('do_hyphenation("%s")'%self.loaded_lang) self.javascript('do_hyphenation("%s")'%self.loaded_lang)
@pyqtSlot(int)
def page_turn_requested(self, backwards):
self.page_turn.emit(bool(backwards))
def _pass_json_value_getter(self): def _pass_json_value_getter(self):
val = json.dumps(self.bridge_value) val = json.dumps(self.bridge_value)
return QString(val) return QString(val)
@ -187,14 +192,11 @@ class Document(QWebPage): # {{{
self.set_bottom_padding(0) self.set_bottom_padding(0)
self.fit_images() self.fit_images()
self.init_hyphenate() self.init_hyphenate()
self.initial_left_margin = unicode(self.javascript( self.javascript('full_screen.save_margins()')
'document.body.style.marginLeft').toString())
self.initial_right_margin = unicode(self.javascript(
'document.body.style.marginRight').toString())
if self.in_paged_mode:
self.switch_to_paged_mode()
if self.in_fullscreen_mode: if self.in_fullscreen_mode:
self.switch_to_fullscreen_mode() self.switch_to_fullscreen_mode()
if self.in_paged_mode:
self.switch_to_paged_mode()
self.read_anchor_positions(use_cache=False) self.read_anchor_positions(use_cache=False)
self.first_load = False self.first_load = False
@ -241,7 +243,6 @@ class Document(QWebPage): # {{{
sz.setWidth(scroll_width+side_margin) sz.setWidth(scroll_width+side_margin)
self.setPreferredContentsSize(sz) self.setPreferredContentsSize(sz)
self.javascript('window.paged_display.fit_images()') self.javascript('window.paged_display.fit_images()')
self.javascript('window.paged_display.check_top_margin()')
@property @property
def column_boundaries(self): def column_boundaries(self):
@ -257,27 +258,13 @@ class Document(QWebPage): # {{{
def switch_to_fullscreen_mode(self): def switch_to_fullscreen_mode(self):
self.in_fullscreen_mode = True self.in_fullscreen_mode = True
if self.in_paged_mode: self.javascript('full_screen.on(%d, %s)'%(self.max_fs_width,
self.javascript('paged_display.max_col_width = %d'%self.max_fs_width) 'true' if self.in_paged_mode else 'false'))
else:
self.javascript('''
var s = document.body.style;
s.maxWidth = "%dpx";
s.marginLeft = "auto";
s.marginRight = "auto";
'''%self.max_fs_width)
def switch_to_window_mode(self): def switch_to_window_mode(self):
self.in_fullscreen_mode = False self.in_fullscreen_mode = False
if self.in_paged_mode: self.javascript('full_screen.off(%s)'%('true' if self.in_paged_mode
self.javascript('paged_display.max_col_width = %d'%-1) else 'false'))
else:
self.javascript('''
var s = document.body.style;
s.maxWidth = "none";
s.marginLeft = "%s";
s.marginRight = "%s";
'''%(self.initial_left_margin, self.initial_right_margin))
@pyqtSignature("QString") @pyqtSignature("QString")
def debug(self, msg): def debug(self, msg):
@ -463,6 +450,7 @@ class DocumentView(QWebView): # {{{
self.connect(self.document, SIGNAL('selectionChanged()'), self.selection_changed) self.connect(self.document, SIGNAL('selectionChanged()'), self.selection_changed)
self.connect(self.document, SIGNAL('animated_scroll_done()'), self.connect(self.document, SIGNAL('animated_scroll_done()'),
self.animated_scroll_done, Qt.QueuedConnection) self.animated_scroll_done, Qt.QueuedConnection)
self.document.page_turn.connect(self.page_turn_requested)
copy_action = self.pageAction(self.document.Copy) copy_action = self.pageAction(self.document.Copy)
copy_action.setIcon(QIcon(I('convert.png'))) copy_action.setIcon(QIcon(I('convert.png')))
d = self.document d = self.document
@ -896,6 +884,12 @@ class DocumentView(QWebView): # {{{
self.manager.scrolled(self.scroll_fraction) self.manager.scrolled(self.scroll_fraction)
#print 'After all:', self.document.ypos #print 'After all:', self.document.ypos
def page_turn_requested(self, backwards):
if backwards:
self.previous_page()
else:
self.next_page()
def scroll_by(self, x=0, y=0, notify=True): def scroll_by(self, x=0, y=0, notify=True):
old_pos = (self.document.xpos if self.document.in_paged_mode else old_pos = (self.document.xpos if self.document.in_paged_mode else
self.document.ypos) self.document.ypos)

View File

@ -32,10 +32,12 @@ class JavaScriptLoader(object):
'indexing':'ebooks.oeb.display.indexing', 'indexing':'ebooks.oeb.display.indexing',
'paged':'ebooks.oeb.display.paged', 'paged':'ebooks.oeb.display.paged',
'utils':'ebooks.oeb.display.utils', 'utils':'ebooks.oeb.display.utils',
'fs':'ebooks.oeb.display.full_screen',
} }
ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images', ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images',
'hyphenation', 'hyphenator', 'utils', 'cfi', 'indexing', 'paged') 'hyphenation', 'hyphenator', 'utils', 'cfi', 'indexing', 'paged',
'fs')
def __init__(self, dynamic_coffeescript=False): def __init__(self, dynamic_coffeescript=False):

View File

@ -272,9 +272,11 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
<h1>%s</h1> <h1>%s</h1>
<h3>%s</h3> <h3>%s</h3>
<h3>%s</h3> <h3>%s</h3>
<h3>%s</h3>
</center> </center>
'''%(_('Full screen mode'), '''%(_('Full screen mode'),
_('Right click to show controls'), _('Right click to show controls'),
_('Tap in the left or right page margin to turn pages'),
_('Press Esc to quit')), _('Press Esc to quit')),
self) self)
self.full_screen_label.setVisible(False) self.full_screen_label.setVisible(False)
@ -496,7 +498,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
a.setStartValue(QSize(width, 0)) a.setStartValue(QSize(width, 0))
a.setEndValue(QSize(width, height)) a.setEndValue(QSize(width, height))
a.start() a.start()
QTimer.singleShot(2750, self.full_screen_label.hide) QTimer.singleShot(3500, self.full_screen_label.hide)
self.view.document.switch_to_fullscreen_mode() self.view.document.switch_to_fullscreen_mode()
if self.view.document.fullscreen_clock: if self.view.document.fullscreen_clock:
self.show_clock() self.show_clock()

View File

@ -71,7 +71,6 @@ class Printing(QObject):
paged_display.set_geometry(1, 0, 0, 0); paged_display.set_geometry(1, 0, 0, 0);
paged_display.layout(); paged_display.layout();
paged_display.fit_images(); paged_display.fit_images();
paged_display.check_top_margin();
''') ''')
while True: while True:

View File

@ -352,6 +352,14 @@ class ResultCache(SearchQueryParser): # {{{
'<=':[2, relop_le] '<=':[2, relop_le]
} }
local_today = ('_today', icu_lower(_('today')))
local_yesterday = ('_yesterday', icu_lower(_('yesterday')))
local_thismonth = ('_thismonth', icu_lower(_('thismonth')))
local_daysago = icu_lower(_('daysago'))
local_daysago_len = len(local_daysago)
untrans_daysago = '_daysago'
untrans_daysago_len = len('_daysago')
def get_dates_matches(self, location, query, candidates): def get_dates_matches(self, location, query, candidates):
matches = set([]) matches = set([])
if len(query) < 2: if len(query) < 2:
@ -390,17 +398,24 @@ class ResultCache(SearchQueryParser): # {{{
if relop is None: if relop is None:
(p, relop) = self.date_search_relops['='] (p, relop) = self.date_search_relops['=']
if query == _('today'): if query in self.local_today:
qd = now() qd = now()
field_count = 3 field_count = 3
elif query == _('yesterday'): elif query in self.local_yesterday:
qd = now() - timedelta(1) qd = now() - timedelta(1)
field_count = 3 field_count = 3
elif query == _('thismonth'): elif query in self.local_thismonth:
qd = now() qd = now()
field_count = 2 field_count = 2
elif query.endswith(_('daysago')): elif query.endswith(self.local_daysago):
num = query[0:-len(_('daysago'))] num = query[0:-self.local_daysago_len]
try:
qd = now() - timedelta(int(num))
except:
raise ParseException(query, len(query), 'Number conversion error', self)
field_count = 3
elif query.endswith(self.untrans_daysago):
num = query[0:-self.untrans_daysago_len]
try: try:
qd = now() - timedelta(int(num)) qd = now() - timedelta(int(num))
except: except:
@ -591,14 +606,23 @@ class ResultCache(SearchQueryParser): # {{{
query = icu_lower(query) query = icu_lower(query)
return matchkind, query return matchkind, query
local_no = icu_lower(_('no'))
local_yes = icu_lower(_('yes'))
local_unchecked = icu_lower(_('unchecked'))
local_checked = icu_lower(_('checked'))
local_empty = icu_lower(_('empty'))
local_blank = icu_lower(_('blank'))
local_bool_values = (
local_no, local_unchecked, '_no', 'false',
local_yes, local_checked, '_yes', 'true',
local_empty, local_blank, '_empty')
def get_bool_matches(self, location, query, candidates): def get_bool_matches(self, location, query, candidates):
bools_are_tristate = self.db_prefs.get('bools_are_tristate') bools_are_tristate = self.db_prefs.get('bools_are_tristate')
loc = self.field_metadata[location]['rec_index'] loc = self.field_metadata[location]['rec_index']
matches = set() matches = set()
query = icu_lower(query) query = icu_lower(query)
if query not in (_('no'), _('unchecked'), '_no', 'false', if query not in self.local_bool_values:
_('yes'), _('checked'), '_yes', 'true',
_('empty'), _('blank'), '_empty'):
raise ParseException(_('Invalid boolean query "{0}"').format(query)) raise ParseException(_('Invalid boolean query "{0}"').format(query))
for id_ in candidates: for id_ in candidates:
item = self._data[id_] item = self._data[id_]
@ -608,20 +632,20 @@ class ResultCache(SearchQueryParser): # {{{
val = force_to_bool(item[loc]) val = force_to_bool(item[loc])
if not bools_are_tristate: if not bools_are_tristate:
if val is None or not val: # item is None or set to false if val is None or not val: # item is None or set to false
if query in [_('no'), _('unchecked'), '_no', 'false']: if query in (self.local_no, self.local_unchecked, '_no', 'false'):
matches.add(item[0]) matches.add(item[0])
else: # item is explicitly set to true else: # item is explicitly set to true
if query in [_('yes'), _('checked'), '_yes', 'true']: if query in (self.local_yes, self.local_checked, '_yes', 'true'):
matches.add(item[0]) matches.add(item[0])
else: else:
if val is None: if val is None:
if query in [_('empty'), _('blank'), '_empty', 'false']: if query in (self.local_empty, self.local_blank, '_empty', 'false'):
matches.add(item[0]) matches.add(item[0])
elif not val: # is not None and false elif not val: # is not None and false
if query in [_('no'), _('unchecked'), '_no', 'true']: if query in (self.local_no, self.local_unchecked, '_no', 'true'):
matches.add(item[0]) matches.add(item[0])
else: # item is not None and true else: # item is not None and true
if query in [_('yes'), _('checked'), '_yes', 'true']: if query in (self.local_yes, self.local_checked, '_yes', 'true'):
matches.add(item[0]) matches.add(item[0])
return matches return matches

View File

@ -17,7 +17,7 @@ from calibre.utils.date import fromtimestamp
from calibre.library.server import listen_on, log_access_file, log_error_file from calibre.library.server import listen_on, log_access_file, log_error_file
from calibre.library.server.utils import expose, AuthController from calibre.library.server.utils import expose, AuthController
from calibre.utils.mdns import publish as publish_zeroconf, \ from calibre.utils.mdns import publish as publish_zeroconf, \
stop_server as stop_zeroconf, get_external_ip unpublish as unpublish_zeroconf, get_external_ip
from calibre.library.server.content import ContentServer from calibre.library.server.content import ContentServer
from calibre.library.server.mobile import MobileServer from calibre.library.server.mobile import MobileServer
from calibre.library.server.xml import XMLServer from calibre.library.server.xml import XMLServer
@ -78,13 +78,18 @@ class BonJour(SimplePlugin): # {{{
SimplePlugin.__init__(self, engine) SimplePlugin.__init__(self, engine)
self.port = port self.port = port
self.prefix = prefix self.prefix = prefix
self.mdns_services = [
('Books in calibre', '_stanza._tcp', self.port,
{'path':self.prefix+'/stanza'}),
('Books in calibre', '_calibre._tcp', self.port,
{'path':self.prefix+'/opds'}),
]
def start(self): def start(self):
try: try:
publish_zeroconf('Books in calibre', '_stanza._tcp', for s in self.mdns_services:
self.port, {'path':self.prefix+'/stanza'}) publish_zeroconf(*s)
publish_zeroconf('Books in calibre', '_calibre._tcp',
self.port, {'path':self.prefix+'/opds'})
except: except:
import traceback import traceback
cherrypy.log.error('Failed to start BonJour:') cherrypy.log.error('Failed to start BonJour:')
@ -94,7 +99,8 @@ class BonJour(SimplePlugin): # {{{
def stop(self): def stop(self):
try: try:
stop_zeroconf() for s in self.mdns_services:
unpublish_zeroconf(*s)
except: except:
import traceback import traceback
cherrypy.log.error('Failed to stop BonJour:') cherrypy.log.error('Failed to stop BonJour:')

File diff suppressed because it is too large Load Diff

View File

@ -871,6 +871,8 @@ class Engine(threading.Thread):
from calibre.constants import DEBUG from calibre.constants import DEBUG
try: try:
rr, wr, er = select.select(rs, [], [], self.timeout) rr, wr, er = select.select(rs, [], [], self.timeout)
if globals()['_GLOBAL_DONE']:
continue
for socket in rr: for socket in rr:
try: try:
self.readers[socket].handle_read() self.readers[socket].handle_read()
@ -953,11 +955,16 @@ class Reaper(threading.Thread):
return return
if globals()['_GLOBAL_DONE']: if globals()['_GLOBAL_DONE']:
return return
now = currentTimeMillis() try:
for record in self.zeroconf.cache.entries(): # can get here in a race condition with shutdown. Swallow the
if record.isExpired(now): # exception and run around the loop again.
self.zeroconf.updateRecord(now, record) now = currentTimeMillis()
self.zeroconf.cache.remove(record) for record in self.zeroconf.cache.entries():
if record.isExpired(now):
self.zeroconf.updateRecord(now, record)
self.zeroconf.cache.remove(record)
except:
pass
class ServiceBrowser(threading.Thread): class ServiceBrowser(threading.Thread):
@ -1419,6 +1426,9 @@ class Zeroconf(object):
i += 1 i += 1
nextTime += _UNREGISTER_TIME nextTime += _UNREGISTER_TIME
def countRegisteredServices(self):
return len(self.services)
def checkService(self, info): def checkService(self, info):
"""Checks the network for a unique service name, modifying the """Checks the network for a unique service name, modifying the
ServiceInfo passed in if it is not unique.""" ServiceInfo passed in if it is not unique."""

View File

@ -240,6 +240,7 @@ class XMLConfig(dict):
def __init__(self, rel_path_to_cf_file): def __init__(self, rel_path_to_cf_file):
dict.__init__(self) dict.__init__(self)
self.no_commit = False
self.defaults = {} self.defaults = {}
self.file_path = os.path.join(config_dir, self.file_path = os.path.join(config_dir,
*(rel_path_to_cf_file.split('/'))) *(rel_path_to_cf_file.split('/')))
@ -304,6 +305,7 @@ class XMLConfig(dict):
self.commit() self.commit()
def commit(self): def commit(self):
if self.no_commit: return
if hasattr(self, 'file_path') and self.file_path: if hasattr(self, 'file_path') and self.file_path:
dpath = os.path.dirname(self.file_path) dpath = os.path.dirname(self.file_path)
if not os.path.exists(dpath): if not os.path.exists(dpath):
@ -314,6 +316,13 @@ class XMLConfig(dict):
f.truncate() f.truncate()
f.write(raw) f.write(raw)
def __enter__(self):
self.no_commit = True
def __exit__(self, *args):
self.no_commit = False
self.commit()
def to_json(obj): def to_json(obj):
if isinstance(obj, bytearray): if isinstance(obj, bytearray):
return {'__class__': 'bytearray', return {'__class__': 'bytearray',

View File

@ -47,18 +47,8 @@ def start_server():
return _server return _server
def publish(desc, type, port, properties=None, add_hostname=True): def create_service(desc, type, port, properties, add_hostname):
'''
Publish a service.
:param desc: Description of service
:param type: Name and type of service. For example _stanza._tcp
:param port: Port the service listens on
:param properties: An optional dictionary whose keys and values will be put
into the TXT record.
'''
port = int(port) port = int(port)
server = start_server()
try: try:
hostname = socket.gethostname().partition('.')[0] hostname = socket.gethostname().partition('.')[0]
except: except:
@ -69,13 +59,39 @@ def publish(desc, type, port, properties=None, add_hostname=True):
local_ip = get_external_ip() local_ip = get_external_ip()
type = type+'.local.' type = type+'.local.'
from calibre.utils.Zeroconf import ServiceInfo from calibre.utils.Zeroconf import ServiceInfo
service = ServiceInfo(type, desc+'.'+type, return ServiceInfo(type, desc+'.'+type,
address=socket.inet_aton(local_ip), address=socket.inet_aton(local_ip),
port=port, port=port,
properties=properties, properties=properties,
server=hostname+'.local.') server=hostname+'.local.')
def publish(desc, type, port, properties=None, add_hostname=True):
'''
Publish a service.
:param desc: Description of service
:param type: Name and type of service. For example _stanza._tcp
:param port: Port the service listens on
:param properties: An optional dictionary whose keys and values will be put
into the TXT record.
'''
server = start_server()
service = create_service(desc, type, port, properties, add_hostname)
server.registerService(service) server.registerService(service)
def unpublish(desc, type, port, properties=None, add_hostname=True):
'''
Unpublish a service.
The parameters must be the same as used in the corresponding call to publish
'''
server = start_server()
service = create_service(desc, type, port, properties, add_hostname)
server.unregisterService(service)
if server.countRegisteredServices() == 0:
stop_server()
def stop_server(): def stop_server():
global _server global _server
if _server is not None: if _server is not None: