mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
0.8.62
This commit is contained in:
commit
5921a12390
@ -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
17
manual/develop.rst
Executable file → Normal 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
|
||||||
------------------------------
|
------------------------------
|
||||||
|
@ -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/')]
|
||||||
|
|
||||||
|
@ -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;"> 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;"> 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
38
recipes/conowego_pl.recipe
Executable 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
32
recipes/dot_net.recipe
Normal 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 world’s best-selling magazine for web designers and developers, featuring tutorials from leading agencies, interviews with the web’s 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'),
|
||||||
|
]
|
@ -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
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
BIN
recipes/icons/conowego_pl.png
Normal file
BIN
recipes/icons/conowego_pl.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 694 B |
BIN
recipes/icons/linux_journal.png
Normal file
BIN
recipes/icons/linux_journal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 443 B |
36
recipes/linux_journal.recipe
Executable file
36
recipes/linux_journal.recipe
Executable 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
33
recipes/list_apart.recipe
Normal 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'),
|
||||||
|
]
|
@ -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')]
|
||||||
|
@ -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
|
|
||||||
|
@ -6,20 +6,30 @@ class SanFranciscoBayGuardian(BasicNewsRecipe):
|
|||||||
__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
|
||||||
|
@ -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'
|
||||||
|
__copyright__ = u'2012, Marc Busqué <marc@lamarciana.com>'
|
||||||
|
title = u'Smashing Magazine'
|
||||||
|
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.'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
max_articles_per_feed = 100
|
tags = 'web development, software'
|
||||||
|
oldest_article = 7
|
||||||
|
remove_empty_feeds = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
encoding = 'utf8'
|
||||||
publisher = 'Smashing Magazine'
|
cover_url = u'http://media.smashingmagazine.com/themes/smashingv4/images/logo.png'
|
||||||
category = 'news, web, IT, css, javascript, html'
|
remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height', 'style']
|
||||||
encoding = 'utf-8'
|
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
|
|
||||||
|
@ -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'
|
|
||||||
|
title = 'Smithsonian Magazine'
|
||||||
|
__author__ = 'Rick Shang'
|
||||||
|
|
||||||
|
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.'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
__author__ = 'Krittika Goyal and TerminalVeracity'
|
category = 'news'
|
||||||
oldest_article = 31#days
|
encoding = 'UTF-8'
|
||||||
max_articles_per_feed = 50
|
keep_only_tags = [dict(attrs={'id':['articleTitle', 'subHead', 'byLine', 'articleImage', 'article-text']})]
|
||||||
use_embedded_content = False
|
remove_tags = [dict(attrs={'class':['related-articles-inpage', 'viewMorePhotos']})]
|
||||||
recursions = 1
|
no_javascript = True
|
||||||
cover_url = 'http://sphotos.xx.fbcdn.net/hphotos-snc7/431147_10150602715983253_764313347_n.jpg'
|
no_stylesheets = True
|
||||||
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}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
remove_stylesheets = True
|
#Go to the main body
|
||||||
remove_tags_after = dict(name='div', attrs={'class':['post','articlePaginationWrapper']})
|
div = soup.find ('div', attrs={'id':'content-inset'})
|
||||||
remove_tags = [
|
|
||||||
dict(name='iframe'),
|
|
||||||
dict(name='div', attrs={'class':['article_sidebar_border','viewMorePhotos','addtoany_share_save_container','meta','social','OUTBRAIN','related-articles-inpage']}),
|
|
||||||
dict(name='div', attrs={'id':['article_sidebar_border', 'most-popular_large', 'most-popular-body_large','comment_section','article-related']}),
|
|
||||||
dict(name='ul', attrs={'class':'cat-breadcrumb col three last'}),
|
|
||||||
dict(name='h4', attrs={'id':'related-topics'}),
|
|
||||||
dict(name='table'),
|
|
||||||
dict(name='a', attrs={'href':['/subArticleBottomWeb','/subArticleTopWeb','/subArticleTopMag','/subArticleBottomMag']}),
|
|
||||||
dict(name='a', attrs={'name':'comments_shaded'}),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
#Find date
|
||||||
|
date = re.sub('.*\:\W*', "", self.tag_to_string(div.find('h2')).strip())
|
||||||
|
self.timefmt = u' [%s]'%date
|
||||||
|
|
||||||
feeds = [
|
#Find cover
|
||||||
('History and Archeology',
|
self.cover_url = div.find('img',src=True)['src']
|
||||||
'http://feeds.feedburner.com/smithsonianmag/history-archaeology'),
|
|
||||||
('People and Places',
|
feeds = OrderedDict()
|
||||||
'http://feeds.feedburner.com/smithsonianmag/people-places'),
|
section_title = ''
|
||||||
('Science and Nature',
|
subsection_title = ''
|
||||||
'http://feeds.feedburner.com/smithsonianmag/science-nature'),
|
for post in div.findAll('div', attrs={'class':['plainModule', 'departments plainModule']}):
|
||||||
('Arts and Culture',
|
articles = []
|
||||||
'http://feeds.feedburner.com/smithsonianmag/arts-culture'),
|
prefix = ''
|
||||||
('Travel',
|
h3=post.find('h3')
|
||||||
'http://feeds.feedburner.com/smithsonianmag/travel'),
|
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
|
|
||||||
|
@ -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 TNR(BasicNewsRecipe):
|
||||||
|
|
||||||
class The_New_Republic(BasicNewsRecipe):
|
|
||||||
title = 'The New Republic'
|
title = 'The New Republic'
|
||||||
__author__ = 'cix3'
|
__author__ = 'Rick Shang'
|
||||||
language = 'en'
|
|
||||||
description = 'Intelligent, stimulating and rigorous examination of American politics, foreign policy and culture'
|
|
||||||
timefmt = ' [%b %d, %Y]'
|
|
||||||
|
|
||||||
oldest_article = 7
|
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.'
|
||||||
max_articles_per_feed = 100
|
language = 'en'
|
||||||
|
category = 'news'
|
||||||
|
encoding = 'UTF-8'
|
||||||
|
remove_tags = [dict(attrs={'class':['print-logo','print-site_name','print-hr']})]
|
||||||
|
no_javascript = True
|
||||||
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
|
||||||
|
@ -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,46 +8,44 @@ 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'}),
|
||||||
|
# dict(attrs={'title' : 'download flash'}),
|
||||||
|
# dict(attrs={'style' : 'padding: 5px'})
|
||||||
|
|
||||||
]
|
]
|
||||||
|
remove_tags_after = [dict(id='bodyText')]
|
||||||
remove_tags=[
|
remove_tags=[
|
||||||
#dict(name='head'),
|
dict(name='li'),
|
||||||
dict(attrs={'class' : ['mystery-meat-link','ltbx-container','ltbx-var ltbx-hbxpn','ltbx-var ltbx-nav-loop','ltbx-var ltbx-url']}),
|
dict(attrs={'class' : 'grid-4 right-hand-column'}),
|
||||||
dict(name='div',attrs={'class' : 'cf'}),
|
|
||||||
dict(attrs={'title' : 'download flash'}),
|
|
||||||
dict(attrs={'style' : 'padding: 5px'})
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
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'),
|
||||||
(u'Sport', u'http://www.thesun.co.uk/sol/homepage/sport/rss'),
|
(u'Sport', u'http://www.thesun.co.uk/sol/homepage/sport/rss'),
|
||||||
|
@ -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.
BIN
resources/images/dot_green.png
Normal file
BIN
resources/images/dot_green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
resources/images/dot_red.png
Normal file
BIN
resources/images/dot_red.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -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>"
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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')
|
||||||
|
@ -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',
|
||||||
|
@ -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'
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def raw_to_book(self, json_book, book_class, prefix):
|
||||||
|
try:
|
||||||
|
book = book_class(prefix, json_book.get('lpath', None))
|
||||||
|
for key,val in json_book.iteritems():
|
||||||
|
meta = self.decode_metadata(key, val)
|
||||||
if key == 'user_metadata':
|
if key == 'user_metadata':
|
||||||
book.set_all_user_metadata(meta)
|
book.set_all_user_metadata(meta)
|
||||||
else:
|
else:
|
||||||
if key == 'classifiers':
|
if key == 'classifiers':
|
||||||
key = 'identifiers'
|
key = 'identifiers'
|
||||||
setattr(book, key, meta)
|
setattr(book, key, meta)
|
||||||
booklist.append(book)
|
return book
|
||||||
except:
|
except:
|
||||||
print 'exception during JSON decoding'
|
print 'exception during JSON decoding'
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -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
|
||||||
|
@ -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('"', '"')
|
text = text.replace('"', '"')
|
||||||
self.buf.write(text.encode('utf-8'))
|
self.buf.write(utf8_text(text))
|
||||||
|
|
||||||
def fixup_links(self):
|
def fixup_links(self):
|
||||||
'''
|
'''
|
||||||
|
65
src/calibre/ebooks/oeb/display/full_screen.coffee
Normal file
65
src/calibre/ebooks/oeb/display/full_screen.coffee
Normal 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()
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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,
|
||||||
|
@ -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:
|
||||||
|
@ -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')))
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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,8 +328,16 @@ class DeviceManager(Thread): # {{{
|
|||||||
self.current_job = None
|
self.current_job = None
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
if do_sleep:
|
||||||
time.sleep(self.sleep_time)
|
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,
|
||||||
args=args, kwargs=kwargs, description=description)
|
args=args, kwargs=kwargs, description=description)
|
||||||
@ -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'])
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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">
|
||||||
|
@ -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>&Show this warning again</string>
|
<string>&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>
|
||||||
|
@ -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> ')
|
self.msg_label = QLabel('<p> ')
|
||||||
|
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([])
|
||||||
|
@ -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()
|
||||||
|
@ -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 + '"')
|
||||||
|
80
src/calibre/gui2/dialogs/smartdevice.py
Normal file
80
src/calibre/gui2/dialogs/smartdevice.py
Normal 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)
|
137
src/calibre/gui2/dialogs/smartdevice.ui
Normal file
137
src/calibre/gui2/dialogs/smartdevice.ui
Normal 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>&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>&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>&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>&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>
|
@ -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:
|
||||||
|
@ -135,6 +135,7 @@ 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...'))
|
||||||
|
with gprefs: # Only write gui.json after initialization is complete
|
||||||
main.initialize(self.library_path, db, self.listener, self.actions)
|
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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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,6 +703,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.read_layout_settings()
|
self.read_layout_settings()
|
||||||
|
|
||||||
def write_settings(self):
|
def write_settings(self):
|
||||||
|
with gprefs: # Only write to gprefs once
|
||||||
config.set('main_window_geometry', self.saveGeometry())
|
config.set('main_window_geometry', self.saveGeometry())
|
||||||
dynamic.set('sort_history', self.library_view.model().sort_history)
|
dynamic.set('sort_history', self.library_view.model().sort_history)
|
||||||
self.save_layout_state()
|
self.save_layout_state()
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
@ -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
|
||||||
|
try:
|
||||||
|
# can get here in a race condition with shutdown. Swallow the
|
||||||
|
# exception and run around the loop again.
|
||||||
now = currentTimeMillis()
|
now = currentTimeMillis()
|
||||||
for record in self.zeroconf.cache.entries():
|
for record in self.zeroconf.cache.entries():
|
||||||
if record.isExpired(now):
|
if record.isExpired(now):
|
||||||
self.zeroconf.updateRecord(now, record)
|
self.zeroconf.updateRecord(now, record)
|
||||||
self.zeroconf.cache.remove(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."""
|
||||||
|
@ -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',
|
||||||
|
@ -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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user