mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
sync
This commit is contained in:
commit
2c34abe0b4
@ -31,3 +31,4 @@ nbproject/
|
|||||||
.pydevproject
|
.pydevproject
|
||||||
.settings/
|
.settings/
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
|
calibre_plugins/
|
33
recipes/borse_online.recipe
Normal file
33
recipes/borse_online.recipe
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
class AdvancedUserRecipe1303841067(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'Börse-online'
|
||||||
|
__author__ = 'schuster'
|
||||||
|
oldest_article = 1
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'de'
|
||||||
|
remove_javascript = True
|
||||||
|
cover_url = 'http://www.dpv.de/images/1995/source.gif'
|
||||||
|
masthead_url = 'http://www.zeitschriften-cover.de/cover/boerse-online-cover-januar-2010-x1387.jpg'
|
||||||
|
extra_css = '''
|
||||||
|
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||||
|
h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||||
|
img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
|
||||||
|
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||||
|
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||||
|
'''
|
||||||
|
remove_tags_bevor = [dict(name='h3')]
|
||||||
|
remove_tags_after = [dict(name='div', attrs={'class':'artikelfuss'})]
|
||||||
|
remove_tags = [dict(attrs={'class':['moduleTopNav', 'moduleHeaderNav', 'text', 'blau', 'poll1150']}),
|
||||||
|
dict(id=['newsletterlayer', 'newsletterlayerClose', 'newsletterlayer_body', 'newsletterarray_error', 'newsletterlayer_emailadress', 'newsletterlayer_submit', 'kommentar']),
|
||||||
|
dict(name=['h2', 'Gesamtranking', 'h3',''])]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return url.replace('.html#nv=rss', '.html?mode=print')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [(u'Börsennachrichten', u'http://www.boerse-online.de/rss/')]
|
||||||
|
|
61
recipes/capital_de.recipe
Normal file
61
recipes/capital_de.recipe
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
class AdvancedUserRecipe1305470859(BasicNewsRecipe):
|
||||||
|
title = u'Capital.de'
|
||||||
|
language = 'de'
|
||||||
|
__author__ = 'schuster'
|
||||||
|
oldest_article =7
|
||||||
|
max_articles_per_feed = 35
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
masthead_url = 'http://www.wirtschaftsmedien-shop.de/media/stores/wirtschaftsmedien/capital/teaser_large_abo.jpg'
|
||||||
|
cover_url = 'http://d1kb9jvg6ylufe.cloudfront.net/WebsiteCMS/de/unternehmen/linktipps/mainColumn/08/image/DE_Capital_bis20mm_SW.jpg'
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||||
|
h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||||
|
img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
|
||||||
|
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||||
|
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||||
|
'''
|
||||||
|
def print_version(self, url):
|
||||||
|
return url.replace ('nv=rss#utm_source=rss2&utm_medium=rss_feed&utm_campaign=/', 'mode=print')
|
||||||
|
remove_tags_bevor = [dict(name='td', attrs={'class':'textcell'})]
|
||||||
|
remove_tags_after = [dict(name='div', attrs={'class':'artikelsplit'})]
|
||||||
|
|
||||||
|
feeds = [ (u'Wirtschaftsmagazin', u'http://www.capital.de/rss/'),
|
||||||
|
(u'Unternehmen', u'http://www.capital.de/rss/unternehmen'),
|
||||||
|
(u'Finanz & Geldanlage', u'http://www.capital.de/rss/finanzen/geldanlage')]
|
||||||
|
|
||||||
|
def append_page(self, soup, appendtag, position):
|
||||||
|
pager = soup.find('div',attrs={'class':'artikelsplit'})
|
||||||
|
if pager:
|
||||||
|
nexturl = self.INDEX + pager.a['href']
|
||||||
|
soup2 = self.index_to_soup(nexturl)
|
||||||
|
texttag = soup2.find('div', attrs={'class':'printable'})
|
||||||
|
for it in texttag.findAll(style=True):
|
||||||
|
del it['style']
|
||||||
|
newpos = len(texttag.contents)
|
||||||
|
self.append_page(soup2,texttag,newpos)
|
||||||
|
texttag.extract()
|
||||||
|
appendtag.insert(position,texttag)
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('div', attrs={'class':'artikelsplit'}):
|
||||||
|
item.extract()
|
||||||
|
self.append_page(soup, soup.body, 3)
|
||||||
|
pager = soup.find('div',attrs={'class':'artikelsplit'})
|
||||||
|
if pager:
|
||||||
|
pager.extract()
|
||||||
|
return self.adeify_images(soup)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
remove_tags = [dict(attrs={'class':['navSeitenAlle', 'kommentieren', 'teaserheader', 'teasercontent', 'info', 'zwischenhead', 'artikelsplit']}),
|
||||||
|
dict(id=['topNav', 'mainNav', 'subNav', 'socialmedia', 'footerRahmen', 'gatrixx_marktinformationen', 'pager', 'weitere']),
|
||||||
|
dict(span=['ratingtext', 'Gesamtranking', 'h3','']),
|
||||||
|
dict(rel=['canonical'])]
|
||||||
|
|
55
recipes/dilemaveche.recipe
Normal file
55
recipes/dilemaveche.recipe
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = u'2011, Silviu Cotoar\u0103'
|
||||||
|
'''
|
||||||
|
dilemaveche.ro
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class DilemaVeche(BasicNewsRecipe):
|
||||||
|
title = u'Dilema Veche'
|
||||||
|
__author__ = u'Silviu Cotoar\u0103'
|
||||||
|
description = u'Sunt vechi, domnule!'
|
||||||
|
publisher = u'Dilema Veche'
|
||||||
|
oldest_article = 50
|
||||||
|
language = 'ro'
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
category = 'Ziare'
|
||||||
|
encoding = 'utf-8'
|
||||||
|
cover_url = 'http://www.dilemaveche.ro/sites/all/themes/dilema/theme/dilema_two/layouter/dilema_two_homepage/logo.png'
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comments' : description
|
||||||
|
,'tags' : category
|
||||||
|
,'language' : language
|
||||||
|
,'publisher' : publisher
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h1', attrs={'class':'art_title'})
|
||||||
|
, dict(name='h1', attrs={'class':'art_title online'})
|
||||||
|
, dict(name='div', attrs={'class':'item'})
|
||||||
|
, dict(name='div', attrs={'class':'art_content'})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':['article_details']})
|
||||||
|
, dict(name='div', attrs={'class':['controale']})
|
||||||
|
, dict(name='div', attrs={'class':['art_related_left']})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags_after = [
|
||||||
|
dict(name='div', attrs={'class':['article_details']})
|
||||||
|
]
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Feeds', u'http://www.dilemaveche.ro/rss.xml')
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
return self.adeify_images(soup)
|
@ -1,19 +1,21 @@
|
|||||||
|
import re
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class AdvancedUserRecipe1302341394(BasicNewsRecipe):
|
class AdvancedUserRecipe1302341394(BasicNewsRecipe):
|
||||||
title = u'DvhN'
|
title = u'DvhN'
|
||||||
oldest_article = 1
|
__author__ = 'Reijndert'
|
||||||
|
oldest_article = 7
|
||||||
max_articles_per_feed = 200
|
max_articles_per_feed = 200
|
||||||
|
|
||||||
__author__ = 'Reijndert'
|
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
cover_url = 'http://www.dvhn.nl/template/Dagblad_v2.0/gfx/logo_DvhN.gif'
|
cover_url = 'http://members.home.nl/apm.de.haas/calibre/DvhN.jpg'
|
||||||
language = 'nl'
|
language = 'nl'
|
||||||
country = 'NL'
|
country = 'NL'
|
||||||
version = 1
|
version = 1
|
||||||
publisher = u'Dagblad van het Noorden'
|
publisher = u'Dagblad van het Noorden'
|
||||||
category = u'Nieuws'
|
category = u'Nieuws'
|
||||||
description = u'Nieuws uit Noord Nederland'
|
description = u'Nieuws uit Noord Nederland'
|
||||||
|
timefmt = ' %Y-%m-%d (%a)'
|
||||||
|
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':'fullPicture'})
|
keep_only_tags = [dict(name='div', attrs={'id':'fullPicture'})
|
||||||
@ -21,11 +23,26 @@ class AdvancedUserRecipe1302341394(BasicNewsRecipe):
|
|||||||
]
|
]
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name=['object','link','iframe','base'])
|
dict(name='span',attrs={'class':'location'})
|
||||||
,dict(name='span',attrs={'class':'copyright'})
|
|
||||||
]
|
]
|
||||||
|
|
||||||
feeds = [(u'Drenthe', u'http://www.dvhn.nl/nieuws/drenthe/index.jsp?service=rss'), (u'Groningen', u'http://www.dvhn.nl/nieuws/groningen/index.jsp?service=rss'), (u'Nederland', u'http://www.dvhn.nl/nieuws/nederland/index.jsp?service=rss'), (u'Wereld', u'http://www.dvhn.nl/nieuws/wereld/index.jsp?service=rss'), (u'Economie', u'http://www.dvhn.nl/nieuws/economie/index.jsp?service=rss'), (u'Sport', u'http://www.dvhn.nl/nieuws/sport/index.jsp?service=rss'), (u'Cultuur', u'http://www.dvhn.nl/nieuws/kunst/index.jsp?service=rss'), (u'24 Uur', u'http://www.dvhn.nl/nieuws/24uurdvhn/index.jsp?service=rss&selectiontype=last24hours')]
|
preprocess_regexps = [
|
||||||
|
(re.compile(r'<a.*?>'), lambda h1: '')
|
||||||
|
,(re.compile(r'</a>'), lambda h2: '')
|
||||||
|
,(re.compile(r'Word vriend van Dagblad van het Noorden op Facebook'), lambda h3: '')
|
||||||
|
,(re.compile(r'Volg Dagblad van het Noorden op Twitter'), lambda h3: '')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [(u'Drenthe', u'http://www.dvhn.nl/nieuws/drenthe/index.jsp?service=rss')
|
||||||
|
, (u'Groningen', u'http://www.dvhn.nl/nieuws/groningen/index.jsp?service=rss')
|
||||||
|
, (u'Nederland', u'http://www.dvhn.nl/nieuws/nederland/index.jsp?service=rss')
|
||||||
|
, (u'Wereld', u'http://www.dvhn.nl/nieuws/wereld/index.jsp?service=rss')
|
||||||
|
, (u'Economie', u'http://www.dvhn.nl/nieuws/economie/index.jsp?service=rss')
|
||||||
|
, (u'Sport', u'http://www.dvhn.nl/nieuws/sport/index.jsp?service=rss')
|
||||||
|
, (u'Cultuur', u'http://www.dvhn.nl/nieuws/kunst/index.jsp?service=rss')
|
||||||
|
, (u'24 Uur', u'http://www.dvhn.nl/nieuws/24uurdvhn/index.jsp?service=rss&selectiontype=last24hours')
|
||||||
|
]
|
||||||
|
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
body {font-family: verdana, arial, helvetica, geneva, sans-serif;}
|
body {font-family: verdana, arial, helvetica, geneva, sans-serif;}
|
||||||
|
38
recipes/glamour.recipe
Normal file
38
recipes/glamour.recipe
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1305547242(BasicNewsRecipe):
|
||||||
|
title = u'Glamour (US)'
|
||||||
|
oldest_article = 21
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'en'
|
||||||
|
remove_javascript = True
|
||||||
|
__author__ = 'Anonymous'
|
||||||
|
remove_tags = [dict(name='div', attrs={'class':'articles_footer', 'class':'printoptions'})]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return url + '?printable=true'
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for alink in soup.findAll('a'):
|
||||||
|
if alink.string is not None:
|
||||||
|
tstr = alink.string
|
||||||
|
alink.replaceWith(tstr)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
feeds = [ (u'All Fashion', u'http://feeds.glamour.com/glamour/all_fashion'),
|
||||||
|
(u'All Beauty', u'http://feeds.glamour.com/glamour/all_beauty'),
|
||||||
|
(u'All Sex, Love & Life', u'http://feeds.glamour.com/glamour/sex_love_life'),
|
||||||
|
(u'All Health & Fitness', u'http://feeds.glamour.com/glamour/health_fitness'),
|
||||||
|
(u'Shopping', u'http://feeds.glamour.com/glamour/shopping'),
|
||||||
|
(u'Slaves to Fashion blog', u'http://feeds.glamour.com/glamour/slavestofashion'),
|
||||||
|
(u'The Girls in the Beauty Department', u'http://feeds.glamour.com/glamour/thegirlsinthebeautydepartment'),
|
||||||
|
(u'Smitten blog', u'http://feeds.glamour.com/glamour/smitten'),
|
||||||
|
(u'Save the Date', u'http://feeds.feedburner.com/glamour/save-the-date'),
|
||||||
|
(u'Single-ish blog', u'http://feeds.glamour.com/glamour/glamoursingle-ish'),
|
||||||
|
(u'Save the Date', u'http://feeds.feedburner.com/glamour/save-the-date'),
|
||||||
|
(u'Vitamin G blog', u'http://feeds.glamour.com/glamour/vitamin-g'),
|
||||||
|
(u'Margarita Shapes Up blog', u'http://feeds.glamour.com/glamour/margaritashapesup'),
|
||||||
|
(u'Little Miss Fortune blog', u'http://feeds.glamour.com/glamour/little-miss-fortune'),
|
||||||
|
]
|
BIN
recipes/icons/dilemaveche.png
Normal file
BIN
recipes/icons/dilemaveche.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 558 B |
BIN
recipes/icons/natgeo.png
Normal file
BIN
recipes/icons/natgeo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 247 B |
32
recipes/impulse_de.recipe
Normal file
32
recipes/impulse_de.recipe
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
class AdvancedUserRecipe1305470859(BasicNewsRecipe):
|
||||||
|
title = u'Impulse.de'
|
||||||
|
language = 'de'
|
||||||
|
__author__ = 'schuster'
|
||||||
|
oldest_article =14
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
cover_url = 'http://www.bvk.de/files/image/bilder/Logo%20Impulse.jpg'
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||||
|
h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||||
|
img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
|
||||||
|
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||||
|
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||||
|
'''
|
||||||
|
def print_version(self, url):
|
||||||
|
return url.replace ('#utm_source=rss2&utm_medium=rss_feed&utm_campaign=/', '?mode=print')
|
||||||
|
remove_tags_bevor = [dict(name='h1', attrs={'class':'h2'})]
|
||||||
|
remove_tags_after = [dict(name='div', attrs={'class':'artikelfuss'})]
|
||||||
|
|
||||||
|
feeds = [ (u'impulstest', u'http://www.impulse.de/rss/')]
|
||||||
|
|
||||||
|
|
||||||
|
remove_tags = [dict(attrs={'class':['navSeitenAlle', 'kommentieren', 'teaserheader', 'teasercontent', 'info', 'zwischenhead', 'kasten_artikel']}),
|
||||||
|
dict(id=['metaNav', 'impKopf', 'impTopNav', 'impSubNav', 'footerRahmen', 'gatrixx_marktinformationen', 'pager', 'weitere', 'socialmedia', 'rating_open']),
|
||||||
|
dict(span=['ratingtext', 'Gesamtranking', 'h3','']),
|
||||||
|
dict(rel=['canonical'])]
|
||||||
|
|
71
recipes/natgeo.recipe
Normal file
71
recipes/natgeo.recipe
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, gagsays <gagsays at gmail dot com>'
|
||||||
|
'''
|
||||||
|
nationalgeographic.com
|
||||||
|
'''
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
class NatGeo(BasicNewsRecipe):
|
||||||
|
title = u'National Geographic'
|
||||||
|
description = 'Daily news articles from The National Geographic'
|
||||||
|
language = 'en'
|
||||||
|
oldest_article = 20
|
||||||
|
max_articles_per_feed = 25
|
||||||
|
encoding = 'utf8'
|
||||||
|
publisher = 'nationalgeographic.com'
|
||||||
|
category = 'science, nat geo'
|
||||||
|
__author__ = 'gagsays'
|
||||||
|
masthead_url = 'http://s.ngeo.com/wpf/sites/themes/global/i/presentation/ng_logo_small.png'
|
||||||
|
description = 'Inspiring people to care about the planet since 1888'
|
||||||
|
timefmt = ' [%a, %d %b, %Y]'
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
body {color: #000000;font-size: medium;}
|
||||||
|
h1 {color: #222222; font-size: large; font-weight:lighter; text-decoration:none; text-align: center;font-family:Georgia,Times New Roman,Times,serif;}
|
||||||
|
h2 {color: #454545; font-size: small; font-weight:lighter; text-decoration:none; text-align: justify; font-style:italic;font-family :Georgia,Times New Roman,Times,serif;}
|
||||||
|
h3 {color: #555555; font-size: small; font-style:italic; margin-top: 10px;}
|
||||||
|
img{margin-bottom: 0.25em;display:block;margin-left: auto;margin-right: auto;}
|
||||||
|
a:link,a,.a,href {text-decoration: none;color: #000000;}
|
||||||
|
.caption{color: #000000;font-size: xx-small;text-align: justify;font-weight:normal;}
|
||||||
|
.credit{color: #555555;font-size: xx-small;text-align: left;font-weight:lighter;}
|
||||||
|
p.author,p.publication{color: #000000;font-size: xx-small;text-align: left;display:inline;}
|
||||||
|
p.publication_time{color: #000000;font-size: xx-small;text-align: right;text-decoration: underline;}
|
||||||
|
p {margin-bottom: 0;}
|
||||||
|
p + p {text-indent: 1.5em;margin-top: 0;}
|
||||||
|
.hidden{display:none;}
|
||||||
|
#page_head{text-transform:uppercase;}
|
||||||
|
'''
|
||||||
|
|
||||||
|
def parse_feeds (self):
|
||||||
|
feeds = BasicNewsRecipe.parse_feeds(self)
|
||||||
|
for feed in feeds:
|
||||||
|
for article in feed.articles[:]:
|
||||||
|
if 'Presented' in article.title or 'Pictures' in article.title:
|
||||||
|
feed.articles.remove(article)
|
||||||
|
return feeds
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for alink in soup.findAll('a'):
|
||||||
|
if alink.string is not None:
|
||||||
|
tstr = alink.string
|
||||||
|
alink.replaceWith(tstr)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
remove_tags_before = dict(id='page_head')
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div',attrs={'id':['page_head','content_mainA']})
|
||||||
|
]
|
||||||
|
remove_tags_after = [
|
||||||
|
dict(name='div',attrs={'class':['article_text','promo_collection']})
|
||||||
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':['aside','primary full_width']})
|
||||||
|
,dict(name='div', attrs={'id':['header_search','navigation_mainB_wrap']})
|
||||||
|
]
|
||||||
|
feeds = [
|
||||||
|
(u'Daily News', u'http://feeds.nationalgeographic.com/ng/News/News_Main')
|
||||||
|
]
|
||||||
|
|
@ -11,6 +11,20 @@ class Newsweek(BasicNewsRecipe):
|
|||||||
|
|
||||||
BASE_URL = 'http://www.newsweek.com'
|
BASE_URL = 'http://www.newsweek.com'
|
||||||
|
|
||||||
|
topics = {
|
||||||
|
'Culture' : '/tag/culture.html',
|
||||||
|
'Business' : '/tag/business.html',
|
||||||
|
'Society' : '/tag/society.html',
|
||||||
|
'Science' : '/tag/science.html',
|
||||||
|
'Education' : '/tag/education.html',
|
||||||
|
'Politics' : '/tag/politics.html',
|
||||||
|
'Health' : '/tag/health.html',
|
||||||
|
'World' : '/tag/world.html',
|
||||||
|
'Nation' : '/tag/nation.html',
|
||||||
|
'Technology' : '/tag/technology.html',
|
||||||
|
'Game Changers' : '/tag/game-changers.html',
|
||||||
|
}
|
||||||
|
|
||||||
keep_only_tags = dict(name='article', attrs={'class':'article-text'})
|
keep_only_tags = dict(name='article', attrs={'class':'article-text'})
|
||||||
remove_tags = [dict(attrs={'data-dartad':True})]
|
remove_tags = [dict(attrs={'data-dartad':True})]
|
||||||
remove_attributes = ['property']
|
remove_attributes = ['property']
|
||||||
@ -21,14 +35,10 @@ class Newsweek(BasicNewsRecipe):
|
|||||||
return soup
|
return soup
|
||||||
|
|
||||||
def newsweek_sections(self):
|
def newsweek_sections(self):
|
||||||
return [
|
for topic_name, topic_url in self.topics.iteritems():
|
||||||
('Nation', 'http://www.newsweek.com/tag/nation.html'),
|
yield (topic_name,
|
||||||
('Society', 'http://www.newsweek.com/tag/society.html'),
|
self.BASE_URL+topic_url)
|
||||||
('Culture', 'http://www.newsweek.com/tag/culture.html'),
|
|
||||||
('World', 'http://www.newsweek.com/tag/world.html'),
|
|
||||||
('Politics', 'http://www.newsweek.com/tag/politics.html'),
|
|
||||||
('Business', 'http://www.newsweek.com/tag/business.html'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def newsweek_parse_section_page(self, soup):
|
def newsweek_parse_section_page(self, soup):
|
||||||
for article in soup.findAll('article', about=True,
|
for article in soup.findAll('article', about=True,
|
||||||
|
@ -41,14 +41,19 @@ authors_completer_append_separator = False
|
|||||||
#: Author sort name algorithm
|
#: Author sort name algorithm
|
||||||
# The algorithm used to copy author to author_sort
|
# The algorithm used to copy author to author_sort
|
||||||
# Possible values are:
|
# Possible values are:
|
||||||
# invert: use "fn ln" -> "ln, fn" (the default algorithm)
|
# invert: use "fn ln" -> "ln, fn"
|
||||||
# copy : copy author to author_sort without modification
|
# copy : copy author to author_sort without modification
|
||||||
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
|
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
|
||||||
# nocomma : "fn ln" -> "ln fn" (without the comma)
|
# nocomma : "fn ln" -> "ln fn" (without the comma)
|
||||||
# When this tweak is changed, the author_sort values stored with each author
|
# When this tweak is changed, the author_sort values stored with each author
|
||||||
# must be recomputed by right-clicking on an author in the left-hand tags pane,
|
# must be recomputed by right-clicking on an author in the left-hand tags pane,
|
||||||
# selecting 'manage authors', and pressing 'Recalculate all author sort values'.
|
# selecting 'manage authors', and pressing 'Recalculate all author sort values'.
|
||||||
|
# The author name suffixes are words that are ignored when they occur at the
|
||||||
|
# end of an author name. The case of the suffix is ignored and trailing
|
||||||
|
# periods are automatically handled.
|
||||||
author_sort_copy_method = 'comma'
|
author_sort_copy_method = 'comma'
|
||||||
|
author_name_suffixes = ('Jr', 'Sr', 'Inc', 'Ph.D', 'Phd',
|
||||||
|
'MD', 'M.D', 'I', 'II', 'III', 'IV')
|
||||||
|
|
||||||
#: Use author sort in Tag Browser
|
#: Use author sort in Tag Browser
|
||||||
# Set which author field to display in the tags pane (the list of authors,
|
# Set which author field to display in the tags pane (the list of authors,
|
||||||
|
@ -630,6 +630,24 @@ def human_readable(size):
|
|||||||
size = size[:-2]
|
size = size[:-2]
|
||||||
return size + " " + suffix
|
return size + " " + suffix
|
||||||
|
|
||||||
|
def remove_bracketed_text(src,
|
||||||
|
brackets={u'(':u')', u'[':u']', u'{':u'}'}):
|
||||||
|
from collections import Counter
|
||||||
|
counts = Counter()
|
||||||
|
buf = []
|
||||||
|
src = force_unicode(src)
|
||||||
|
rmap = dict([(v, k) for k, v in brackets.iteritems()])
|
||||||
|
for char in src:
|
||||||
|
if char in brackets:
|
||||||
|
counts[char] += 1
|
||||||
|
elif char in rmap:
|
||||||
|
idx = rmap[char]
|
||||||
|
if counts[idx] > 0:
|
||||||
|
counts[idx] -= 1
|
||||||
|
elif sum(counts.itervalues()) < 1:
|
||||||
|
buf.append(char)
|
||||||
|
return u''.join(buf)
|
||||||
|
|
||||||
if isosx:
|
if isosx:
|
||||||
import glob, shutil
|
import glob, shutil
|
||||||
fdir = os.path.expanduser('~/.fonts')
|
fdir = os.path.expanduser('~/.fonts')
|
||||||
|
@ -1164,6 +1164,11 @@ class StoreFoylesUKStore(StoreBase):
|
|||||||
description = _('Foyles of London, online.')
|
description = _('Foyles of London, online.')
|
||||||
actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore'
|
actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore'
|
||||||
|
|
||||||
|
class StoreGoogleBooksStore(StoreBase):
|
||||||
|
name = 'Google Books'
|
||||||
|
description = _('Google Books')
|
||||||
|
actual_plugin = 'calibre.gui2.store.google_books_plugin:GoogleBooksStore'
|
||||||
|
|
||||||
class StoreGutenbergStore(StoreBase):
|
class StoreGutenbergStore(StoreBase):
|
||||||
name = 'Project Gutenberg'
|
name = 'Project Gutenberg'
|
||||||
description = _('The first producer of free ebooks.')
|
description = _('The first producer of free ebooks.')
|
||||||
@ -1219,7 +1224,8 @@ plugins += [StoreArchiveOrgStore, StoreAmazonKindleStore, StoreAmazonDEKindleSto
|
|||||||
StoreBeamEBooksDEStore, StoreBeWriteStore,
|
StoreBeamEBooksDEStore, StoreBeWriteStore,
|
||||||
StoreDieselEbooksStore, StoreEbookscomStore, StoreEPubBuyDEStore,
|
StoreDieselEbooksStore, StoreEbookscomStore, StoreEPubBuyDEStore,
|
||||||
StoreEHarlequinStore, StoreFeedbooksStore,
|
StoreEHarlequinStore, StoreFeedbooksStore,
|
||||||
StoreFoylesUKStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore,
|
StoreFoylesUKStore, StoreGoogleBooksStore, StoreGutenbergStore,
|
||||||
|
StoreKoboStore, StoreManyBooksStore,
|
||||||
StoreMobileReadStore, StoreNextoStore, StoreOpenLibraryStore, StoreSmashwordsStore,
|
StoreMobileReadStore, StoreNextoStore, StoreOpenLibraryStore, StoreSmashwordsStore,
|
||||||
StoreWaterstonesUKStore, StoreWeightlessBooksStore, StoreWizardsTowerBooksStore]
|
StoreWaterstonesUKStore, StoreWeightlessBooksStore, StoreWizardsTowerBooksStore]
|
||||||
|
|
||||||
|
@ -941,7 +941,7 @@ class ITUNES(DriverBase):
|
|||||||
# declared in use_plugboard_ext and a device name of ITUNES
|
# declared in use_plugboard_ext and a device name of ITUNES
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info("ITUNES.set_plugboard()")
|
self.log.info("ITUNES.set_plugboard()")
|
||||||
#self.log.info(' using plugboard %s' % plugboards)
|
#self.log.info(' plugboard: %s' % plugboards)
|
||||||
self.plugboards = plugboards
|
self.plugboards = plugboards
|
||||||
self.plugboard_func = pb_func
|
self.plugboard_func = pb_func
|
||||||
|
|
||||||
@ -1052,7 +1052,6 @@ class ITUNES(DriverBase):
|
|||||||
'title': metadata[i].title,
|
'title': metadata[i].title,
|
||||||
'uuid': metadata[i].uuid }
|
'uuid': metadata[i].uuid }
|
||||||
|
|
||||||
|
|
||||||
# Report progress
|
# Report progress
|
||||||
if self.report_progress is not None:
|
if self.report_progress is not None:
|
||||||
self.report_progress((i+1)/file_count, _('%d of %d') % (i+1, file_count))
|
self.report_progress((i+1)/file_count, _('%d of %d') % (i+1, file_count))
|
||||||
@ -2744,7 +2743,7 @@ class ITUNES(DriverBase):
|
|||||||
# Update metadata from plugboard
|
# Update metadata from plugboard
|
||||||
# If self.plugboard is None (no transforms), original metadata is returned intact
|
# If self.plugboard is None (no transforms), original metadata is returned intact
|
||||||
metadata_x = self._xform_metadata_via_plugboard(metadata, this_book.format)
|
metadata_x = self._xform_metadata_via_plugboard(metadata, this_book.format)
|
||||||
|
self.log("metadata.title_sort: %s metadata_x.title_sort: %s" % (metadata.title_sort, metadata_x.title_sort))
|
||||||
if isosx:
|
if isosx:
|
||||||
if lb_added:
|
if lb_added:
|
||||||
lb_added.name.set(metadata_x.title)
|
lb_added.name.set(metadata_x.title)
|
||||||
@ -2754,8 +2753,7 @@ class ITUNES(DriverBase):
|
|||||||
lb_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
lb_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
||||||
lb_added.enabled.set(True)
|
lb_added.enabled.set(True)
|
||||||
lb_added.sort_artist.set(icu_title(metadata_x.author_sort))
|
lb_added.sort_artist.set(icu_title(metadata_x.author_sort))
|
||||||
lb_added.sort_name.set(metadata.title_sort)
|
lb_added.sort_name.set(metadata_x.title_sort)
|
||||||
|
|
||||||
|
|
||||||
if db_added:
|
if db_added:
|
||||||
db_added.name.set(metadata_x.title)
|
db_added.name.set(metadata_x.title)
|
||||||
@ -2765,7 +2763,7 @@ class ITUNES(DriverBase):
|
|||||||
db_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
db_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
||||||
db_added.enabled.set(True)
|
db_added.enabled.set(True)
|
||||||
db_added.sort_artist.set(icu_title(metadata_x.author_sort))
|
db_added.sort_artist.set(icu_title(metadata_x.author_sort))
|
||||||
db_added.sort_name.set(metadata.title_sort)
|
db_added.sort_name.set(metadata_x.title_sort)
|
||||||
|
|
||||||
if metadata_x.comments:
|
if metadata_x.comments:
|
||||||
if lb_added:
|
if lb_added:
|
||||||
@ -2785,6 +2783,7 @@ class ITUNES(DriverBase):
|
|||||||
|
|
||||||
# Set genre from series if available, else first alpha tag
|
# Set genre from series if available, else first alpha tag
|
||||||
# Otherwise iTunes grabs the first dc:subject from the opf metadata
|
# Otherwise iTunes grabs the first dc:subject from the opf metadata
|
||||||
|
# If title_sort applied in plugboard, that overrides using series/index as title_sort
|
||||||
if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]:
|
if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" ITUNES._update_iTunes_metadata()")
|
self.log.info(" ITUNES._update_iTunes_metadata()")
|
||||||
@ -2796,7 +2795,9 @@ class ITUNES(DriverBase):
|
|||||||
fraction = index-integer
|
fraction = index-integer
|
||||||
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
|
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
|
||||||
if lb_added:
|
if lb_added:
|
||||||
lb_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index))
|
# If no title_sort plugboard tweak, create sort_name from series/index
|
||||||
|
if metadata.title_sort == metadata_x.title_sort:
|
||||||
|
lb_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index))
|
||||||
lb_added.episode_ID.set(metadata_x.series)
|
lb_added.episode_ID.set(metadata_x.series)
|
||||||
lb_added.episode_number.set(metadata_x.series_index)
|
lb_added.episode_number.set(metadata_x.series_index)
|
||||||
|
|
||||||
@ -2810,7 +2811,9 @@ class ITUNES(DriverBase):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if db_added:
|
if db_added:
|
||||||
db_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index))
|
# If no title_sort plugboard tweak, create sort_name from series/index
|
||||||
|
if metadata.title_sort == metadata_x.title_sort:
|
||||||
|
db_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index))
|
||||||
db_added.episode_ID.set(metadata_x.series)
|
db_added.episode_ID.set(metadata_x.series)
|
||||||
db_added.episode_number.set(metadata_x.series_index)
|
db_added.episode_number.set(metadata_x.series_index)
|
||||||
|
|
||||||
@ -2845,7 +2848,7 @@ class ITUNES(DriverBase):
|
|||||||
lb_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
lb_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
||||||
lb_added.Enabled = True
|
lb_added.Enabled = True
|
||||||
lb_added.SortArtist = icu_title(metadata_x.author_sort)
|
lb_added.SortArtist = icu_title(metadata_x.author_sort)
|
||||||
lb_added.SortName = metadata.title_sort
|
lb_added.SortName = metadata_x.title_sort
|
||||||
|
|
||||||
if db_added:
|
if db_added:
|
||||||
db_added.Name = metadata_x.title
|
db_added.Name = metadata_x.title
|
||||||
@ -2855,7 +2858,7 @@ class ITUNES(DriverBase):
|
|||||||
db_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
db_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
|
||||||
db_added.Enabled = True
|
db_added.Enabled = True
|
||||||
db_added.SortArtist = icu_title(metadata_x.author_sort)
|
db_added.SortArtist = icu_title(metadata_x.author_sort)
|
||||||
db_added.SortName = metadata.title_sort
|
db_added.SortName = metadata_x.title_sort
|
||||||
|
|
||||||
if metadata_x.comments:
|
if metadata_x.comments:
|
||||||
if lb_added:
|
if lb_added:
|
||||||
@ -2888,7 +2891,9 @@ class ITUNES(DriverBase):
|
|||||||
fraction = index-integer
|
fraction = index-integer
|
||||||
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
|
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
|
||||||
if lb_added:
|
if lb_added:
|
||||||
lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
|
# If no title_sort plugboard tweak, create sort_name from series/index
|
||||||
|
if metadata.title_sort == metadata_x.title_sort:
|
||||||
|
lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
|
||||||
lb_added.EpisodeID = metadata_x.series
|
lb_added.EpisodeID = metadata_x.series
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -2914,7 +2919,9 @@ class ITUNES(DriverBase):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if db_added:
|
if db_added:
|
||||||
db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
|
# If no title_sort plugboard tweak, create sort_name from series/index
|
||||||
|
if metadata.title_sort == metadata_x.title_sort:
|
||||||
|
db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
|
||||||
db_added.EpisodeID = metadata_x.series
|
db_added.EpisodeID = metadata_x.series
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -2975,6 +2982,9 @@ class ITUNES(DriverBase):
|
|||||||
newmi.publisher if book.publisher != newmi.publisher else ''))
|
newmi.publisher if book.publisher != newmi.publisher else ''))
|
||||||
self.log.info(" tags: %s %s" % (book.tags, ">>> %s" %
|
self.log.info(" tags: %s %s" % (book.tags, ">>> %s" %
|
||||||
newmi.tags if book.tags != newmi.tags else ''))
|
newmi.tags if book.tags != newmi.tags else ''))
|
||||||
|
else:
|
||||||
|
self.log(" matching plugboard not found")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
newmi = book
|
newmi = book
|
||||||
return newmi
|
return newmi
|
||||||
|
@ -10,7 +10,7 @@ import os, sys, re
|
|||||||
from urllib import unquote, quote
|
from urllib import unquote, quote
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from calibre import relpath, guess_type
|
from calibre import relpath, guess_type, remove_bracketed_text
|
||||||
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
|
|
||||||
@ -27,20 +27,37 @@ def authors_to_string(authors):
|
|||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
_bracket_pat = re.compile(r'[\[({].*?[})\]]')
|
def author_to_author_sort(author, method=None):
|
||||||
def author_to_author_sort(author):
|
|
||||||
if not author:
|
if not author:
|
||||||
return ''
|
return u''
|
||||||
method = tweaks['author_sort_copy_method']
|
sauthor = remove_bracketed_text(author).strip()
|
||||||
if method == 'copy' or (method == 'comma' and ',' in author):
|
tokens = sauthor.split()
|
||||||
|
if len(tokens) < 2:
|
||||||
return author
|
return author
|
||||||
author = _bracket_pat.sub('', author).strip()
|
if method is None:
|
||||||
tokens = author.split()
|
method = tweaks['author_sort_copy_method']
|
||||||
if tokens and tokens[-1] not in ('Inc.', 'Inc'):
|
if method == u'copy':
|
||||||
tokens = tokens[-1:] + tokens[:-1]
|
return author
|
||||||
if len(tokens) > 1 and method != 'nocomma':
|
suffixes = set([x.lower() for x in tweaks['author_name_suffixes']])
|
||||||
tokens[0] += ','
|
suffixes |= set([x+u'.' for x in suffixes])
|
||||||
return ' '.join(tokens)
|
|
||||||
|
last = tokens[-1].lower()
|
||||||
|
suffix = None
|
||||||
|
if last in suffixes:
|
||||||
|
suffix = tokens[-1]
|
||||||
|
tokens = tokens[:-1]
|
||||||
|
|
||||||
|
if method == u'comma' and u',' in u''.join(tokens):
|
||||||
|
return author
|
||||||
|
|
||||||
|
atokens = tokens[-1:] + tokens[:-1]
|
||||||
|
if suffix:
|
||||||
|
atokens.append(suffix)
|
||||||
|
|
||||||
|
if method != u'nocomma' and len(atokens) > 1:
|
||||||
|
atokens[0] += u','
|
||||||
|
|
||||||
|
return u' '.join(atokens)
|
||||||
|
|
||||||
def authors_to_sort_string(authors):
|
def authors_to_sort_string(authors):
|
||||||
return ' & '.join(map(author_to_author_sort, authors))
|
return ' & '.join(map(author_to_author_sort, authors))
|
||||||
|
@ -631,10 +631,11 @@ class Application(QApplication):
|
|||||||
if (islinux or isfreebsd) and st in ('windows', 'motif', 'cde'):
|
if (islinux or isfreebsd) and st in ('windows', 'motif', 'cde'):
|
||||||
from PyQt4.Qt import QStyleFactory
|
from PyQt4.Qt import QStyleFactory
|
||||||
styles = set(map(unicode, QStyleFactory.keys()))
|
styles = set(map(unicode, QStyleFactory.keys()))
|
||||||
if 'Cleanlooks' in styles:
|
if 'Plastique' in styles and os.environ.get('KDE_FULL_SESSION',
|
||||||
self.setStyle('Cleanlooks')
|
False):
|
||||||
else:
|
|
||||||
self.setStyle('Plastique')
|
self.setStyle('Plastique')
|
||||||
|
elif 'Cleanlooks' in styles:
|
||||||
|
self.setStyle('Cleanlooks')
|
||||||
|
|
||||||
def _send_file_open_events(self):
|
def _send_file_open_events(self):
|
||||||
with self._file_open_lock:
|
with self._file_open_lock:
|
||||||
|
@ -9,11 +9,12 @@ from functools import partial
|
|||||||
|
|
||||||
from PyQt4.Qt import QMenu, QObject, QTimer
|
from PyQt4.Qt import QMenu, QObject, QTimer
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog, question_dialog
|
||||||
from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog
|
from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.dialogs.confirm_delete_location import confirm_location
|
from calibre.gui2.dialogs.confirm_delete_location import confirm_location
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.utils.recycle_bin import can_recycle
|
||||||
|
|
||||||
single_shot = partial(QTimer.singleShot, 10)
|
single_shot = partial(QTimer.singleShot, 10)
|
||||||
|
|
||||||
@ -24,6 +25,15 @@ class MultiDeleter(QObject):
|
|||||||
QObject.__init__(self, gui)
|
QObject.__init__(self, gui)
|
||||||
self.model = gui.library_view.model()
|
self.model = gui.library_view.model()
|
||||||
self.ids = ids
|
self.ids = ids
|
||||||
|
self.permanent = False
|
||||||
|
if can_recycle and len(ids) > 100:
|
||||||
|
if question_dialog(gui, _('Are you sure?'), '<p>'+
|
||||||
|
_('You are trying to delete %d books. '
|
||||||
|
'Sending so many files to the Recycle'
|
||||||
|
' Bin <b>can be slow</b>. Should calibre skip the'
|
||||||
|
' Recycle Bin? If you click Yes the files'
|
||||||
|
' will be <b>permanently deleted</b>.')%len(ids)):
|
||||||
|
self.permanent = True
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
self.failures = []
|
self.failures = []
|
||||||
self.deleted_ids = []
|
self.deleted_ids = []
|
||||||
@ -44,7 +54,8 @@ class MultiDeleter(QObject):
|
|||||||
title_ = self.model.db.title(id_, index_is_id=True)
|
title_ = self.model.db.title(id_, index_is_id=True)
|
||||||
if title_:
|
if title_:
|
||||||
title = title_
|
title = title_
|
||||||
self.model.db.delete_book(id_, notify=False, commit=False)
|
self.model.db.delete_book(id_, notify=False, commit=False,
|
||||||
|
permanent=self.permanent)
|
||||||
self.deleted_ids.append(id_)
|
self.deleted_ids.append(id_)
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -68,7 +68,7 @@ class StoreAction(InterfaceAction):
|
|||||||
a = ' '.join(a)
|
a = ' '.join(a)
|
||||||
corrected_authors.append(a)
|
corrected_authors.append(a)
|
||||||
|
|
||||||
return ' & '.join(corrected_authors)
|
return ' & '.join(corrected_authors).strip()
|
||||||
|
|
||||||
def search_author(self):
|
def search_author(self):
|
||||||
row = self._get_selected_row()
|
row = self._get_selected_row()
|
||||||
@ -87,7 +87,7 @@ class StoreAction(InterfaceAction):
|
|||||||
mi = self.gui.current_view().model().get_book_display_info(row)
|
mi = self.gui.current_view().model().get_book_display_info(row)
|
||||||
title = mi.title
|
title = mi.title
|
||||||
|
|
||||||
return title
|
return title.strip()
|
||||||
|
|
||||||
def search_title(self):
|
def search_title(self):
|
||||||
row = self._get_selected_row()
|
row = self._get_selected_row()
|
||||||
|
316
src/calibre/gui2/bars.py
Normal file
316
src/calibre/gui2/bars.py
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QObject, QToolBar, Qt, QSize, QToolButton, QVBoxLayout,
|
||||||
|
QLabel, QWidget, QAction, QMenuBar, QMenu)
|
||||||
|
|
||||||
|
from calibre.constants import isosx
|
||||||
|
from calibre.gui2 import gprefs
|
||||||
|
|
||||||
|
class ToolBar(QToolBar): # {{{
|
||||||
|
|
||||||
|
def __init__(self, donate, location_manager, parent):
|
||||||
|
QToolBar.__init__(self, parent)
|
||||||
|
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||||
|
self.setMovable(False)
|
||||||
|
self.setFloatable(False)
|
||||||
|
self.setOrientation(Qt.Horizontal)
|
||||||
|
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
|
||||||
|
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
|
||||||
|
self.preferred_width = self.sizeHint().width()
|
||||||
|
self.gui = parent
|
||||||
|
self.donate_button = donate
|
||||||
|
self.added_actions = []
|
||||||
|
|
||||||
|
self.location_manager = location_manager
|
||||||
|
donate.setAutoRaise(True)
|
||||||
|
donate.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.showing_donate = False
|
||||||
|
|
||||||
|
def resizeEvent(self, ev):
|
||||||
|
QToolBar.resizeEvent(self, ev)
|
||||||
|
style = self.get_text_style()
|
||||||
|
self.setToolButtonStyle(style)
|
||||||
|
if hasattr(self, 'd_widget') and hasattr(self.d_widget, 'filler'):
|
||||||
|
self.d_widget.filler.setVisible(style != Qt.ToolButtonIconOnly)
|
||||||
|
|
||||||
|
def get_text_style(self):
|
||||||
|
style = Qt.ToolButtonTextUnderIcon
|
||||||
|
s = gprefs['toolbar_icon_size']
|
||||||
|
if s != 'off':
|
||||||
|
p = gprefs['toolbar_text']
|
||||||
|
if p == 'never':
|
||||||
|
style = Qt.ToolButtonIconOnly
|
||||||
|
elif p == 'auto' and self.preferred_width > self.width()+35:
|
||||||
|
style = Qt.ToolButtonIconOnly
|
||||||
|
return style
|
||||||
|
|
||||||
|
def contextMenuEvent(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_lm_actions(self):
|
||||||
|
for ac in self.added_actions:
|
||||||
|
if ac in self.location_manager.all_actions:
|
||||||
|
ac.setVisible(ac in self.location_manager.available_actions)
|
||||||
|
|
||||||
|
def init_bar(self, actions):
|
||||||
|
self.showing_donate = False
|
||||||
|
for ac in self.added_actions:
|
||||||
|
m = ac.menu()
|
||||||
|
if m is not None:
|
||||||
|
m.setVisible(False)
|
||||||
|
|
||||||
|
self.clear()
|
||||||
|
self.added_actions = []
|
||||||
|
|
||||||
|
bar = self
|
||||||
|
|
||||||
|
for what in actions:
|
||||||
|
if what is None:
|
||||||
|
bar.addSeparator()
|
||||||
|
elif what == 'Location Manager':
|
||||||
|
for ac in self.location_manager.all_actions:
|
||||||
|
bar.addAction(ac)
|
||||||
|
bar.added_actions.append(ac)
|
||||||
|
bar.setup_tool_button(bar, ac, QToolButton.MenuButtonPopup)
|
||||||
|
ac.setVisible(False)
|
||||||
|
elif what == 'Donate':
|
||||||
|
self.d_widget = QWidget()
|
||||||
|
self.d_widget.setLayout(QVBoxLayout())
|
||||||
|
self.d_widget.layout().addWidget(self.donate_button)
|
||||||
|
if isosx:
|
||||||
|
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
|
||||||
|
self.d_widget.layout().setContentsMargins(0,0,0,0)
|
||||||
|
self.d_widget.setContentsMargins(0,0,0,0)
|
||||||
|
self.d_widget.filler = QLabel(u'\u00a0')
|
||||||
|
self.d_widget.layout().addWidget(self.d_widget.filler)
|
||||||
|
bar.addWidget(self.d_widget)
|
||||||
|
self.showing_donate = True
|
||||||
|
elif what in self.gui.iactions:
|
||||||
|
action = self.gui.iactions[what]
|
||||||
|
bar.addAction(action.qaction)
|
||||||
|
self.added_actions.append(action.qaction)
|
||||||
|
self.setup_tool_button(bar, action.qaction, action.popup_type)
|
||||||
|
self.preferred_width = self.sizeHint().width()
|
||||||
|
|
||||||
|
def setup_tool_button(self, bar, ac, menu_mode=None):
|
||||||
|
ch = bar.widgetForAction(ac)
|
||||||
|
if ch is None:
|
||||||
|
ch = self.child_bar.widgetForAction(ac)
|
||||||
|
ch.setCursor(Qt.PointingHandCursor)
|
||||||
|
ch.setAutoRaise(True)
|
||||||
|
if ac.menu() is not None and menu_mode is not None:
|
||||||
|
ch.setPopupMode(menu_mode)
|
||||||
|
return ch
|
||||||
|
|
||||||
|
#support drag&drop from/to library from/to reader/card
|
||||||
|
def dragEnterEvent(self, event):
|
||||||
|
md = event.mimeData()
|
||||||
|
if md.hasFormat("application/calibre+from_library") or \
|
||||||
|
md.hasFormat("application/calibre+from_device"):
|
||||||
|
event.setDropAction(Qt.CopyAction)
|
||||||
|
event.accept()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
def dragMoveEvent(self, event):
|
||||||
|
allowed = False
|
||||||
|
md = event.mimeData()
|
||||||
|
#Drop is only allowed in the location manager widget's different from the selected one
|
||||||
|
for ac in self.location_manager.available_actions:
|
||||||
|
w = self.widgetForAction(ac)
|
||||||
|
if w is not None:
|
||||||
|
if ( md.hasFormat("application/calibre+from_library") or \
|
||||||
|
md.hasFormat("application/calibre+from_device") ) and \
|
||||||
|
w.geometry().contains(event.pos()) and \
|
||||||
|
isinstance(w, QToolButton) and not w.isChecked():
|
||||||
|
allowed = True
|
||||||
|
break
|
||||||
|
if allowed:
|
||||||
|
event.acceptProposedAction()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
def dropEvent(self, event):
|
||||||
|
data = event.mimeData()
|
||||||
|
|
||||||
|
mime = 'application/calibre+from_library'
|
||||||
|
if data.hasFormat(mime):
|
||||||
|
ids = list(map(int, str(data.data(mime)).split()))
|
||||||
|
tgt = None
|
||||||
|
for ac in self.location_manager.available_actions:
|
||||||
|
w = self.widgetForAction(ac)
|
||||||
|
if w is not None and w.geometry().contains(event.pos()):
|
||||||
|
tgt = ac.calibre_name
|
||||||
|
if tgt is not None:
|
||||||
|
if tgt == 'main':
|
||||||
|
tgt = None
|
||||||
|
self.gui.sync_to_device(tgt, False, send_ids=ids)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
mime = 'application/calibre+from_device'
|
||||||
|
if data.hasFormat(mime):
|
||||||
|
paths = [unicode(u.toLocalFile()) for u in data.urls()]
|
||||||
|
if paths:
|
||||||
|
self.gui.iactions['Add Books'].add_books_from_device(
|
||||||
|
self.gui.current_view(), paths=paths)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class MenuAction(QAction): # {{{
|
||||||
|
|
||||||
|
def __init__(self, clone, parent):
|
||||||
|
QAction.__init__(self, clone.text(), parent)
|
||||||
|
self.clone = clone
|
||||||
|
clone.changed.connect(self.clone_changed)
|
||||||
|
|
||||||
|
def clone_changed(self):
|
||||||
|
self.setText(self.clone.text())
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class MenuBar(QMenuBar): # {{{
|
||||||
|
|
||||||
|
def __init__(self, location_manager, parent):
|
||||||
|
QMenuBar.__init__(self, parent)
|
||||||
|
self.gui = parent
|
||||||
|
self.setNativeMenuBar(True)
|
||||||
|
|
||||||
|
self.location_manager = location_manager
|
||||||
|
self.added_actions = []
|
||||||
|
|
||||||
|
self.donate_action = QAction(_('Donate'), self)
|
||||||
|
self.donate_menu = QMenu()
|
||||||
|
self.donate_menu.addAction(self.gui.donate_action)
|
||||||
|
self.donate_action.setMenu(self.donate_menu)
|
||||||
|
|
||||||
|
def update_lm_actions(self):
|
||||||
|
for ac in self.added_actions:
|
||||||
|
if ac in self.location_manager.all_actions:
|
||||||
|
ac.setVisible(ac in self.location_manager.available_actions)
|
||||||
|
|
||||||
|
def init_bar(self, actions):
|
||||||
|
for ac in self.added_actions:
|
||||||
|
m = ac.menu()
|
||||||
|
if m is not None:
|
||||||
|
m.setVisible(False)
|
||||||
|
|
||||||
|
self.clear()
|
||||||
|
self.added_actions = []
|
||||||
|
|
||||||
|
for what in actions:
|
||||||
|
if what is None:
|
||||||
|
continue
|
||||||
|
elif what == 'Location Manager':
|
||||||
|
for ac in self.location_manager.all_actions:
|
||||||
|
ac = self.build_menu(ac)
|
||||||
|
self.addAction(ac)
|
||||||
|
self.added_actions.append(ac)
|
||||||
|
ac.setVisible(False)
|
||||||
|
elif what == 'Donate':
|
||||||
|
self.addAction(self.donate_action)
|
||||||
|
elif what in self.gui.iactions:
|
||||||
|
action = self.gui.iactions[what]
|
||||||
|
ac = self.build_menu(action.qaction)
|
||||||
|
self.addAction(ac)
|
||||||
|
self.added_actions.append(ac)
|
||||||
|
|
||||||
|
def build_menu(self, action):
|
||||||
|
m = action.menu()
|
||||||
|
ac = MenuAction(action, self)
|
||||||
|
if m is None:
|
||||||
|
m = QMenu()
|
||||||
|
m.addAction(action)
|
||||||
|
ac.setMenu(m)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class BarsManager(QObject):
|
||||||
|
|
||||||
|
def __init__(self, donate_button, location_manager, parent):
|
||||||
|
QObject.__init__(self, parent)
|
||||||
|
self.donate_button, self.location_manager = (donate_button,
|
||||||
|
location_manager)
|
||||||
|
|
||||||
|
bars = [ToolBar(donate_button, location_manager, parent) for i in
|
||||||
|
range(3)]
|
||||||
|
self.main_bars = tuple(bars[:2])
|
||||||
|
self.child_bars = tuple(bars[2:])
|
||||||
|
|
||||||
|
self.menu_bar = MenuBar(self.location_manager, self.parent())
|
||||||
|
self.parent().setMenuBar(self.menu_bar)
|
||||||
|
|
||||||
|
self.apply_settings()
|
||||||
|
self.init_bars()
|
||||||
|
|
||||||
|
def database_changed(self, db):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bars(self):
|
||||||
|
for x in self.main_bars + self.child_bars:
|
||||||
|
yield x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def showing_donate(self):
|
||||||
|
for b in self.bars:
|
||||||
|
if b.isVisible() and b.showing_donate:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def init_bars(self):
|
||||||
|
self.bar_actions = tuple(
|
||||||
|
[gprefs['action-layout-toolbar'+x] for x in ('', '-device')] +
|
||||||
|
[gprefs['action-layout-toolbar-child']] +
|
||||||
|
[gprefs['action-layout-menubar']] +
|
||||||
|
[gprefs['action-layout-menubar-device']]
|
||||||
|
)
|
||||||
|
|
||||||
|
for bar, actions in zip(self.bars, self.bar_actions[:3]):
|
||||||
|
bar.init_bar(actions)
|
||||||
|
|
||||||
|
def update_bars(self):
|
||||||
|
'''
|
||||||
|
This shows the correct main toolbar and rebuilds the menubar based on
|
||||||
|
whether a device is connected or not. Note that the toolbars are
|
||||||
|
explicitly not rebuilt, this is to workaround a Qt limitation iwth
|
||||||
|
QToolButton's popup menus and modal dialogs. If you want the toolbars
|
||||||
|
rebuilt, call init_bars().
|
||||||
|
'''
|
||||||
|
showing_device = self.location_manager.has_device
|
||||||
|
main_bar = self.main_bars[1 if showing_device else 0]
|
||||||
|
child_bar = self.child_bars[0]
|
||||||
|
for bar in self.bars:
|
||||||
|
bar.setVisible(False)
|
||||||
|
bar.update_lm_actions()
|
||||||
|
if main_bar.added_actions:
|
||||||
|
main_bar.setVisible(True)
|
||||||
|
if child_bar.added_actions:
|
||||||
|
child_bar.setVisible(True)
|
||||||
|
|
||||||
|
self.menu_bar.init_bar(self.bar_actions[4 if showing_device else 3])
|
||||||
|
self.menu_bar.update_lm_actions()
|
||||||
|
self.menu_bar.setVisible(bool(self.menu_bar.added_actions))
|
||||||
|
|
||||||
|
def apply_settings(self):
|
||||||
|
sz = gprefs['toolbar_icon_size']
|
||||||
|
sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz]
|
||||||
|
style = Qt.ToolButtonTextUnderIcon
|
||||||
|
if sz > 0 and gprefs['toolbar_text'] == 'never':
|
||||||
|
style = Qt.ToolButtonIconOnly
|
||||||
|
|
||||||
|
for bar in self.bars:
|
||||||
|
bar.setIconSize(QSize(sz, sz))
|
||||||
|
bar.setToolButtonStyle(style)
|
||||||
|
self.donate_button.set_normal_icon_size(sz, sz)
|
||||||
|
|
||||||
|
|
@ -751,6 +751,7 @@ class DeviceMixin(object): # {{{
|
|||||||
if self.current_view() != self.library_view:
|
if self.current_view() != self.library_view:
|
||||||
self.book_details.reset_info()
|
self.book_details.reset_info()
|
||||||
self.location_manager.update_devices()
|
self.location_manager.update_devices()
|
||||||
|
self.bars_manager.update_bars()
|
||||||
self.library_view.set_device_connected(self.device_connected)
|
self.library_view.set_device_connected(self.device_connected)
|
||||||
self.refresh_ondevice()
|
self.refresh_ondevice()
|
||||||
device_signals.device_connection_changed.emit(connected)
|
device_signals.device_connection_changed.emit(connected)
|
||||||
@ -764,6 +765,7 @@ class DeviceMixin(object): # {{{
|
|||||||
info, cp, fs = job.result
|
info, cp, fs = job.result
|
||||||
self.location_manager.update_devices(cp, fs,
|
self.location_manager.update_devices(cp, fs,
|
||||||
self.device_manager.device.icon)
|
self.device_manager.device.icon)
|
||||||
|
self.bars_manager.update_bars()
|
||||||
self.status_bar.device_connected(info[0])
|
self.status_bar.device_connected(info[0])
|
||||||
self.device_manager.books(Dispatcher(self.metadata_downloaded))
|
self.device_manager.books(Dispatcher(self.metadata_downloaded))
|
||||||
|
|
||||||
|
@ -78,22 +78,22 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QGridLayout">
|
||||||
<item>
|
<item row="0" column="0">
|
||||||
<widget class="QPushButton" name="sort_by_author">
|
<widget class="QPushButton" name="sort_by_author">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Sort by author</string>
|
<string>Sort by author</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="0" column="1">
|
||||||
<widget class="QPushButton" name="sort_by_author_sort">
|
<widget class="QPushButton" name="sort_by_author_sort">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Sort by author sort</string>
|
<string>Sort by author sort</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="0">
|
||||||
<widget class="QPushButton" name="recalc_author_sort">
|
<widget class="QPushButton" name="recalc_author_sort">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Reset all the author sort values to a value automatically
|
<string>Reset all the author sort values to a value automatically
|
||||||
@ -105,7 +105,7 @@ generated can be controlled via Preferences->Advanced->Tweaks</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="1">
|
||||||
<widget class="QPushButton" name="auth_sort_to_author">
|
<widget class="QPushButton" name="auth_sort_to_author">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Copy author sort to author for every author. You typically use this button
|
<string>Copy author sort to author for every author. You typically use this button
|
||||||
@ -116,20 +116,7 @@ after changing Preferences->Advanced->Tweaks->Author sort name algorith
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="2">
|
||||||
<spacer name="horizontalSpacer_3">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||||
|
@ -7,15 +7,15 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import (QIcon, Qt, QWidget, QToolBar, QSize,
|
from PyQt4.Qt import (QIcon, Qt, QWidget, QSize,
|
||||||
pyqtSignal, QToolButton, QMenu, QMenuBar, QAction,
|
pyqtSignal, QToolButton, QMenu,
|
||||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
|
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
|
||||||
|
|
||||||
|
|
||||||
from calibre.constants import __appname__, isosx
|
from calibre.constants import __appname__
|
||||||
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
|
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
|
||||||
from calibre.gui2.throbber import ThrobbingButton
|
from calibre.gui2.throbber import ThrobbingButton
|
||||||
from calibre.gui2 import gprefs
|
from calibre.gui2.bars import BarsManager
|
||||||
from calibre.gui2.widgets import ComboBoxWithHelp
|
from calibre.gui2.widgets import ComboBoxWithHelp
|
||||||
from calibre import human_readable
|
from calibre import human_readable
|
||||||
|
|
||||||
@ -35,6 +35,8 @@ class LocationManager(QObject): # {{{
|
|||||||
self._mem = []
|
self._mem = []
|
||||||
self.tooltips = {}
|
self.tooltips = {}
|
||||||
|
|
||||||
|
self.all_actions = []
|
||||||
|
|
||||||
def ac(name, text, icon, tooltip):
|
def ac(name, text, icon, tooltip):
|
||||||
icon = QIcon(I(icon))
|
icon = QIcon(I(icon))
|
||||||
ac = self.location_actions.addAction(icon, text)
|
ac = self.location_actions.addAction(icon, text)
|
||||||
@ -44,7 +46,7 @@ class LocationManager(QObject): # {{{
|
|||||||
receiver = partial(self._location_selected, name)
|
receiver = partial(self._location_selected, name)
|
||||||
ac.triggered.connect(receiver)
|
ac.triggered.connect(receiver)
|
||||||
self.tooltips[name] = tooltip
|
self.tooltips[name] = tooltip
|
||||||
|
|
||||||
m = QMenu(parent)
|
m = QMenu(parent)
|
||||||
self._mem.append(m)
|
self._mem.append(m)
|
||||||
a = m.addAction(icon, tooltip)
|
a = m.addAction(icon, tooltip)
|
||||||
@ -59,6 +61,7 @@ class LocationManager(QObject): # {{{
|
|||||||
ac.setMenu(m)
|
ac.setMenu(m)
|
||||||
ac.calibre_name = name
|
ac.calibre_name = name
|
||||||
|
|
||||||
|
self.all_actions.append(ac)
|
||||||
return ac
|
return ac
|
||||||
|
|
||||||
self.library_action = ac('library', _('Library'), 'lt.png',
|
self.library_action = ac('library', _('Library'), 'lt.png',
|
||||||
@ -77,7 +80,7 @@ class LocationManager(QObject): # {{{
|
|||||||
self.switch_menu.addSeparator()
|
self.switch_menu.addSeparator()
|
||||||
else:
|
else:
|
||||||
self.switch_menu = QMenu()
|
self.switch_menu = QMenu()
|
||||||
|
|
||||||
self.switch_menu.addAction(choose_action)
|
self.switch_menu.addAction(choose_action)
|
||||||
self.cs_menus = []
|
self.cs_menus = []
|
||||||
for t, acs in [(_('Quick switch'), quick_actions),
|
for t, acs in [(_('Quick switch'), quick_actions),
|
||||||
@ -91,7 +94,7 @@ class LocationManager(QObject): # {{{
|
|||||||
self.switch_menu.addSeparator()
|
self.switch_menu.addSeparator()
|
||||||
for ac in switch_actions:
|
for ac in switch_actions:
|
||||||
self.switch_menu.addAction(ac)
|
self.switch_menu.addAction(ac)
|
||||||
|
|
||||||
if self.switch_menu != self.library_action.menu():
|
if self.switch_menu != self.library_action.menu():
|
||||||
self.library_action.setMenu(self.switch_menu)
|
self.library_action.setMenu(self.switch_menu)
|
||||||
|
|
||||||
@ -234,259 +237,6 @@ class Spacer(QWidget): # {{{
|
|||||||
self.l.addStretch(10)
|
self.l.addStretch(10)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class MenuAction(QAction): # {{{
|
|
||||||
|
|
||||||
def __init__(self, clone, parent):
|
|
||||||
QAction.__init__(self, clone.text(), parent)
|
|
||||||
self.clone = clone
|
|
||||||
clone.changed.connect(self.clone_changed)
|
|
||||||
|
|
||||||
def clone_changed(self):
|
|
||||||
self.setText(self.clone.text())
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class MenuBar(QMenuBar): # {{{
|
|
||||||
|
|
||||||
def __init__(self, location_manager, parent):
|
|
||||||
QMenuBar.__init__(self, parent)
|
|
||||||
self.gui = parent
|
|
||||||
self.setNativeMenuBar(True)
|
|
||||||
|
|
||||||
self.location_manager = location_manager
|
|
||||||
self.location_manager.locations_changed.connect(self.build_bar)
|
|
||||||
self.added_actions = []
|
|
||||||
|
|
||||||
self.donate_action = QAction(_('Donate'), self)
|
|
||||||
self.donate_menu = QMenu()
|
|
||||||
self.donate_menu.addAction(self.gui.donate_action)
|
|
||||||
self.donate_action.setMenu(self.donate_menu)
|
|
||||||
self.build_bar()
|
|
||||||
|
|
||||||
def build_bar(self, changed_action=None):
|
|
||||||
showing_device = self.location_manager.has_device
|
|
||||||
actions = '-device' if showing_device else ''
|
|
||||||
actions = gprefs['action-layout-menubar'+actions]
|
|
||||||
|
|
||||||
show_main = len(actions) > 0
|
|
||||||
self.setVisible(show_main)
|
|
||||||
|
|
||||||
for ac in self.added_actions:
|
|
||||||
m = ac.menu()
|
|
||||||
if m is not None:
|
|
||||||
m.setVisible(False)
|
|
||||||
|
|
||||||
self.clear()
|
|
||||||
self.added_actions = []
|
|
||||||
self.action_map = {}
|
|
||||||
|
|
||||||
for what in actions:
|
|
||||||
if what is None:
|
|
||||||
continue
|
|
||||||
elif what == 'Location Manager':
|
|
||||||
for ac in self.location_manager.available_actions:
|
|
||||||
ac = self.build_menu(ac)
|
|
||||||
self.addAction(ac)
|
|
||||||
self.added_actions.append(ac)
|
|
||||||
elif what == 'Donate':
|
|
||||||
self.addAction(self.donate_action)
|
|
||||||
elif what in self.gui.iactions:
|
|
||||||
action = self.gui.iactions[what]
|
|
||||||
ac = self.build_menu(action.qaction)
|
|
||||||
self.addAction(ac)
|
|
||||||
self.added_actions.append(ac)
|
|
||||||
|
|
||||||
def build_menu(self, action):
|
|
||||||
m = action.menu()
|
|
||||||
ac = MenuAction(action, self)
|
|
||||||
if m is None:
|
|
||||||
m = QMenu()
|
|
||||||
m.addAction(action)
|
|
||||||
ac.setMenu(m)
|
|
||||||
return ac
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class BaseToolBar(QToolBar): # {{{
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
QToolBar.__init__(self, parent)
|
|
||||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
|
||||||
self.setMovable(False)
|
|
||||||
self.setFloatable(False)
|
|
||||||
self.setOrientation(Qt.Horizontal)
|
|
||||||
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
|
|
||||||
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
|
|
||||||
self.preferred_width = self.sizeHint().width()
|
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
|
||||||
QToolBar.resizeEvent(self, ev)
|
|
||||||
style = self.get_text_style()
|
|
||||||
self.setToolButtonStyle(style)
|
|
||||||
if hasattr(self, 'd_widget') and hasattr(self.d_widget, 'filler'):
|
|
||||||
self.d_widget.filler.setVisible(style != Qt.ToolButtonIconOnly)
|
|
||||||
|
|
||||||
def get_text_style(self):
|
|
||||||
style = Qt.ToolButtonTextUnderIcon
|
|
||||||
s = gprefs['toolbar_icon_size']
|
|
||||||
if s != 'off':
|
|
||||||
p = gprefs['toolbar_text']
|
|
||||||
if p == 'never':
|
|
||||||
style = Qt.ToolButtonIconOnly
|
|
||||||
elif p == 'auto' and self.preferred_width > self.width()+35:
|
|
||||||
style = Qt.ToolButtonIconOnly
|
|
||||||
return style
|
|
||||||
|
|
||||||
def contextMenuEvent(self, *args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class ToolBar(BaseToolBar): # {{{
|
|
||||||
|
|
||||||
def __init__(self, donate, location_manager, child_bar, parent):
|
|
||||||
BaseToolBar.__init__(self, parent)
|
|
||||||
self.gui = parent
|
|
||||||
self.child_bar = child_bar
|
|
||||||
self.donate_button = donate
|
|
||||||
self.apply_settings()
|
|
||||||
|
|
||||||
self.location_manager = location_manager
|
|
||||||
self.location_manager.locations_changed.connect(self.build_bar)
|
|
||||||
donate.setAutoRaise(True)
|
|
||||||
donate.setCursor(Qt.PointingHandCursor)
|
|
||||||
self.added_actions = []
|
|
||||||
self.build_bar()
|
|
||||||
self.setAcceptDrops(True)
|
|
||||||
|
|
||||||
def apply_settings(self):
|
|
||||||
sz = gprefs['toolbar_icon_size']
|
|
||||||
sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz]
|
|
||||||
self.setIconSize(QSize(sz, sz))
|
|
||||||
self.child_bar.setIconSize(QSize(sz, sz))
|
|
||||||
style = Qt.ToolButtonTextUnderIcon
|
|
||||||
if sz > 0 and gprefs['toolbar_text'] == 'never':
|
|
||||||
style = Qt.ToolButtonIconOnly
|
|
||||||
self.setToolButtonStyle(style)
|
|
||||||
self.child_bar.setToolButtonStyle(style)
|
|
||||||
self.donate_button.set_normal_icon_size(sz, sz)
|
|
||||||
|
|
||||||
def build_bar(self):
|
|
||||||
self.showing_donate = False
|
|
||||||
showing_device = self.location_manager.has_device
|
|
||||||
mactions = '-device' if showing_device else ''
|
|
||||||
mactions = gprefs['action-layout-toolbar'+mactions]
|
|
||||||
cactions = gprefs['action-layout-toolbar-child']
|
|
||||||
|
|
||||||
show_main = len(mactions) > 0
|
|
||||||
self.setVisible(show_main)
|
|
||||||
show_child = len(cactions) > 0
|
|
||||||
self.child_bar.setVisible(show_child)
|
|
||||||
|
|
||||||
for ac in self.added_actions:
|
|
||||||
m = ac.menu()
|
|
||||||
if m is not None:
|
|
||||||
m.setVisible(False)
|
|
||||||
|
|
||||||
self.clear()
|
|
||||||
self.child_bar.clear()
|
|
||||||
self.added_actions = []
|
|
||||||
|
|
||||||
for bar, actions in ((self, mactions), (self.child_bar, cactions)):
|
|
||||||
for what in actions:
|
|
||||||
if what is None:
|
|
||||||
bar.addSeparator()
|
|
||||||
elif what == 'Location Manager':
|
|
||||||
for ac in self.location_manager.available_actions:
|
|
||||||
bar.addAction(ac)
|
|
||||||
bar.added_actions.append(ac)
|
|
||||||
bar.setup_tool_button(bar, ac, QToolButton.MenuButtonPopup)
|
|
||||||
elif what == 'Donate':
|
|
||||||
self.d_widget = QWidget()
|
|
||||||
self.d_widget.setLayout(QVBoxLayout())
|
|
||||||
self.d_widget.layout().addWidget(self.donate_button)
|
|
||||||
if isosx:
|
|
||||||
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
|
|
||||||
self.d_widget.layout().setContentsMargins(0,0,0,0)
|
|
||||||
self.d_widget.setContentsMargins(0,0,0,0)
|
|
||||||
self.d_widget.filler = QLabel(u'\u00a0')
|
|
||||||
self.d_widget.layout().addWidget(self.d_widget.filler)
|
|
||||||
bar.addWidget(self.d_widget)
|
|
||||||
self.showing_donate = True
|
|
||||||
elif what in self.gui.iactions:
|
|
||||||
action = self.gui.iactions[what]
|
|
||||||
bar.addAction(action.qaction)
|
|
||||||
self.added_actions.append(action.qaction)
|
|
||||||
self.setup_tool_button(bar, action.qaction, action.popup_type)
|
|
||||||
self.preferred_width = self.sizeHint().width()
|
|
||||||
self.child_bar.preferred_width = self.child_bar.sizeHint().width()
|
|
||||||
|
|
||||||
def setup_tool_button(self, bar, ac, menu_mode=None):
|
|
||||||
ch = bar.widgetForAction(ac)
|
|
||||||
if ch is None:
|
|
||||||
ch = self.child_bar.widgetForAction(ac)
|
|
||||||
ch.setCursor(Qt.PointingHandCursor)
|
|
||||||
ch.setAutoRaise(True)
|
|
||||||
if ac.menu() is not None and menu_mode is not None:
|
|
||||||
ch.setPopupMode(menu_mode)
|
|
||||||
return ch
|
|
||||||
|
|
||||||
def database_changed(self, db):
|
|
||||||
pass
|
|
||||||
|
|
||||||
#support drag&drop from/to library from/to reader/card
|
|
||||||
def dragEnterEvent(self, event):
|
|
||||||
md = event.mimeData()
|
|
||||||
if md.hasFormat("application/calibre+from_library") or \
|
|
||||||
md.hasFormat("application/calibre+from_device"):
|
|
||||||
event.setDropAction(Qt.CopyAction)
|
|
||||||
event.accept()
|
|
||||||
else:
|
|
||||||
event.ignore()
|
|
||||||
|
|
||||||
def dragMoveEvent(self, event):
|
|
||||||
allowed = False
|
|
||||||
md = event.mimeData()
|
|
||||||
#Drop is only allowed in the location manager widget's different from the selected one
|
|
||||||
for ac in self.location_manager.available_actions:
|
|
||||||
w = self.widgetForAction(ac)
|
|
||||||
if w is not None:
|
|
||||||
if ( md.hasFormat("application/calibre+from_library") or \
|
|
||||||
md.hasFormat("application/calibre+from_device") ) and \
|
|
||||||
w.geometry().contains(event.pos()) and \
|
|
||||||
isinstance(w, QToolButton) and not w.isChecked():
|
|
||||||
allowed = True
|
|
||||||
break
|
|
||||||
if allowed:
|
|
||||||
event.acceptProposedAction()
|
|
||||||
else:
|
|
||||||
event.ignore()
|
|
||||||
|
|
||||||
def dropEvent(self, event):
|
|
||||||
data = event.mimeData()
|
|
||||||
|
|
||||||
mime = 'application/calibre+from_library'
|
|
||||||
if data.hasFormat(mime):
|
|
||||||
ids = list(map(int, str(data.data(mime)).split()))
|
|
||||||
tgt = None
|
|
||||||
for ac in self.location_manager.available_actions:
|
|
||||||
w = self.widgetForAction(ac)
|
|
||||||
if w is not None and w.geometry().contains(event.pos()):
|
|
||||||
tgt = ac.calibre_name
|
|
||||||
if tgt is not None:
|
|
||||||
if tgt == 'main':
|
|
||||||
tgt = None
|
|
||||||
self.gui.sync_to_device(tgt, False, send_ids=ids)
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
mime = 'application/calibre+from_device'
|
|
||||||
if data.hasFormat(mime):
|
|
||||||
paths = [unicode(u.toLocalFile()) for u in data.urls()]
|
|
||||||
if paths:
|
|
||||||
self.gui.iactions['Add Books'].add_books_from_device(
|
|
||||||
self.gui.current_view(), paths=paths)
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class MainWindowMixin(object): # {{{
|
class MainWindowMixin(object): # {{{
|
||||||
|
|
||||||
@ -507,13 +257,13 @@ class MainWindowMixin(object): # {{{
|
|||||||
self.iactions['Fetch News'].init_scheduler(db)
|
self.iactions['Fetch News'].init_scheduler(db)
|
||||||
|
|
||||||
self.search_bar = SearchBar(self)
|
self.search_bar = SearchBar(self)
|
||||||
self.child_bar = BaseToolBar(self)
|
self.bars_manager = BarsManager(self.donate_button,
|
||||||
self.tool_bar = ToolBar(self.donate_button,
|
self.location_manager, self)
|
||||||
self.location_manager, self.child_bar, self)
|
for bar in self.bars_manager.main_bars:
|
||||||
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
self.addToolBar(Qt.TopToolBarArea, bar)
|
||||||
self.addToolBar(Qt.BottomToolBarArea, self.child_bar)
|
for bar in self.bars_manager.child_bars:
|
||||||
self.menu_bar = MenuBar(self.location_manager, self)
|
self.addToolBar(Qt.BottomToolBarArea, bar)
|
||||||
self.setMenuBar(self.menu_bar)
|
self.bars_manager.update_bars()
|
||||||
self.setUnifiedTitleAndToolBarOnMac(True)
|
self.setUnifiedTitleAndToolBarOnMac(True)
|
||||||
|
|
||||||
l = self.centralwidget.layout()
|
l = self.centralwidget.layout()
|
||||||
|
@ -361,10 +361,9 @@ class Preferences(QMainWindow):
|
|||||||
self.gui.tags_view.recount()
|
self.gui.tags_view.recount()
|
||||||
self.gui.create_device_menu()
|
self.gui.create_device_menu()
|
||||||
self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
|
self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
|
||||||
self.gui.tool_bar.build_bar()
|
self.gui.bars_manager.apply_settings()
|
||||||
self.gui.menu_bar.build_bar()
|
self.gui.bars_manager.update_bars()
|
||||||
self.gui.build_context_menus()
|
self.gui.build_context_menus()
|
||||||
self.gui.tool_bar.apply_settings()
|
|
||||||
|
|
||||||
return QMainWindow.closeEvent(self, *args)
|
return QMainWindow.closeEvent(self, *args)
|
||||||
|
|
||||||
|
@ -5,17 +5,20 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QLineEdit, QComboBox, SIGNAL, QListWidgetItem
|
from PyQt4.Qt import Qt, QLineEdit, QComboBox, SIGNAL, QListWidgetItem
|
||||||
|
|
||||||
from calibre.customize.ui import is_disabled
|
from calibre.customize.ui import is_disabled
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog, question_dialog
|
||||||
from calibre.gui2.device import device_name_for_plugboards
|
from calibre.gui2.device import device_name_for_plugboards
|
||||||
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
||||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||||
from calibre.gui2.preferences.plugboard_ui import Ui_Form
|
from calibre.gui2.preferences.plugboard_ui import Ui_Form
|
||||||
from calibre.customize.ui import metadata_writers, device_plugins
|
from calibre.customize.ui import metadata_writers, device_plugins
|
||||||
from calibre.library.save_to_disk import plugboard_any_format_value, \
|
from calibre.library.save_to_disk import plugboard_any_format_value, \
|
||||||
plugboard_any_device_value, plugboard_save_to_disk_value
|
plugboard_any_device_value, plugboard_save_to_disk_value, \
|
||||||
|
find_plugboard
|
||||||
from calibre.library.server.content import plugboard_content_server_value, \
|
from calibre.library.server.content import plugboard_content_server_value, \
|
||||||
plugboard_content_server_formats
|
plugboard_content_server_formats
|
||||||
from calibre.utils.formatter import validation_formatter
|
from calibre.utils.formatter import validation_formatter
|
||||||
@ -46,9 +49,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
def genesis(self, gui):
|
def genesis(self, gui):
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
self.db = gui.library_view.model().db
|
self.db = gui.library_view.model().db
|
||||||
self.current_plugboards = self.db.prefs.get('plugboards',{})
|
|
||||||
self.current_device = None
|
|
||||||
self.current_format = None
|
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
def field_cmp(x, y):
|
def field_cmp(x, y):
|
||||||
@ -64,6 +64,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
|
|
||||||
ConfigWidgetBase.initialize(self)
|
ConfigWidgetBase.initialize(self)
|
||||||
|
|
||||||
|
self.current_plugboards = copy.deepcopy(self.db.prefs.get('plugboards',{}))
|
||||||
|
self.current_device = None
|
||||||
|
self.current_format = None
|
||||||
|
|
||||||
if self.gui.device_manager.connected_device is not None:
|
if self.gui.device_manager.connected_device is not None:
|
||||||
self.device_label.setText(_('Device currently connected: ') +
|
self.device_label.setText(_('Device currently connected: ') +
|
||||||
self.gui.device_manager.connected_device.__class__.__name__)
|
self.gui.device_manager.connected_device.__class__.__name__)
|
||||||
@ -196,51 +200,66 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
return
|
return
|
||||||
self.clear_fields(edit_boxes=True)
|
self.clear_fields(edit_boxes=True)
|
||||||
self.current_device = unicode(txt)
|
self.current_device = unicode(txt)
|
||||||
error = False
|
|
||||||
if self.current_format == plugboard_any_format_value:
|
|
||||||
# user specified any format.
|
|
||||||
for f in self.current_plugboards:
|
|
||||||
devs = set(self.current_plugboards[f])
|
|
||||||
if self.current_device != plugboard_save_to_disk_value and \
|
|
||||||
plugboard_any_device_value in devs:
|
|
||||||
# specific format/any device in list. conflict.
|
|
||||||
# note: any device does not match save_to_disk
|
|
||||||
error = True
|
|
||||||
break
|
|
||||||
if self.current_device in devs:
|
|
||||||
# specific format/current device in list. conflict
|
|
||||||
error = True
|
|
||||||
break
|
|
||||||
if self.current_device == plugboard_any_device_value:
|
|
||||||
# any device and a specific device already there. conflict
|
|
||||||
error = True
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# user specified specific format.
|
|
||||||
for f in self.current_plugboards:
|
|
||||||
devs = set(self.current_plugboards[f])
|
|
||||||
if f == plugboard_any_format_value and \
|
|
||||||
self.current_device in devs:
|
|
||||||
# any format/same device in list. conflict.
|
|
||||||
error = True
|
|
||||||
break
|
|
||||||
if f == self.current_format and self.current_device in devs:
|
|
||||||
# current format/current device in list. conflict
|
|
||||||
error = True
|
|
||||||
break
|
|
||||||
if f == self.current_format and plugboard_any_device_value in devs:
|
|
||||||
# current format/any device in list. conflict
|
|
||||||
error = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if error:
|
if self.current_format in self.current_plugboards and \
|
||||||
|
self.current_device in self.current_plugboards[self.current_format]:
|
||||||
error_dialog(self, '',
|
error_dialog(self, '',
|
||||||
_('That format and device already has a plugboard or '
|
_('That format and device already has a plugboard.'),
|
||||||
'conflicts with another plugboard.'),
|
|
||||||
show=True)
|
show=True)
|
||||||
self.new_device.setCurrentIndex(0)
|
self.new_device.setCurrentIndex(0)
|
||||||
return
|
return
|
||||||
if self.current_device in self.device_to_formats_map:
|
|
||||||
|
# If we have a specific format/device combination, check if a more
|
||||||
|
# general combination matches.
|
||||||
|
if self.current_format != plugboard_any_format_value and \
|
||||||
|
self.current_device != plugboard_any_device_value:
|
||||||
|
if find_plugboard(self.current_device, self.current_format,
|
||||||
|
self.current_plugboards):
|
||||||
|
if not question_dialog(self.gui,
|
||||||
|
_('Possibly override plugboard?'),
|
||||||
|
_('A more general plugboard already exists for '
|
||||||
|
'that format and device. '
|
||||||
|
'Are you sure you want to add the new plugboard?')):
|
||||||
|
self.new_device.setCurrentIndex(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we have a specific format, check if we are adding a possibly-
|
||||||
|
# covered plugboard
|
||||||
|
if self.current_format != plugboard_any_format_value:
|
||||||
|
if self.current_format in self.current_plugboards:
|
||||||
|
if self.current_device == plugboard_any_device_value:
|
||||||
|
if not question_dialog(self.gui,
|
||||||
|
_('Add possibly overridden plugboard?'),
|
||||||
|
_('More specific device plugboards exist for '
|
||||||
|
'that format. '
|
||||||
|
'Are you sure you want to add the new plugboard?')):
|
||||||
|
self.new_device.setCurrentIndex(0)
|
||||||
|
return
|
||||||
|
# We are adding an 'any format' entry. Check if we are adding a specific
|
||||||
|
# device and if so, does some other plugboard match that device.
|
||||||
|
elif self.current_device != plugboard_any_device_value:
|
||||||
|
for fmt in self.current_plugboards:
|
||||||
|
if find_plugboard(self.current_device, fmt, self.current_plugboards):
|
||||||
|
if not question_dialog(self.gui,
|
||||||
|
_('Really add plugboard?'),
|
||||||
|
_('A different plugboard matches that format and '
|
||||||
|
'device combination. '
|
||||||
|
'Are you sure you want to add the new plugboard?')):
|
||||||
|
self.new_device.setCurrentIndex(0)
|
||||||
|
return
|
||||||
|
# We are adding an any format/any device entry, which will be overridden
|
||||||
|
# by any other entry. Ask if such entries exist.
|
||||||
|
elif len(self.current_plugboards):
|
||||||
|
if not question_dialog(self.gui,
|
||||||
|
_('Add possibly overridden plugboard?'),
|
||||||
|
_('More specific format and device plugboards '
|
||||||
|
'already exist. '
|
||||||
|
'Are you sure you want to add the new plugboard?')):
|
||||||
|
self.new_device.setCurrentIndex(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.current_format != plugboard_any_format_value and \
|
||||||
|
self.current_device in self.device_to_formats_map:
|
||||||
allowable_formats = self.device_to_formats_map[self.current_device]
|
allowable_formats = self.device_to_formats_map[self.current_device]
|
||||||
if self.current_format not in allowable_formats:
|
if self.current_format not in allowable_formats:
|
||||||
error_dialog(self, '',
|
error_dialog(self, '',
|
||||||
|
@ -317,6 +317,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
am.restore_defaults()
|
am.restore_defaults()
|
||||||
self.changed_signal.emit()
|
self.changed_signal.emit()
|
||||||
|
|
||||||
|
def refresh_gui(self, gui):
|
||||||
|
gui.bars_manager.init_bars()
|
||||||
|
gui.bars_manager.update_bars()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from PyQt4.Qt import QApplication
|
from PyQt4.Qt import QApplication
|
||||||
|
93
src/calibre/gui2/store/google_books_plugin.py
Normal file
93
src/calibre/gui2/store/google_books_plugin.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import urllib
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
from lxml import html
|
||||||
|
|
||||||
|
from PyQt4.Qt import QUrl
|
||||||
|
|
||||||
|
from calibre import browser, url_slash_cleaner
|
||||||
|
from calibre.gui2 import open_url
|
||||||
|
from calibre.gui2.store import StorePlugin
|
||||||
|
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||||
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
|
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||||
|
|
||||||
|
class GoogleBooksStore(BasicStoreConfig, StorePlugin):
|
||||||
|
|
||||||
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
|
url = 'http://books.google.com/'
|
||||||
|
|
||||||
|
if external or self.config.get('open_external', False):
|
||||||
|
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||||
|
else:
|
||||||
|
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||||
|
d.setWindowTitle(self.name)
|
||||||
|
d.set_tags(self.config.get('tags', ''))
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def search(self, query, max_results=10, timeout=60):
|
||||||
|
url = 'http://www.google.com/search?tbm=bks&q=' + urllib.quote_plus(query)
|
||||||
|
|
||||||
|
br = browser()
|
||||||
|
|
||||||
|
counter = max_results
|
||||||
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
|
doc = html.fromstring(f.read())
|
||||||
|
for data in doc.xpath('//ol[@id="rso"]/li'):
|
||||||
|
if counter <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
id = ''.join(data.xpath('.//h3/a/@href'))
|
||||||
|
if not id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = ''.join(data.xpath('.//h3/a//text()'))
|
||||||
|
authors = data.xpath('.//span[@class="gl"]//a//text()')
|
||||||
|
if authors[-1].strip().lower() == 'preview':
|
||||||
|
authors = authors[:-1]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
author = ', '.join(authors)
|
||||||
|
|
||||||
|
counter -= 1
|
||||||
|
|
||||||
|
s = SearchResult()
|
||||||
|
s.title = title.strip()
|
||||||
|
s.author = author.strip()
|
||||||
|
s.detail_item = id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNKNOWN
|
||||||
|
|
||||||
|
yield s
|
||||||
|
|
||||||
|
def get_details(self, search_result, timeout):
|
||||||
|
br = browser()
|
||||||
|
with closing(br.open(search_result.detail_item, timeout=timeout)) as nf:
|
||||||
|
doc = html.fromstring(nf.read())
|
||||||
|
|
||||||
|
search_result.cover_url = ''.join(doc.xpath('//div[@class="sidebarcover"]//img/@src'))
|
||||||
|
|
||||||
|
# Try to get the set price.
|
||||||
|
price = ''.join(doc.xpath('//div[@class="buy-price-container"]/span[contains(@class, "buy-price")]/text()'))
|
||||||
|
# Try to get the price inside of a buy button.
|
||||||
|
if not price.strip():
|
||||||
|
price = ''.join(doc.xpath('//div[@class="buy-container"]/a/text()'))
|
||||||
|
price = price.split('-')[-1]
|
||||||
|
# No price set for this book.
|
||||||
|
if not price.strip():
|
||||||
|
price = '$0.00'
|
||||||
|
search_result.price = price.strip()
|
||||||
|
|
||||||
|
search_result.formats = ', '.join(doc.xpath('//div[contains(@class, "download-panel-div")]//a/text()')).upper()
|
||||||
|
if not search_result.formats:
|
||||||
|
search_result.formats = _('Unknown')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
@ -26,7 +26,7 @@ class NextoStore(BasicStoreConfig, StorePlugin):
|
|||||||
def open(self, parent=None, detail_item=None, external=False):
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
pid = '155711'
|
pid = '155711'
|
||||||
|
|
||||||
url = 'http://www.nexto.pl/ebooki_c1015.xml?pid=' + pid
|
url = 'http://www.nexto.pl/ebooki_c1015.xml'
|
||||||
detail_url = None
|
detail_url = None
|
||||||
|
|
||||||
if detail_item:
|
if detail_item:
|
||||||
|
@ -52,7 +52,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
stores_check_widget.setLayout(stores_group_layout)
|
stores_check_widget.setLayout(stores_group_layout)
|
||||||
for x in sorted(self.store_plugins.keys(), key=lambda x: x.lower()):
|
for x in sorted(self.store_plugins.keys(), key=lambda x: x.lower()):
|
||||||
cbox = QCheckBox(x)
|
cbox = QCheckBox(x)
|
||||||
cbox.setChecked(True)
|
cbox.setChecked(False)
|
||||||
stores_group_layout.addWidget(cbox)
|
stores_group_layout.addWidget(cbox)
|
||||||
setattr(self, 'store_check_' + x, cbox)
|
setattr(self, 'store_check_' + x, cbox)
|
||||||
stores_group_layout.addStretch()
|
stores_group_layout.addStretch()
|
||||||
|
@ -44,40 +44,70 @@ class WizardsTowerBooksStore(BasicStoreConfig, StorePlugin):
|
|||||||
counter = max_results
|
counter = max_results
|
||||||
with closing(br.open(url, timeout=timeout)) as f:
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
doc = html.fromstring(f.read())
|
doc = html.fromstring(f.read())
|
||||||
for data in doc.xpath('//table[@class="gridp"]//td'):
|
if 'search.html' in f.geturl():
|
||||||
if counter <= 0:
|
for data in doc.xpath('//table[@class="gridp"]//td'):
|
||||||
break
|
if counter <= 0:
|
||||||
|
break
|
||||||
id = ''.join(data.xpath('.//span[@class="prti"]/a/@href'))
|
|
||||||
id = id.strip()
|
id = ''.join(data.xpath('.//span[@class="prti"]/a/@href'))
|
||||||
if not id:
|
id = id.strip()
|
||||||
continue
|
if not id:
|
||||||
|
continue
|
||||||
cover_url = ''.join(data.xpath('.//div[@class="prim"]/a/img/@src'))
|
|
||||||
cover_url = url_slash_cleaner(self.url + cover_url.strip())
|
cover_url = ''.join(data.xpath('.//div[@class="prim"]/a/img/@src'))
|
||||||
|
cover_url = url_slash_cleaner(self.url + cover_url.strip())
|
||||||
price = ''.join(data.xpath('.//font[@class="selling_price"]//text()'))
|
|
||||||
price = price.strip()
|
price = ''.join(data.xpath('.//font[@class="selling_price"]//text()'))
|
||||||
if not price:
|
price = price.strip()
|
||||||
continue
|
if not price:
|
||||||
|
continue
|
||||||
title = ''.join(data.xpath('.//span[@class="prti"]/a/b/text()'))
|
|
||||||
author = ''.join(data.xpath('.//p[@class="last"]/text()'))
|
title = ''.join(data.xpath('.//span[@class="prti"]/a/b/text()'))
|
||||||
a, b, author = author.partition(' by ')
|
author = ''.join(data.xpath('.//p[@class="last"]/text()'))
|
||||||
|
a, b, author = author.partition(' by ')
|
||||||
counter -= 1
|
|
||||||
|
counter -= 1
|
||||||
|
|
||||||
|
s = SearchResult()
|
||||||
|
s.cover_url = cover_url
|
||||||
|
s.title = title.strip()
|
||||||
|
s.author = author.strip()
|
||||||
|
s.price = price.strip()
|
||||||
|
s.detail_item = id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
|
||||||
|
yield s
|
||||||
|
# Exact match brought us to the books detail page.
|
||||||
|
else:
|
||||||
s = SearchResult()
|
s = SearchResult()
|
||||||
s.cover_url = cover_url
|
|
||||||
s.title = title.strip()
|
cover_url = ''.join(doc.xpath('//div[@id="image"]/a/img[@title="Zoom"]/@src')).strip()
|
||||||
s.author = author.strip()
|
s.cover_url = url_slash_cleaner(self.url + cover_url.strip())
|
||||||
s.price = price.strip()
|
|
||||||
s.detail_item = id.strip()
|
s.title = ''.join(doc.xpath('//form[@name="details"]/h1/text()')).strip()
|
||||||
|
|
||||||
|
authors = doc.xpath('//p[contains(., "Author:")]//text()')
|
||||||
|
author_index = None
|
||||||
|
for i, a in enumerate(authors):
|
||||||
|
if 'author' in a.lower():
|
||||||
|
author_index = i + 1
|
||||||
|
break
|
||||||
|
if author_index is not None and len(authors) > author_index:
|
||||||
|
a = authors[author_index]
|
||||||
|
a = a.replace(u'\xa0', '')
|
||||||
|
s.author = a.strip()
|
||||||
|
|
||||||
|
s.price = ''.join(doc.xpath('//span[@id="price_selling"]//text()')).strip()
|
||||||
|
s.detail_item = f.geturl().replace(self.url, '').strip()
|
||||||
|
s.formats = ', '.join(doc.xpath('//select[@id="N1_"]//option//text()'))
|
||||||
s.drm = SearchResult.DRM_UNLOCKED
|
s.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
|
||||||
yield s
|
yield s
|
||||||
|
|
||||||
def get_details(self, search_result, timeout):
|
def get_details(self, search_result, timeout):
|
||||||
|
if search_result.formats:
|
||||||
|
return False
|
||||||
|
|
||||||
br = browser()
|
br = browser()
|
||||||
with closing(br.open(url_slash_cleaner(self.url + search_result.detail_item), timeout=timeout)) as nf:
|
with closing(br.open(url_slash_cleaner(self.url + search_result.detail_item), timeout=timeout)) as nf:
|
||||||
idata = html.fromstring(nf.read())
|
idata = html.fromstring(nf.read())
|
||||||
|
@ -288,8 +288,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.db_images.reset()
|
self.db_images.reset()
|
||||||
|
|
||||||
self.library_view.model().count_changed()
|
self.library_view.model().count_changed()
|
||||||
self.tool_bar.database_changed(self.library_view.model().db)
|
self.bars_manager.database_changed(self.library_view.model().db)
|
||||||
self.library_view.model().database_changed.connect(self.tool_bar.database_changed,
|
self.library_view.model().database_changed.connect(self.bars_manager.database_changed,
|
||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
|
|
||||||
########################### Tags Browser ##############################
|
########################### Tags Browser ##############################
|
||||||
@ -324,7 +324,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
|
|
||||||
self.read_settings()
|
self.read_settings()
|
||||||
self.finalize_layout()
|
self.finalize_layout()
|
||||||
if self.tool_bar.showing_donate:
|
if self.bars_manager.showing_donate:
|
||||||
self.donate_button.start_animation()
|
self.donate_button.start_animation()
|
||||||
self.set_window_title()
|
self.set_window_title()
|
||||||
|
|
||||||
|
@ -1145,7 +1145,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def delete_book(self, id, notify=True, commit=True):
|
def delete_book(self, id, notify=True, commit=True, permanent=False):
|
||||||
'''
|
'''
|
||||||
Removes book from the result cache and the underlying database.
|
Removes book from the result cache and the underlying database.
|
||||||
If you set commit to False, you must call clean() manually afterwards
|
If you set commit to False, you must call clean() manually afterwards
|
||||||
@ -1155,10 +1155,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
except:
|
except:
|
||||||
path = None
|
path = None
|
||||||
if path and os.path.exists(path):
|
if path and os.path.exists(path):
|
||||||
self.rmtree(path)
|
self.rmtree(path, permanent=permanent)
|
||||||
parent = os.path.dirname(path)
|
parent = os.path.dirname(path)
|
||||||
if len(os.listdir(parent)) == 0:
|
if len(os.listdir(parent)) == 0:
|
||||||
self.rmtree(parent)
|
self.rmtree(parent, permanent=permanent)
|
||||||
self.conn.execute('DELETE FROM books WHERE id=?', (id,))
|
self.conn.execute('DELETE FROM books WHERE id=?', (id,))
|
||||||
if commit:
|
if commit:
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
@ -56,16 +56,17 @@ for x in FORMAT_ARG_DESCS:
|
|||||||
def find_plugboard(device_name, format, plugboards):
|
def find_plugboard(device_name, format, plugboards):
|
||||||
cpb = None
|
cpb = None
|
||||||
if format in plugboards:
|
if format in plugboards:
|
||||||
cpb = plugboards[format]
|
pb = plugboards[format]
|
||||||
elif plugboard_any_format_value in plugboards:
|
if device_name in pb:
|
||||||
cpb = plugboards[plugboard_any_format_value]
|
cpb = pb[device_name]
|
||||||
if cpb is not None:
|
elif plugboard_any_device_value in pb:
|
||||||
if device_name in cpb:
|
cpb = pb[plugboard_any_device_value]
|
||||||
cpb = cpb[device_name]
|
if not cpb and plugboard_any_format_value in plugboards:
|
||||||
elif plugboard_any_device_value in cpb:
|
pb = plugboards[plugboard_any_format_value]
|
||||||
cpb = cpb[plugboard_any_device_value]
|
if device_name in pb:
|
||||||
else:
|
cpb = pb[device_name]
|
||||||
cpb = None
|
elif plugboard_any_device_value in pb:
|
||||||
|
cpb = pb[plugboard_any_device_value]
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Device using plugboard', format, device_name, cpb)
|
prints('Device using plugboard', format, device_name, cpb)
|
||||||
return cpb
|
return cpb
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -50,6 +50,12 @@ PARALLEL_FUNCS = {
|
|||||||
|
|
||||||
'save_book' :
|
'save_book' :
|
||||||
('calibre.ebooks.metadata.worker', 'save_book', 'notification'),
|
('calibre.ebooks.metadata.worker', 'save_book', 'notification'),
|
||||||
|
|
||||||
|
'arbitrary' :
|
||||||
|
('calibre.utils.ipc.worker', 'arbitrary', None),
|
||||||
|
|
||||||
|
'arbitrary_n' :
|
||||||
|
('calibre.utils.ipc.worker', 'arbitrary', 'notification'),
|
||||||
}
|
}
|
||||||
|
|
||||||
class Progress(Thread):
|
class Progress(Thread):
|
||||||
@ -73,7 +79,55 @@ class Progress(Thread):
|
|||||||
except:
|
except:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def arbitrary(module_name, func_name, args, kwargs={}):
|
||||||
|
'''
|
||||||
|
An entry point that allows arbitrary functions to be run in a parallel
|
||||||
|
process. useful for plugin developers that want to run jobs in a parallel
|
||||||
|
process.
|
||||||
|
|
||||||
|
To use this entry point, simply create a ParallelJob with the module and
|
||||||
|
function names for the real entry point.
|
||||||
|
|
||||||
|
Remember that args and kwargs must be serialized so only use basic types
|
||||||
|
for them.
|
||||||
|
|
||||||
|
To use this, you will do something like
|
||||||
|
|
||||||
|
from calibre.gui2 import Dispatcher
|
||||||
|
gui.job_manager.run_job(Dispatcher(job_done), 'arbitrary',
|
||||||
|
args=('calibre_plugins.myplugin.worker', 'do_work',
|
||||||
|
('arg1' 'arg2', 'arg3')),
|
||||||
|
description='Change the world')
|
||||||
|
|
||||||
|
The function job_done will be called on completion, see the code in
|
||||||
|
gui2.actions.catalog for an example of using run_job and Dispatcher.
|
||||||
|
|
||||||
|
:param module_name: The fully qualified name of the module that contains
|
||||||
|
the actual function to be run. For example:
|
||||||
|
calibre_plugins.myplugin.worker
|
||||||
|
:param func_name: The name of the function to be run.
|
||||||
|
:param name: A list (or tuple) of arguments that will be passed to the
|
||||||
|
function ``func_name``
|
||||||
|
:param kwargs: A dictionary of keyword arguments to pass to func_name
|
||||||
|
'''
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
func = getattr(module, func_name)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
def arbitrary_n(module_name, func_name, args, kwargs={},
|
||||||
|
notification=lambda x, y: y):
|
||||||
|
'''
|
||||||
|
Same as :func:`arbitrary` above, except that func_name must support a
|
||||||
|
keyword argument "notification". This will be a function that accepts two
|
||||||
|
arguments. func_name should call it periodically with progress information.
|
||||||
|
The first argument is a float between 0 and 1 that represent percent
|
||||||
|
completed and the second is a string with a message (it can be an empty
|
||||||
|
string).
|
||||||
|
'''
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
func = getattr(module, func_name)
|
||||||
|
kwargs['notification'] = notification
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
def get_func(name):
|
def get_func(name):
|
||||||
module, func, notification = PARALLEL_FUNCS[name]
|
module, func, notification = PARALLEL_FUNCS[name]
|
||||||
|
@ -24,6 +24,7 @@ elif isosx:
|
|||||||
path = path.decode(filesystem_encoding)
|
path = path.decode(filesystem_encoding)
|
||||||
u.send2trash(path)
|
u.send2trash(path)
|
||||||
|
|
||||||
|
can_recycle = callable(recycle)
|
||||||
|
|
||||||
def delete_file(path):
|
def delete_file(path):
|
||||||
if callable(recycle):
|
if callable(recycle):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user