mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
GwR revisions
This commit is contained in:
commit
75f243a470
BIN
resources/images/news/sarajevo_x.png
Normal file
BIN
resources/images/news/sarajevo_x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 542 B |
@ -1,189 +1,76 @@
|
|||||||
__license__ = 'GPL v3'
|
import string
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
||||||
|
|
||||||
import re
|
|
||||||
from calibre import strftime
|
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class Newsweek(BasicNewsRecipe):
|
class Newsweek(BasicNewsRecipe):
|
||||||
|
|
||||||
|
|
||||||
title = 'Newsweek'
|
title = 'Newsweek'
|
||||||
__author__ = 'Kovid Goyal and Sujata Raman'
|
__author__ = 'Kovid Goyal'
|
||||||
description = 'Weekly news and current affairs in the US'
|
description = 'Weekly news and current affairs in the US'
|
||||||
|
language = 'en'
|
||||||
|
encoding = 'utf-8'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
extra_css = '''
|
BASE_URL = 'http://www.newsweek.com'
|
||||||
h1{font-family:Arial,Helvetica,sans-serif; font-size:large; color:#383733;}
|
INDEX = BASE_URL+'/topics.html'
|
||||||
.deck{font-family:Georgia,sans-serif; color:#383733;}
|
|
||||||
.bylineDate{font-family:georgia ; color:#58544A; font-size:x-small;}
|
|
||||||
.authorInfo{font-family:arial,helvetica,sans-serif; color:#0066CC; font-size:x-small;}
|
|
||||||
.articleUpdated{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
|
|
||||||
.issueDate{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small; font-style:italic;}
|
|
||||||
h5{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
|
|
||||||
h6{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
|
|
||||||
.story{font-family:georgia,sans-serif ;color:black;}
|
|
||||||
.photoCredit{color:#999999; font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
|
|
||||||
.photoCaption{color:#0A0A09;font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
|
|
||||||
.fwArticle{font-family:Arial,Helvetica,sans-serif;font-size:x-small;font-weight:bold;}
|
|
||||||
'''
|
|
||||||
|
|
||||||
encoding = 'utf-8'
|
keep_only_tags = dict(name='article', attrs={'class':'article-text'})
|
||||||
language = 'en'
|
remove_tags = [dict(attrs={'data-dartad':True})]
|
||||||
|
remove_attributes = ['property']
|
||||||
|
|
||||||
remove_tags = [
|
def postprocess_html(self, soup, first):
|
||||||
{'class':['fwArticle noHr','fwArticle','hdlBulletItem','head-content','navbar','link', 'ad', 'sponsorLinksArticle', 'mm-content',
|
for tag in soup.findAll(name=['article', 'header']):
|
||||||
'inline-social-links-wrapper', 'email-article','ToolBox',
|
tag.name = 'div'
|
||||||
'inline-promo-link', 'sponsorship',
|
return soup
|
||||||
'inlineComponentRight',
|
|
||||||
'comments-and-social-links-wrapper', 'EmailArticleBlock']},
|
def newsweek_sections(self):
|
||||||
{'id' : ['footer', 'ticker-data', 'topTenVertical',
|
soup = self.index_to_soup(self.INDEX)
|
||||||
'digg-top-five', 'mesothorax', 'nw-comments', 'my-take-landing',
|
for a in soup.findAll('a', title='Primary tag', href=True):
|
||||||
'ToolBox', 'EmailMain']},
|
yield (string.capitalize(self.tag_to_string(a)),
|
||||||
{'class': re.compile('related-cloud')},
|
self.BASE_URL+a['href'])
|
||||||
dict(name='li', attrs={'id':['slug_bigbox']})
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
keep_only_tags = [{'class':['article HorizontalHeader',
|
def newsweek_parse_section_page(self, soup):
|
||||||
'articlecontent','photoBox', 'article columnist first']}, ]
|
for article in soup.findAll('article', about=True,
|
||||||
recursions = 1
|
attrs={'class':'stream-item'}):
|
||||||
match_regexps = [r'http://www.newsweek.com/id/\S+/page/\d+']
|
title = article.find(attrs={'property': 'dc:title'})
|
||||||
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
|
if title is None: continue
|
||||||
|
title = self.tag_to_string(title)
|
||||||
def find_title(self, section):
|
url = self.BASE_URL + article['about']
|
||||||
d = {'scope':'Scope', 'thetake':'The Take', 'features':'Features',
|
desc = ''
|
||||||
None:'Departments', 'culture':'Culture'}
|
author = article.find({'property':'dc:creator'})
|
||||||
ans = None
|
if author:
|
||||||
a = section.find('a', attrs={'name':True})
|
desc = u'by %s. '%self.tag_to_string(author)
|
||||||
if a is not None:
|
p = article.find(attrs={'property':'dc:abstract'})
|
||||||
ans = a['name']
|
if p is not None:
|
||||||
return d.get(ans, ans)
|
for a in p.find('a'): a.extract()
|
||||||
|
desc += self.tag_to_string(p)
|
||||||
|
t = article.find('time', attrs={'property':'dc:created'})
|
||||||
def find_articles(self, section):
|
date = ''
|
||||||
ans = []
|
if t is not None:
|
||||||
for x in section.findAll('h5'):
|
date = u' [%s]'%self.tag_to_string(t)
|
||||||
title = ' '.join(x.findAll(text=True)).strip()
|
self.log('\tFound article:', title, 'at', url)
|
||||||
a = x.find('a')
|
self.log('\t\t', desc)
|
||||||
if not a: continue
|
yield {'title':title, 'url':url, 'description':desc, 'date':date}
|
||||||
href = a['href']
|
|
||||||
ans.append({'title':title, 'url':href, 'description':'', 'date': strftime('%a, %d %b')})
|
|
||||||
if not ans:
|
|
||||||
for x in section.findAll('div', attrs={'class':'hdlItem'}):
|
|
||||||
a = x.find('a', href=True)
|
|
||||||
if not a : continue
|
|
||||||
title = ' '.join(a.findAll(text=True)).strip()
|
|
||||||
href = a['href']
|
|
||||||
if 'http://xtra.newsweek.com' in href: continue
|
|
||||||
ans.append({'title':title, 'url':href, 'description':'', 'date': strftime('%a, %d %b')})
|
|
||||||
|
|
||||||
#for x in ans:
|
|
||||||
# x['url'] += '/output/print'
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def parse_index(self):
|
def parse_index(self):
|
||||||
soup = self.get_current_issue()
|
sections = []
|
||||||
if not soup:
|
for section, shref in self.newsweek_sections():
|
||||||
raise RuntimeError('Unable to connect to newsweek.com. Try again later.')
|
self.log('Processing section', section, shref)
|
||||||
sections = soup.findAll('div', attrs={'class':'featurewell'})
|
articles = []
|
||||||
titles = map(self.find_title, sections)
|
soups = [self.index_to_soup(shref)]
|
||||||
articles = map(self.find_articles, sections)
|
na = soups[0].find('a', rel='next')
|
||||||
ans = list(zip(titles, articles))
|
if na:
|
||||||
def fcmp(x, y):
|
soups.append(self.index_to_soup(self.BASE_URL+na['href']))
|
||||||
tx, ty = x[0], y[0]
|
for soup in soups:
|
||||||
if tx == "Features": return cmp(1, 2)
|
articles.extend(self.newsweek_parse_section_page(soup))
|
||||||
if ty == "Features": return cmp(2, 1)
|
if self.test and len(articles) > 1:
|
||||||
return cmp(tx, ty)
|
break
|
||||||
return sorted(ans, cmp=fcmp)
|
if articles:
|
||||||
|
sections.append((section, articles))
|
||||||
def ensure_html(self, soup):
|
if self.test and len(sections) > 1:
|
||||||
root = soup.find(name=True)
|
break
|
||||||
if root.name == 'html': return soup
|
return sections
|
||||||
nsoup = BeautifulSoup('<html><head></head><body/></html>')
|
|
||||||
nroot = nsoup.find(name='body')
|
|
||||||
for x in soup.contents:
|
|
||||||
if getattr(x, 'name', False):
|
|
||||||
x.extract()
|
|
||||||
nroot.insert(len(nroot), x)
|
|
||||||
return nsoup
|
|
||||||
|
|
||||||
def postprocess_html(self, soup, first_fetch):
|
|
||||||
if not first_fetch:
|
|
||||||
h1 = soup.find(id='headline')
|
|
||||||
if h1:
|
|
||||||
h1.extract()
|
|
||||||
div = soup.find(attrs={'class':'articleInfo'})
|
|
||||||
if div:
|
|
||||||
div.extract()
|
|
||||||
divs = list(soup.findAll('div', 'pagination'))
|
|
||||||
if not divs:
|
|
||||||
return self.ensure_html(soup)
|
|
||||||
for div in divs[1:]: div.extract()
|
|
||||||
all_a = divs[0].findAll('a', href=True)
|
|
||||||
divs[0]['style']="display:none"
|
|
||||||
if len(all_a) > 1:
|
|
||||||
all_a[-1].extract()
|
|
||||||
test = re.compile(self.match_regexps[0])
|
|
||||||
for a in soup.findAll('a', href=test):
|
|
||||||
if a not in all_a:
|
|
||||||
del a['href']
|
|
||||||
return self.ensure_html(soup)
|
|
||||||
|
|
||||||
def get_current_issue(self):
|
|
||||||
soup = self.index_to_soup('http://www.newsweek.com')
|
|
||||||
div = soup.find('div', attrs={'class':re.compile('more-from-mag')})
|
|
||||||
if div is None: return None
|
|
||||||
a = div.find('a')
|
|
||||||
if a is not None:
|
|
||||||
href = a['href'].split('#')[0]
|
|
||||||
return self.index_to_soup(href)
|
|
||||||
|
|
||||||
def get_cover_url(self):
|
|
||||||
cover_url = None
|
|
||||||
soup = self.index_to_soup('http://www.newsweek.com')
|
|
||||||
link_item = soup.find('div',attrs={'class':'cover-image'})
|
|
||||||
if link_item and link_item.a and link_item.a.img:
|
|
||||||
cover_url = link_item.a.img['src']
|
|
||||||
return cover_url
|
|
||||||
|
|
||||||
|
|
||||||
def postprocess_book(self, oeb, opts, log) :
|
|
||||||
|
|
||||||
def extractByline(href) :
|
|
||||||
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
|
|
||||||
byline = soup.find(True,attrs={'class':'authorInfo'})
|
|
||||||
byline = self.tag_to_string(byline) if byline is not None else ''
|
|
||||||
issueDate = soup.find(True,attrs={'class':'issueDate'})
|
|
||||||
issueDate = self.tag_to_string(issueDate) if issueDate is not None else ''
|
|
||||||
issueDate = re.sub(',','', issueDate)
|
|
||||||
if byline > '' and issueDate > '' :
|
|
||||||
return byline + ' | ' + issueDate
|
|
||||||
else :
|
|
||||||
return byline + issueDate
|
|
||||||
|
|
||||||
def extractDescription(href) :
|
|
||||||
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
|
|
||||||
description = soup.find(True,attrs={'name':'description'})
|
|
||||||
if description is not None and description.has_key('content'):
|
|
||||||
description = description['content']
|
|
||||||
if description.startswith('Newsweek magazine online plus') :
|
|
||||||
description = soup.find(True, attrs={'class':'story'})
|
|
||||||
firstPara = soup.find('p')
|
|
||||||
description = self.tag_to_string(firstPara)
|
|
||||||
else :
|
|
||||||
description = soup.find(True, attrs={'class':'story'})
|
|
||||||
firstPara = soup.find('p')
|
|
||||||
description = self.tag_to_string(firstPara)
|
|
||||||
return description
|
|
||||||
|
|
||||||
for section in oeb.toc :
|
|
||||||
for article in section :
|
|
||||||
if article.author is None :
|
|
||||||
article.author = extractByline(article.href)
|
|
||||||
if article.description is None :
|
|
||||||
article.description = extractDescription(article.href)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
@ -391,10 +391,14 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
# Skip ad pages before actual article
|
# Skip ad pages served before actual article
|
||||||
skip_tag = soup.find(True, {'name':'skip'})
|
skip_tag = soup.find(True, {'name':'skip'})
|
||||||
if skip_tag is not None:
|
if skip_tag is not None:
|
||||||
soup = self.index_to_soup(skip_tag.parent['href'])
|
self.log.error("Found forwarding link: %s" % skip_tag.parent['href'])
|
||||||
|
url = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
|
||||||
|
url += '?pagewanted=all'
|
||||||
|
self.log.error("Skipping ad to article at '%s'" % url)
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
return self.strip_anchors(soup)
|
return self.strip_anchors(soup)
|
||||||
|
|
||||||
def postprocess_html(self,soup, True):
|
def postprocess_html(self,soup, True):
|
||||||
|
@ -280,18 +280,14 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
'''
|
# Skip ad pages served before actual article
|
||||||
refresh = soup.find('meta', {'http-equiv':'refresh'})
|
|
||||||
if refresh is None:
|
|
||||||
return soup
|
|
||||||
content = refresh.get('content').partition('=')[2]
|
|
||||||
raw = self.browser.open('http://www.nytimes.com'+content).read()
|
|
||||||
return BeautifulSoup(raw.decode('cp1252', 'replace'))
|
|
||||||
'''
|
|
||||||
# Skip ad pages before actual article
|
|
||||||
skip_tag = soup.find(True, {'name':'skip'})
|
skip_tag = soup.find(True, {'name':'skip'})
|
||||||
if skip_tag is not None:
|
if skip_tag is not None:
|
||||||
soup = self.index_to_soup(skip_tag.parent['href'])
|
self.log.error("Found forwarding link: %s" % skip_tag.parent['href'])
|
||||||
|
url = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
|
||||||
|
url += '?pagewanted=all'
|
||||||
|
self.log.error("Skipping ad to article at '%s'" % url)
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
return self.strip_anchors(soup)
|
return self.strip_anchors(soup)
|
||||||
|
|
||||||
def postprocess_html(self,soup, True):
|
def postprocess_html(self,soup, True):
|
||||||
|
66
resources/recipes/sarajevo_x.recipe
Normal file
66
resources/recipes/sarajevo_x.recipe
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
|
||||||
|
'''
|
||||||
|
sarajevo-x.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
import re
|
||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
from calibre.ebooks.BeautifulSoup import Tag, NavigableString
|
||||||
|
|
||||||
|
class SarajevoX(BasicNewsRecipe):
|
||||||
|
title = 'Sarajevo-x.com'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'Sarajevo-x.com - najposjeceniji bosanskohercegovacki internet portal'
|
||||||
|
publisher = 'InterSoft d.o.o.'
|
||||||
|
category = 'news, politics, Bosnia and Herzegovina,Sarajevo-x.com, internet, portal, vijesti, bosna i hercegovina, sarajevo'
|
||||||
|
oldest_article = 2
|
||||||
|
delay = 1
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'cp1250'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'bs'
|
||||||
|
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif} .article_description{font-family: sans1, sans-serif} div#fotka{display: block} img{margin-bottom: 0.5em} '
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'class':'content-bg'})]
|
||||||
|
remove_tags_after = dict(name='div',attrs={'class':'izvor'})
|
||||||
|
remove_tags = [dict(name=['object','link','base','table'])]
|
||||||
|
remove_attributes = ['height','width','alt','border']
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'BIH' , u'http://www.sarajevo-x.com/rss/bih' )
|
||||||
|
,(u'Svijet' , u'http://www.sarajevo-x.com/rss/svijet' )
|
||||||
|
,(u'Biznis' , u'http://www.sarajevo-x.com/rss/biznis' )
|
||||||
|
,(u'Sport' , u'http://www.sarajevo-x.com/rss/sport' )
|
||||||
|
,(u'Showtime' , u'http://www.sarajevo-x.com/rss/showtime' )
|
||||||
|
,(u'Scitech' , u'http://www.sarajevo-x.com/rss/scitech' )
|
||||||
|
,(u'Lifestyle' , u'http://www.sarajevo-x.com/rss/lifestyle' )
|
||||||
|
,(u'Kultura' , u'http://www.sarajevo-x.com/rss/kultura' )
|
||||||
|
,(u'Zanimljivosti', u'http://www.sarajevo-x.com/rss/zanimljivosti')
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
dtag = soup.find('div',attrs={'id':'fotka'})
|
||||||
|
if dtag:
|
||||||
|
sp = soup.find('div',attrs={'id':'opisslike'})
|
||||||
|
img = soup.find('img')
|
||||||
|
if sp:
|
||||||
|
sp
|
||||||
|
else:
|
||||||
|
mtag = Tag(soup,'div',[("id","opisslike"),("class","opscitech")])
|
||||||
|
mopis = NavigableString("Opis")
|
||||||
|
mtag.insert(0,mopis)
|
||||||
|
img.append(mtag)
|
||||||
|
return soup
|
||||||
|
|
@ -29,7 +29,7 @@ class Plugin(object):
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
#: List of platforms this plugin works on
|
#: List of platforms this plugin works on
|
||||||
#: For example: ``['windows', 'osx', 'linux']
|
#: For example: ``['windows', 'osx', 'linux']``
|
||||||
supported_platforms = []
|
supported_platforms = []
|
||||||
|
|
||||||
#: The name of this plugin. You must set it something other
|
#: The name of this plugin. You must set it something other
|
||||||
@ -214,10 +214,8 @@ class MetadataReaderPlugin(Plugin):
|
|||||||
Return metadata for the file represented by stream (a file like object
|
Return metadata for the file represented by stream (a file like object
|
||||||
that supports reading). Raise an exception when there is an error
|
that supports reading). Raise an exception when there is an error
|
||||||
with the input data.
|
with the input data.
|
||||||
|
|
||||||
:param type: The type of file. Guaranteed to be one of the entries
|
:param type: The type of file. Guaranteed to be one of the entries
|
||||||
in :attr:`file_types`.
|
in :attr:`file_types`.
|
||||||
|
|
||||||
:return: A :class:`calibre.ebooks.metadata.MetaInformation` object
|
:return: A :class:`calibre.ebooks.metadata.MetaInformation` object
|
||||||
'''
|
'''
|
||||||
return None
|
return None
|
||||||
@ -245,11 +243,9 @@ class MetadataWriterPlugin(Plugin):
|
|||||||
Set metadata for the file represented by stream (a file like object
|
Set metadata for the file represented by stream (a file like object
|
||||||
that supports reading). Raise an exception when there is an error
|
that supports reading). Raise an exception when there is an error
|
||||||
with the input data.
|
with the input data.
|
||||||
|
|
||||||
:param type: The type of file. Guaranteed to be one of the entries
|
:param type: The type of file. Guaranteed to be one of the entries
|
||||||
in :attr:`file_types`.
|
in :attr:`file_types`.
|
||||||
:param mi: A :class:`calibre.ebooks.metadata.MetaInformation` object
|
:param mi: A :class:`calibre.ebooks.metadata.MetaInformation` object
|
||||||
|
|
||||||
'''
|
'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -240,6 +240,9 @@ class OutputProfile(Plugin):
|
|||||||
# Device supports displaying a nested TOC
|
# Device supports displaying a nested TOC
|
||||||
supports_nested_toc = True
|
supports_nested_toc = True
|
||||||
|
|
||||||
|
# If True output should be optimized for a touchscreen interface
|
||||||
|
touchscreen = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tags_to_string(cls, tags):
|
def tags_to_string(cls, tags):
|
||||||
return escape(', '.join(tags))
|
return escape(', '.join(tags))
|
||||||
|
@ -14,8 +14,14 @@ XMLDECL_RE = re.compile(r'^\s*<[?]xml.*?[?]>')
|
|||||||
SVG_NS = 'http://www.w3.org/2000/svg'
|
SVG_NS = 'http://www.w3.org/2000/svg'
|
||||||
XLINK_NS = 'http://www.w3.org/1999/xlink'
|
XLINK_NS = 'http://www.w3.org/1999/xlink'
|
||||||
|
|
||||||
convert_entities = functools.partial(entity_to_unicode, exceptions=['quot',
|
convert_entities = functools.partial(entity_to_unicode,
|
||||||
'apos', 'lt', 'gt', 'amp', '#60', '#62'])
|
result_exceptions = {
|
||||||
|
u'<' : '<',
|
||||||
|
u'>' : '>',
|
||||||
|
u"'" : ''',
|
||||||
|
u'"' : '"',
|
||||||
|
u'&' : '&',
|
||||||
|
})
|
||||||
_span_pat = re.compile('<span.*?</span>', re.DOTALL|re.IGNORECASE)
|
_span_pat = re.compile('<span.*?</span>', re.DOTALL|re.IGNORECASE)
|
||||||
|
|
||||||
LIGATURES = {
|
LIGATURES = {
|
||||||
|
@ -416,9 +416,9 @@ class HTMLInput(InputFormatPlugin):
|
|||||||
link = unquote(link).replace('/', os.sep)
|
link = unquote(link).replace('/', os.sep)
|
||||||
if not link.strip():
|
if not link.strip():
|
||||||
return link_
|
return link_
|
||||||
if base and not os.path.isabs(link):
|
|
||||||
link = os.path.join(base, link)
|
|
||||||
try:
|
try:
|
||||||
|
if base and not os.path.isabs(link):
|
||||||
|
link = os.path.join(base, link)
|
||||||
link = os.path.abspath(link)
|
link = os.path.abspath(link)
|
||||||
except:
|
except:
|
||||||
return link_
|
return link_
|
||||||
|
@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import struct, array, zlib, cStringIO, collections, re
|
import struct, array, zlib, cStringIO, collections, re
|
||||||
|
|
||||||
from calibre.ebooks.lrf import LRFParseError, PRS500_PROFILE
|
from calibre.ebooks.lrf import LRFParseError, PRS500_PROFILE
|
||||||
from calibre import entity_to_unicode
|
from calibre import entity_to_unicode, prepare_string_for_xml
|
||||||
from calibre.ebooks.lrf.tags import Tag
|
from calibre.ebooks.lrf.tags import Tag
|
||||||
|
|
||||||
ruby_tags = {
|
ruby_tags = {
|
||||||
@ -870,7 +870,7 @@ class Text(LRFStream):
|
|||||||
open_containers = collections.deque()
|
open_containers = collections.deque()
|
||||||
for c in self.content:
|
for c in self.content:
|
||||||
if isinstance(c, basestring):
|
if isinstance(c, basestring):
|
||||||
s += c
|
s += prepare_string_for_xml(c)
|
||||||
elif c is None:
|
elif c is None:
|
||||||
if open_containers:
|
if open_containers:
|
||||||
p = open_containers.pop()
|
p = open_containers.pop()
|
||||||
|
@ -11,7 +11,7 @@ import re
|
|||||||
|
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
|
from calibre import entity_to_unicode
|
||||||
|
|
||||||
def get_metadata(stream):
|
def get_metadata(stream):
|
||||||
src = stream.read()
|
src = stream.read()
|
||||||
@ -43,6 +43,10 @@ def get_metadata_(src, encoding=None):
|
|||||||
if match:
|
if match:
|
||||||
author = match.group(2).replace(',', ';')
|
author = match.group(2).replace(',', ';')
|
||||||
|
|
||||||
|
ent_pat = re.compile(r'&(\S+)?;')
|
||||||
|
title = ent_pat.sub(entity_to_unicode, title)
|
||||||
|
if author:
|
||||||
|
author = ent_pat.sub(entity_to_unicode, author)
|
||||||
mi = MetaInformation(title, [author] if author else None)
|
mi = MetaInformation(title, [author] if author else None)
|
||||||
|
|
||||||
# Publisher
|
# Publisher
|
||||||
|
@ -787,7 +787,6 @@ class Manifest(object):
|
|||||||
data = self.oeb.decode(data)
|
data = self.oeb.decode(data)
|
||||||
data = self.oeb.html_preprocessor(data)
|
data = self.oeb.html_preprocessor(data)
|
||||||
|
|
||||||
|
|
||||||
# Remove DOCTYPE declaration as it messes up parsing
|
# Remove DOCTYPE declaration as it messes up parsing
|
||||||
# In particular, it causes tostring to insert xmlns
|
# In particular, it causes tostring to insert xmlns
|
||||||
# declarations, which messes up the coercing logic
|
# declarations, which messes up the coercing logic
|
||||||
|
@ -136,6 +136,8 @@ class CoverManager(object):
|
|||||||
href = g['cover'].href
|
href = g['cover'].href
|
||||||
else:
|
else:
|
||||||
href = self.default_cover()
|
href = self.default_cover()
|
||||||
|
if href is None:
|
||||||
|
return
|
||||||
width, height = self.inspect_cover(href)
|
width, height = self.inspect_cover(href)
|
||||||
if width is None or height is None:
|
if width is None or height is None:
|
||||||
self.log.warning('Failed to read cover dimensions')
|
self.log.warning('Failed to read cover dimensions')
|
||||||
|
@ -97,7 +97,8 @@ def _config():
|
|||||||
help=_('Overwrite author and title with new metadata'))
|
help=_('Overwrite author and title with new metadata'))
|
||||||
c.add_opt('enforce_cpu_limit', default=True,
|
c.add_opt('enforce_cpu_limit', default=True,
|
||||||
help=_('Limit max simultaneous jobs to number of CPUs'))
|
help=_('Limit max simultaneous jobs to number of CPUs'))
|
||||||
|
c.add_opt('tag_browser_hidden_categories', default=set(),
|
||||||
|
help=_('tag browser categories not to display'))
|
||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
config = _config()
|
config = _config()
|
||||||
|
@ -14,6 +14,7 @@ from calibre.gui2.convert.regex_builder_ui import Ui_RegexBuilder
|
|||||||
from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit
|
from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit
|
||||||
from calibre.gui2 import error_dialog, choose_files
|
from calibre.gui2 import error_dialog, choose_files
|
||||||
from calibre.ebooks.oeb.iterator import EbookIterator
|
from calibre.ebooks.oeb.iterator import EbookIterator
|
||||||
|
from calibre.ebooks.conversion.preprocess import convert_entities
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
|
|
||||||
class RegexBuilder(QDialog, Ui_RegexBuilder):
|
class RegexBuilder(QDialog, Ui_RegexBuilder):
|
||||||
@ -87,8 +88,10 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
self.iterator = EbookIterator(pathtoebook)
|
self.iterator = EbookIterator(pathtoebook)
|
||||||
self.iterator.__enter__(only_input_plugin=True)
|
self.iterator.__enter__(only_input_plugin=True)
|
||||||
text = [u'']
|
text = [u'']
|
||||||
|
ent_pat = re.compile(r'&(\S+?);')
|
||||||
for path in self.iterator.spine:
|
for path in self.iterator.spine:
|
||||||
html = open(path, 'rb').read().decode('utf-8', 'replace')
|
html = open(path, 'rb').read().decode('utf-8', 'replace')
|
||||||
|
html = ent_pat.sub(convert_entities, html)
|
||||||
text.append(html)
|
text.append(html)
|
||||||
self.preview.setPlainText('\n---\n'.join(text))
|
self.preview.setPlainText('\n---\n'.join(text))
|
||||||
|
|
||||||
|
@ -1123,12 +1123,12 @@ class DeviceGUI(object):
|
|||||||
if cache:
|
if cache:
|
||||||
if id in cache['db_ids']:
|
if id in cache['db_ids']:
|
||||||
loc[i] = True
|
loc[i] = True
|
||||||
break
|
continue
|
||||||
if mi.authors and \
|
if mi.authors and \
|
||||||
re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \
|
re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \
|
||||||
in cache['authors']:
|
in cache['authors']:
|
||||||
loc[i] = True
|
loc[i] = True
|
||||||
break
|
continue
|
||||||
return loc
|
return loc
|
||||||
|
|
||||||
def set_books_in_library(self, booklists, reset=False):
|
def set_books_in_library(self, booklists, reset=False):
|
||||||
|
@ -14,7 +14,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
|||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
||||||
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||||
from calibre.gui2 import choose_dir, error_dialog, config, \
|
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
|
||||||
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
||||||
warning_dialog, ResizableDialog, question_dialog
|
warning_dialog, ResizableDialog, question_dialog
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
@ -480,6 +480,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
|
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
|
||||||
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
||||||
self.port.editingFinished.connect(self.check_port_value)
|
self.port.editingFinished.connect(self.check_port_value)
|
||||||
|
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
|
||||||
|
True))
|
||||||
|
|
||||||
def check_port_value(self, *args):
|
def check_port_value(self, *args):
|
||||||
port = self.port.value()
|
port = self.port.value()
|
||||||
@ -852,6 +854,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
|
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
|
||||||
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
|
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
|
||||||
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
|
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
|
||||||
|
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
|
||||||
fmts = []
|
fmts = []
|
||||||
for i in range(self.viewer.count()):
|
for i in range(self.viewer.count()):
|
||||||
if self.viewer.item(i).checkState() == Qt.Checked:
|
if self.viewer.item(i).checkState() == Qt.Checked:
|
||||||
|
@ -331,8 +331,8 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page">
|
<widget class="QWidget" name="page">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
<layout class="QGridLayout" name="gridLayout_8">
|
||||||
<item>
|
<item row="0" column="0">
|
||||||
<widget class="QCheckBox" name="roman_numerals">
|
<widget class="QCheckBox" name="roman_numerals">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Use &Roman numerals for series number</string>
|
<string>Use &Roman numerals for series number</string>
|
||||||
@ -342,28 +342,35 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="0">
|
||||||
<widget class="QCheckBox" name="systray_icon">
|
<widget class="QCheckBox" name="systray_icon">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable system &tray icon (needs restart)</string>
|
<string>Enable system &tray icon (needs restart)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="1">
|
||||||
<widget class="QCheckBox" name="systray_notifications">
|
<widget class="QCheckBox" name="systray_notifications">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &notifications in system tray</string>
|
<string>Show &notifications in system tray</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QCheckBox" name="show_splash_screen">
|
||||||
|
<property name="text">
|
||||||
|
<string>Show &splash screen at startup</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="separate_cover_flow">
|
<widget class="QCheckBox" name="separate_cover_flow">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show cover &browser in a separate window (needs restart)</string>
|
<string>Show cover &browser in a separate window (needs restart)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="4" column="0">
|
||||||
<widget class="QCheckBox" name="search_as_you_type">
|
<widget class="QCheckBox" name="search_as_you_type">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Search as you type</string>
|
<string>Search as you type</string>
|
||||||
@ -373,21 +380,21 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="5" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="sync_news">
|
<widget class="QCheckBox" name="sync_news">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Automatically send downloaded &news to ebook reader</string>
|
<string>Automatically send downloaded &news to ebook reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="6" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="delete_news">
|
<widget class="QCheckBox" name="delete_news">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="7" column="0" colspan="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_6">
|
<widget class="QLabel" name="label_6">
|
||||||
@ -404,7 +411,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="8" column="0" colspan="2">
|
||||||
<widget class="QGroupBox" name="groupBox_2">
|
<widget class="QGroupBox" name="groupBox_2">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Toolbar</string>
|
<string>Toolbar</string>
|
||||||
@ -452,7 +459,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="9" column="0" colspan="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
@ -527,12 +534,12 @@
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="add_custcol_button">
|
<widget class="QToolButton" name="add_custcol_button">
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Add a user-defined column</string>
|
<string>Add a user-defined column</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="../../../../../resources/images.qrc">
|
<iconset resource="../../../../../resources/images.qrc">
|
||||||
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
|
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
|
||||||
|
86
src/calibre/gui2/dialogs/saved_search_editor.py
Normal file
86
src/calibre/gui2/dialogs/saved_search_editor.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt4.QtCore import SIGNAL, Qt
|
||||||
|
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
||||||
|
|
||||||
|
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
|
||||||
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
from calibre.constants import islinux
|
||||||
|
|
||||||
|
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
|
||||||
|
|
||||||
|
def __init__(self, window, initial_search=None):
|
||||||
|
QDialog.__init__(self, window)
|
||||||
|
Ui_SavedSearchEditor.__init__(self)
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.connect(self.add_search_button, SIGNAL('clicked()'), self.add_search)
|
||||||
|
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
|
||||||
|
self.current_index_changed)
|
||||||
|
self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search)
|
||||||
|
|
||||||
|
self.current_search_name = None
|
||||||
|
self.searches = {}
|
||||||
|
self.searches_to_delete = []
|
||||||
|
for name in saved_searches.names():
|
||||||
|
self.searches[name] = saved_searches.lookup(name)
|
||||||
|
|
||||||
|
self.populate_search_list()
|
||||||
|
if initial_search is not None and initial_search in self.searches:
|
||||||
|
self.select_search(initial_search)
|
||||||
|
|
||||||
|
def populate_search_list(self):
|
||||||
|
self.search_name_box.clear()
|
||||||
|
for name in sorted(self.searches.keys()):
|
||||||
|
self.search_name_box.addItem(name)
|
||||||
|
|
||||||
|
def add_search(self):
|
||||||
|
search_name = unicode(self.input_box.text()).strip()
|
||||||
|
if search_name == '':
|
||||||
|
return False
|
||||||
|
if search_name not in self.searches:
|
||||||
|
self.searches[search_name] = ''
|
||||||
|
self.populate_search_list()
|
||||||
|
self.select_search(search_name)
|
||||||
|
else:
|
||||||
|
self.select_search(search_name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def del_search(self):
|
||||||
|
if self.current_search_name is not None:
|
||||||
|
if not confirm('<p>'+_('The current saved search will be '
|
||||||
|
'<b>permanently deleted</b>. Are you sure?')
|
||||||
|
+'</p>', 'saved_search_editor_delete', self):
|
||||||
|
return
|
||||||
|
del self.searches[self.current_search_name]
|
||||||
|
self.searches_to_delete.append(self.current_search_name)
|
||||||
|
self.current_search_name = None
|
||||||
|
self.search_name_box.removeItem(self.search_name_box.currentIndex())
|
||||||
|
|
||||||
|
def select_search(self, name):
|
||||||
|
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
|
||||||
|
|
||||||
|
def current_index_changed(self, idx):
|
||||||
|
if self.current_search_name:
|
||||||
|
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
|
||||||
|
name = unicode(self.search_name_box.itemText(idx))
|
||||||
|
if name:
|
||||||
|
self.current_search_name = name
|
||||||
|
self.search_text.setPlainText(self.searches[name])
|
||||||
|
else:
|
||||||
|
self.current_search_name = None
|
||||||
|
self.search_text.setPlainText('')
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
if self.current_search_name:
|
||||||
|
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
|
||||||
|
for name in self.searches_to_delete:
|
||||||
|
saved_searches.delete(name)
|
||||||
|
for name in self.searches:
|
||||||
|
saved_searches.add(name, self.searches[name])
|
||||||
|
QDialog.accept(self)
|
185
src/calibre/gui2/dialogs/saved_search_editor.ui
Normal file
185
src/calibre/gui2/dialogs/saved_search_editor.ui
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>SavedSearchEditor</class>
|
||||||
|
<widget class="QDialog" name="SavedSearchEditor">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>548</width>
|
||||||
|
<height>148</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Saved Search Editor</string>
|
||||||
|
</property>
|
||||||
|
<property name="windowIcon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
<property name="centerButtons">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Saved Search: </string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>search_name_box</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="search_name_box">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>160</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>145</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Select a saved search to edit</string>
|
||||||
|
</property>
|
||||||
|
<property name="editable">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QToolButton" name="delete_search_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Delete this selected saved search</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="3">
|
||||||
|
<spacer>
|
||||||
|
<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 row="0" column="4">
|
||||||
|
<widget class="QLineEdit" name="input_box">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>60</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Enter a new saved search name.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="5">
|
||||||
|
<widget class="QToolButton" name="add_search_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Add the new saved search</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QPlainTextEdit" name="search_text">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Change the contents of the saved search</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources>
|
||||||
|
<include location="../../../../../calibre_datesearch/resources/images"/>
|
||||||
|
</resources>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>SavedSearchEditor</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>SavedSearchEditor</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -24,13 +24,12 @@ class Item:
|
|||||||
class TagCategories(QDialog, Ui_TagCategories):
|
class TagCategories(QDialog, Ui_TagCategories):
|
||||||
category_labels_orig = ['', 'authors', 'series', 'publishers', 'tags']
|
category_labels_orig = ['', 'authors', 'series', 'publishers', 'tags']
|
||||||
|
|
||||||
def __init__(self, window, db, index=None):
|
def __init__(self, window, db, on_category=None):
|
||||||
QDialog.__init__(self, window)
|
QDialog.__init__(self, window)
|
||||||
Ui_TagCategories.__init__(self)
|
Ui_TagCategories.__init__(self)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.db = db
|
self.db = db
|
||||||
self.index = index
|
|
||||||
self.applied_items = []
|
self.applied_items = []
|
||||||
|
|
||||||
cc_icon = QIcon(I('column.svg'))
|
cc_icon = QIcon(I('column.svg'))
|
||||||
@ -102,8 +101,10 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
self.connect(self.applied_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags)
|
self.connect(self.applied_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags)
|
||||||
|
|
||||||
self.populate_category_list()
|
self.populate_category_list()
|
||||||
return
|
if on_category is not None:
|
||||||
self.select_category(0)
|
l = self.category_box.findText(on_category)
|
||||||
|
if l >= 0:
|
||||||
|
self.category_box.setCurrentIndex(l)
|
||||||
|
|
||||||
def make_list_widget(self, item):
|
def make_list_widget(self, item):
|
||||||
n = item.name if item.exists else item.name + _(' (not on any book)')
|
n = item.name if item.exists else item.name + _(' (not on any book)')
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Tag Editor</string>
|
<string>User Categories Editor</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset>
|
<iconset>
|
||||||
|
89
src/calibre/gui2/dialogs/tag_list_editor.py
Normal file
89
src/calibre/gui2/dialogs/tag_list_editor.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
from PyQt4.QtCore import SIGNAL, Qt
|
||||||
|
from PyQt4.QtGui import QDialog, QListWidgetItem
|
||||||
|
|
||||||
|
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
|
||||||
|
from calibre.gui2 import question_dialog, error_dialog
|
||||||
|
|
||||||
|
class TagListEditor(QDialog, Ui_TagListEditor):
|
||||||
|
|
||||||
|
def tag_cmp(self, x, y):
|
||||||
|
return cmp(x.lower(), y.lower())
|
||||||
|
|
||||||
|
def __init__(self, window, db, tag_to_match):
|
||||||
|
QDialog.__init__(self, window)
|
||||||
|
Ui_TagListEditor.__init__(self)
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.to_rename = {}
|
||||||
|
self.to_delete = []
|
||||||
|
self.db = db
|
||||||
|
self.all_tags = {}
|
||||||
|
for k,v in db.get_tags_with_ids():
|
||||||
|
self.all_tags[v] = k
|
||||||
|
for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp):
|
||||||
|
item = QListWidgetItem(tag)
|
||||||
|
item.setData(Qt.UserRole, self.all_tags[tag])
|
||||||
|
self.available_tags.addItem(item)
|
||||||
|
|
||||||
|
items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly)
|
||||||
|
if len(items) == 1:
|
||||||
|
self.available_tags.setCurrentItem(items[0])
|
||||||
|
|
||||||
|
self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
|
||||||
|
self.connect(self.rename_button, SIGNAL('clicked()'), self.rename_tag)
|
||||||
|
self.connect(self.available_tags, SIGNAL('itemDoubleClicked(QListWidgetItem *)'), self._rename_tag)
|
||||||
|
self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
|
||||||
|
|
||||||
|
def finish_editing(self, item):
|
||||||
|
if item.text() != self.item_before_editing.text():
|
||||||
|
if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
|
||||||
|
error_dialog(self, 'Tag already used',
|
||||||
|
'The tag %s is already used.'%(item.text())).exec_()
|
||||||
|
item.setText(self.item_before_editing.text())
|
||||||
|
return
|
||||||
|
id,ign = self.item_before_editing.data(Qt.UserRole).toInt()
|
||||||
|
self.to_rename[item.text()] = id
|
||||||
|
|
||||||
|
def rename_tag(self):
|
||||||
|
item = self.available_tags.currentItem()
|
||||||
|
self._rename_tag(item)
|
||||||
|
|
||||||
|
def _rename_tag(self, item):
|
||||||
|
if item is None:
|
||||||
|
error_dialog(self, 'No tag selected', 'You must select one tag from the list of Available tags.').exec_()
|
||||||
|
return
|
||||||
|
self.item_before_editing = item.clone()
|
||||||
|
item.setFlags (item.flags() | Qt.ItemIsEditable);
|
||||||
|
self.available_tags.editItem(item)
|
||||||
|
|
||||||
|
def delete_tags(self, item=None):
|
||||||
|
confirms, deletes = [], []
|
||||||
|
items = self.available_tags.selectedItems() if item is None else [item]
|
||||||
|
if not items:
|
||||||
|
error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_()
|
||||||
|
return
|
||||||
|
for item in items:
|
||||||
|
if self.db.is_tag_used(unicode(item.text())):
|
||||||
|
confirms.append(item)
|
||||||
|
else:
|
||||||
|
deletes.append(item)
|
||||||
|
if confirms:
|
||||||
|
ct = ', '.join([unicode(item.text()) for item in confirms])
|
||||||
|
if question_dialog(self, _('Are your sure?'),
|
||||||
|
'<p>'+_('The following tags are used by one or more books. '
|
||||||
|
'Are you certain you want to delete them?')+'<br>'+ct):
|
||||||
|
deletes += confirms
|
||||||
|
|
||||||
|
for item in deletes:
|
||||||
|
self.to_delete.append(item)
|
||||||
|
self.available_tags.takeItem(self.available_tags.row(item))
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
for text in self.to_rename:
|
||||||
|
self.db.rename_tag(self.to_rename[text], unicode(text))
|
||||||
|
for item in self.to_delete:
|
||||||
|
self.db.delete_tag(unicode(item.text()))
|
||||||
|
QDialog.accept(self)
|
||||||
|
|
163
src/calibre/gui2/dialogs/tag_list_editor.ui
Normal file
163
src/calibre/gui2/dialogs/tag_list_editor.ui
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>TagListEditor</class>
|
||||||
|
<widget class="QDialog" name="TagListEditor">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>397</width>
|
||||||
|
<height>335</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Tag Editor</string>
|
||||||
|
</property>
|
||||||
|
<property name="windowIcon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Tags in use</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>available_tags</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="delete_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Delete tag from database. This will unapply the tag from all books and then remove it from the database.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>32</width>
|
||||||
|
<height>32</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="rename_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Rename the tag everywhere it is used.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>32</width>
|
||||||
|
<height>32</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+S</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="available_tags">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>TagListEditor</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>TagListEditor</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -5,13 +5,15 @@ import sys, os, time, socket, traceback
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \
|
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \
|
||||||
QThread, pyqtSignal, Qt, QProgressDialog, QString
|
QThread, pyqtSignal, Qt, QProgressDialog, QString, QPixmap, \
|
||||||
|
QSplashScreen, QApplication
|
||||||
|
|
||||||
from calibre import prints, plugins
|
from calibre import prints, plugins
|
||||||
from calibre.constants import iswindows, __appname__, isosx, filesystem_encoding
|
from calibre.constants import iswindows, __appname__, isosx, DEBUG, \
|
||||||
|
filesystem_encoding
|
||||||
from calibre.utils.ipc import ADDRESS, RC
|
from calibre.utils.ipc import ADDRESS, RC
|
||||||
from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \
|
from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \
|
||||||
Application, choose_dir, error_dialog, question_dialog
|
Application, choose_dir, error_dialog, question_dialog, gprefs
|
||||||
from calibre.gui2.main_window import option_parser as _option_parser
|
from calibre.gui2.main_window import option_parser as _option_parser
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
@ -113,15 +115,25 @@ class GuiRunner(QObject):
|
|||||||
initialization'''
|
initialization'''
|
||||||
|
|
||||||
def __init__(self, opts, args, actions, listener, app):
|
def __init__(self, opts, args, actions, listener, app):
|
||||||
|
self.startup_time = time.time()
|
||||||
self.opts, self.args, self.listener, self.app = opts, args, listener, app
|
self.opts, self.args, self.listener, self.app = opts, args, listener, app
|
||||||
self.actions = actions
|
self.actions = actions
|
||||||
self.main = None
|
self.main = None
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
|
self.splash_screen = None
|
||||||
self.timer = QTimer.singleShot(1, self.initialize)
|
self.timer = QTimer.singleShot(1, self.initialize)
|
||||||
|
if DEBUG:
|
||||||
|
prints('Starting up...')
|
||||||
|
|
||||||
def start_gui(self):
|
def start_gui(self):
|
||||||
from calibre.gui2.ui import Main
|
from calibre.gui2.ui import Main
|
||||||
main = Main(self.library_path, self.db, self.listener, self.opts, self.actions)
|
main = Main(self.opts)
|
||||||
|
if self.splash_screen is not None:
|
||||||
|
self.splash_screen.showMessage(_('Initializing user interface...'))
|
||||||
|
self.splash_screen.finish(main)
|
||||||
|
main.initialize(self.library_path, self.db, self.listener, self.actions)
|
||||||
|
if DEBUG:
|
||||||
|
prints('Started up in', time.time() - self.startup_time)
|
||||||
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
|
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
|
||||||
sys.excepthook = main.unhandled_exception
|
sys.excepthook = main.unhandled_exception
|
||||||
if len(self.args) > 1:
|
if len(self.args) > 1:
|
||||||
@ -142,7 +154,7 @@ class GuiRunner(QObject):
|
|||||||
|
|
||||||
if db is None and tb is not None:
|
if db is None and tb is not None:
|
||||||
# DB Repair failed
|
# DB Repair failed
|
||||||
error_dialog(None, _('Repairing failed'),
|
error_dialog(self.splash_screen, _('Repairing failed'),
|
||||||
_('The database repair failed. Starting with '
|
_('The database repair failed. Starting with '
|
||||||
'a new empty library.'),
|
'a new empty library.'),
|
||||||
det_msg=tb, show=True)
|
det_msg=tb, show=True)
|
||||||
@ -159,7 +171,7 @@ class GuiRunner(QObject):
|
|||||||
os.makedirs(x)
|
os.makedirs(x)
|
||||||
except:
|
except:
|
||||||
x = os.path.expanduser('~')
|
x = os.path.expanduser('~')
|
||||||
candidate = choose_dir(None, 'choose calibre library',
|
candidate = choose_dir(self.splash_screen, 'choose calibre library',
|
||||||
_('Choose a location for your new calibre e-book library'),
|
_('Choose a location for your new calibre e-book library'),
|
||||||
default_dir=x)
|
default_dir=x)
|
||||||
|
|
||||||
@ -170,7 +182,7 @@ class GuiRunner(QObject):
|
|||||||
self.library_path = candidate
|
self.library_path = candidate
|
||||||
db = LibraryDatabase2(candidate)
|
db = LibraryDatabase2(candidate)
|
||||||
except:
|
except:
|
||||||
error_dialog(None, _('Bad database location'),
|
error_dialog(self.splash_screen, _('Bad database location'),
|
||||||
_('Bad database location %r. calibre will now quit.'
|
_('Bad database location %r. calibre will now quit.'
|
||||||
)%self.library_path,
|
)%self.library_path,
|
||||||
det_msg=traceback.format_exc(), show=True)
|
det_msg=traceback.format_exc(), show=True)
|
||||||
@ -184,7 +196,7 @@ class GuiRunner(QObject):
|
|||||||
try:
|
try:
|
||||||
db = LibraryDatabase2(self.library_path)
|
db = LibraryDatabase2(self.library_path)
|
||||||
except (sqlite.Error, DatabaseException):
|
except (sqlite.Error, DatabaseException):
|
||||||
repair = question_dialog(None, _('Corrupted database'),
|
repair = question_dialog(self.splash_screen, _('Corrupted database'),
|
||||||
_('Your calibre database appears to be corrupted. Do '
|
_('Your calibre database appears to be corrupted. Do '
|
||||||
'you want calibre to try and repair it automatically? '
|
'you want calibre to try and repair it automatically? '
|
||||||
'If you say No, a new empty calibre library will be created.'),
|
'If you say No, a new empty calibre library will be created.'),
|
||||||
@ -203,14 +215,27 @@ class GuiRunner(QObject):
|
|||||||
self.repair.start()
|
self.repair.start()
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
error_dialog(None, _('Bad database location'),
|
error_dialog(self.splash_screen, _('Bad database location'),
|
||||||
_('Bad database location %r. Will start with '
|
_('Bad database location %r. Will start with '
|
||||||
' a new, empty calibre library')%self.library_path,
|
' a new, empty calibre library')%self.library_path,
|
||||||
det_msg=traceback.format_exc(), show=True)
|
det_msg=traceback.format_exc(), show=True)
|
||||||
|
|
||||||
self.initialize_db_stage2(db, None)
|
self.initialize_db_stage2(db, None)
|
||||||
|
|
||||||
|
def show_splash_screen(self):
|
||||||
|
self.splash_pixmap = QPixmap()
|
||||||
|
self.splash_pixmap.load(I('library.png'))
|
||||||
|
self.splash_screen = QSplashScreen(self.splash_pixmap,
|
||||||
|
Qt.SplashScreen|Qt.WindowStaysOnTopHint)
|
||||||
|
self.splash_screen.showMessage(_('Starting %s: Loading books...') %
|
||||||
|
__appname__)
|
||||||
|
self.splash_screen.show()
|
||||||
|
QApplication.instance().processEvents()
|
||||||
|
|
||||||
def initialize(self, *args):
|
def initialize(self, *args):
|
||||||
|
if gprefs.get('show_splash_screen', True):
|
||||||
|
self.show_splash_screen()
|
||||||
|
|
||||||
self.library_path = get_library_path()
|
self.library_path = get_library_path()
|
||||||
if self.library_path is None:
|
if self.library_path is None:
|
||||||
self.initialization_failed()
|
self.initialization_failed()
|
||||||
|
@ -8,10 +8,11 @@ Browsing book collection by tags.
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
from itertools import izip
|
from itertools import izip
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
||||||
QFont, QSize, QIcon, QPoint, \
|
QFont, QSize, QIcon, QPoint, \
|
||||||
QAbstractItemModel, QVariant, QModelIndex
|
QAbstractItemModel, QVariant, QModelIndex, QMenu
|
||||||
from calibre.gui2 import config, NONE
|
from calibre.gui2 import config, NONE
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.library.field_metadata import TagsIcons
|
from calibre.library.field_metadata import TagsIcons
|
||||||
@ -19,9 +20,12 @@ from calibre.utils.search_query_parser import saved_searches
|
|||||||
|
|
||||||
class TagsView(QTreeView): # {{{
|
class TagsView(QTreeView): # {{{
|
||||||
|
|
||||||
need_refresh = pyqtSignal()
|
need_refresh = pyqtSignal()
|
||||||
restriction_set = pyqtSignal(object)
|
restriction_set = pyqtSignal(object)
|
||||||
tags_marked = pyqtSignal(object, object)
|
tags_marked = pyqtSignal(object, object)
|
||||||
|
user_category_edit = pyqtSignal(object)
|
||||||
|
tag_list_edit = pyqtSignal(object)
|
||||||
|
saved_search_edit = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QTreeView.__init__(self, *args)
|
QTreeView.__init__(self, *args)
|
||||||
@ -31,13 +35,16 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.tag_match = None
|
self.tag_match = None
|
||||||
|
|
||||||
def set_database(self, db, tag_match, popularity, restriction):
|
def set_database(self, db, tag_match, popularity, restriction):
|
||||||
self._model = TagsModel(db, parent=self)
|
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||||
|
self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories)
|
||||||
self.popularity = popularity
|
self.popularity = popularity
|
||||||
self.restriction = restriction
|
self.restriction = restriction
|
||||||
self.tag_match = tag_match
|
self.tag_match = tag_match
|
||||||
self.db = db
|
self.db = db
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.clicked.connect(self.toggle)
|
self.clicked.connect(self.toggle)
|
||||||
|
self.customContextMenuRequested.connect(self.show_context_menu)
|
||||||
self.popularity.setChecked(config['sort_by_popularity'])
|
self.popularity.setChecked(config['sort_by_popularity'])
|
||||||
self.popularity.stateChanged.connect(self.sort_changed)
|
self.popularity.stateChanged.connect(self.sort_changed)
|
||||||
self.restriction.activated[str].connect(self.search_restriction_set)
|
self.restriction.activated[str].connect(self.search_restriction_set)
|
||||||
@ -45,10 +52,6 @@ class TagsView(QTreeView): # {{{
|
|||||||
db.add_listener(self.database_changed)
|
db.add_listener(self.database_changed)
|
||||||
self.saved_searches_changed(recount=False)
|
self.saved_searches_changed(recount=False)
|
||||||
|
|
||||||
def create_tag_category(self, name, tag_list):
|
|
||||||
self._model.create_tag_category(name, tag_list)
|
|
||||||
self.recount()
|
|
||||||
|
|
||||||
def database_changed(self, event, ids):
|
def database_changed(self, event, ids):
|
||||||
self.need_refresh.emit()
|
self.need_refresh.emit()
|
||||||
|
|
||||||
@ -72,12 +75,87 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.recount() # Must happen after the emission of the restriction_set signal
|
self.recount() # Must happen after the emission of the restriction_set signal
|
||||||
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
# Swallow everything except leftButton so context menus work correctly
|
||||||
|
if event.button() == Qt.LeftButton:
|
||||||
|
QTreeView.mouseReleaseEvent(self, event)
|
||||||
|
|
||||||
def toggle(self, index):
|
def toggle(self, index):
|
||||||
modifiers = int(QApplication.keyboardModifiers())
|
modifiers = int(QApplication.keyboardModifiers())
|
||||||
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
|
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
|
||||||
if self._model.toggle(index, exclusive):
|
if self._model.toggle(index, exclusive):
|
||||||
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
||||||
|
|
||||||
|
def context_menu_handler(self, action=None, category=None):
|
||||||
|
if not action:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if action == 'manage_tags':
|
||||||
|
self.tag_list_edit.emit(category)
|
||||||
|
return
|
||||||
|
if action == 'manage_categories':
|
||||||
|
self.user_category_edit.emit(category)
|
||||||
|
return
|
||||||
|
if action == 'manage_searches':
|
||||||
|
self.saved_search_edit.emit(category)
|
||||||
|
return
|
||||||
|
if action == 'hide':
|
||||||
|
self.hidden_categories.add(category)
|
||||||
|
elif action == 'show':
|
||||||
|
self.hidden_categories.discard(category)
|
||||||
|
elif action == 'defaults':
|
||||||
|
self.hidden_categories.clear()
|
||||||
|
config.set('tag_browser_hidden_categories', self.hidden_categories)
|
||||||
|
self.set_new_model()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
def show_context_menu(self, point):
|
||||||
|
index = self.indexAt(point)
|
||||||
|
if not index.isValid():
|
||||||
|
return False
|
||||||
|
item = index.internalPointer()
|
||||||
|
tag_name = ''
|
||||||
|
if item.type == TagTreeItem.TAG:
|
||||||
|
tag_name = item.tag.name
|
||||||
|
item = item.parent
|
||||||
|
if item.type == TagTreeItem.CATEGORY:
|
||||||
|
category = unicode(item.name.toString())
|
||||||
|
self.context_menu = QMenu(self)
|
||||||
|
self.context_menu.addAction(_('Hide %s') % category,
|
||||||
|
partial(self.context_menu_handler, action='hide', category=category))
|
||||||
|
|
||||||
|
if self.hidden_categories:
|
||||||
|
self.context_menu.addSeparator()
|
||||||
|
m = self.context_menu.addMenu(_('Show category'))
|
||||||
|
for col in self.hidden_categories:
|
||||||
|
m.addAction(col,
|
||||||
|
partial(self.context_menu_handler, action='show', category=col))
|
||||||
|
self.context_menu.addSeparator()
|
||||||
|
self.context_menu.addAction(_('Restore defaults'),
|
||||||
|
partial(self.context_menu_handler, action='defaults'))
|
||||||
|
|
||||||
|
self.context_menu.addSeparator()
|
||||||
|
self.context_menu.addAction(_('Manage Tags'),
|
||||||
|
partial(self.context_menu_handler, action='manage_tags',
|
||||||
|
category=tag_name))
|
||||||
|
|
||||||
|
if category in prefs['user_categories'].keys():
|
||||||
|
self.context_menu.addAction(_('Manage User Categories'),
|
||||||
|
partial(self.context_menu_handler, action='manage_categories',
|
||||||
|
category=category))
|
||||||
|
else:
|
||||||
|
self.context_menu.addAction(_('Manage User Categories'),
|
||||||
|
partial(self.context_menu_handler, action='manage_categories',
|
||||||
|
category=None))
|
||||||
|
|
||||||
|
self.context_menu.addAction(_('Manage Saved Searches'),
|
||||||
|
partial(self.context_menu_handler, action='manage_searches',
|
||||||
|
category=tag_name))
|
||||||
|
|
||||||
|
self.context_menu.popup(self.mapToGlobal(point))
|
||||||
|
return True
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.model().clear_state()
|
self.model().clear_state()
|
||||||
|
|
||||||
@ -110,13 +188,12 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.setCurrentIndex(idx)
|
self.setCurrentIndex(idx)
|
||||||
self.scrollTo(idx, QTreeView.PositionAtCenter)
|
self.scrollTo(idx, QTreeView.PositionAtCenter)
|
||||||
|
|
||||||
'''
|
# If the number of user categories changed, if custom columns have come or
|
||||||
If the number of user categories changed, or if custom columns have come or gone,
|
# gone, or if columns have been hidden or restored, we must rebuild the
|
||||||
we must rebuild the model. Reason: it is much easier to do that than to reconstruct
|
# model. Reason: it is much easier than reconstructing the browser tree.
|
||||||
the browser tree.
|
|
||||||
'''
|
|
||||||
def set_new_model(self):
|
def set_new_model(self):
|
||||||
self._model = TagsModel(self.db, parent=self)
|
self._model = TagsModel(self.db, parent=self,
|
||||||
|
hidden_categories=self.hidden_categories)
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -200,7 +277,7 @@ class TagTreeItem(object): # {{{
|
|||||||
|
|
||||||
class TagsModel(QAbstractItemModel): # {{{
|
class TagsModel(QAbstractItemModel): # {{{
|
||||||
|
|
||||||
def __init__(self, db, parent=None):
|
def __init__(self, db, parent=None, hidden_categories=None):
|
||||||
QAbstractItemModel.__init__(self, parent)
|
QAbstractItemModel.__init__(self, parent)
|
||||||
|
|
||||||
# must do this here because 'QPixmap: Must construct a QApplication
|
# must do this here because 'QPixmap: Must construct a QApplication
|
||||||
@ -220,6 +297,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
||||||
self.db = db
|
self.db = db
|
||||||
|
self.hidden_categories = hidden_categories
|
||||||
self.search_restriction = ''
|
self.search_restriction = ''
|
||||||
self.ignore_next_search = 0
|
self.ignore_next_search = 0
|
||||||
|
|
||||||
@ -237,6 +315,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
data = self.get_node_tree(config['sort_by_popularity'])
|
data = self.get_node_tree(config['sort_by_popularity'])
|
||||||
self.root_item = TagTreeItem()
|
self.root_item = TagTreeItem()
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
|
continue
|
||||||
if self.db.field_metadata[r]['kind'] != 'user':
|
if self.db.field_metadata[r]['kind'] != 'user':
|
||||||
tt = _('The lookup/search name is "{0}"').format(r)
|
tt = _('The lookup/search name is "{0}"').format(r)
|
||||||
else:
|
else:
|
||||||
@ -271,12 +351,16 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
data = self.get_node_tree(config['sort_by_popularity']) # get category data
|
data = self.get_node_tree(config['sort_by_popularity']) # get category data
|
||||||
|
row_index = -1
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
category = self.root_item.children[i]
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
|
continue
|
||||||
|
row_index += 1
|
||||||
|
category = self.root_item.children[row_index]
|
||||||
names = [t.tag.name for t in category.children]
|
names = [t.tag.name for t in category.children]
|
||||||
states = [t.tag.state for t in category.children]
|
states = [t.tag.state for t in category.children]
|
||||||
state_map = dict(izip(names, states))
|
state_map = dict(izip(names, states))
|
||||||
category_index = self.index(i, 0, QModelIndex())
|
category_index = self.index(row_index, 0, QModelIndex())
|
||||||
if len(category.children) > 0:
|
if len(category.children) > 0:
|
||||||
self.beginRemoveRows(category_index, 0,
|
self.beginRemoveRows(category_index, 0,
|
||||||
len(category.children)-1)
|
len(category.children)-1)
|
||||||
@ -401,16 +485,20 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
def tokens(self):
|
def tokens(self):
|
||||||
ans = []
|
ans = []
|
||||||
tags_seen = set()
|
tags_seen = set()
|
||||||
|
row_index = -1
|
||||||
for i, key in enumerate(self.row_map):
|
for i, key in enumerate(self.row_map):
|
||||||
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
|
continue
|
||||||
|
row_index += 1
|
||||||
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
|
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
|
||||||
continue
|
continue
|
||||||
category_item = self.root_item.children[i]
|
category_item = self.root_item.children[row_index]
|
||||||
for tag_item in category_item.children:
|
for tag_item in category_item.children:
|
||||||
tag = tag_item.tag
|
tag = tag_item.tag
|
||||||
if tag.state > 0:
|
if tag.state > 0:
|
||||||
prefix = ' not ' if tag.state == 2 else ''
|
prefix = ' not ' if tag.state == 2 else ''
|
||||||
category = key if key != 'news' else 'tag'
|
category = key if key != 'news' else 'tag'
|
||||||
if tag.name[0] == u'\u2605': # char is a star. Assume rating
|
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
|
||||||
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
|
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
|
||||||
else:
|
else:
|
||||||
if category == 'tags':
|
if category == 'tags':
|
||||||
|
@ -61,6 +61,8 @@ from calibre.library.database2 import LibraryDatabase2
|
|||||||
from calibre.library.caches import CoverCache
|
from calibre.library.caches import CoverCache
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.dialogs.tag_categories import TagCategories
|
from calibre.gui2.dialogs.tag_categories import TagCategories
|
||||||
|
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
||||||
|
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
||||||
|
|
||||||
class SaveMenu(QMenu):
|
class SaveMenu(QMenu):
|
||||||
|
|
||||||
@ -127,13 +129,18 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
pixmap_to_data(pixmap))
|
pixmap_to_data(pixmap))
|
||||||
|
|
||||||
self.last_time = datetime.datetime.now()
|
self.last_time = datetime.datetime.now()
|
||||||
def __init__(self, library_path, db, listener, opts, actions, parent=None):
|
|
||||||
|
def __init__(self, opts, parent=None):
|
||||||
|
MainWindow.__init__(self, opts, parent)
|
||||||
|
self.opts = opts
|
||||||
|
|
||||||
|
def initialize(self, library_path, db, listener, actions):
|
||||||
|
opts = self.opts
|
||||||
self.last_time = datetime.datetime.now()
|
self.last_time = datetime.datetime.now()
|
||||||
self.preferences_action, self.quit_action = actions
|
self.preferences_action, self.quit_action = actions
|
||||||
self.library_path = library_path
|
self.library_path = library_path
|
||||||
self.spare_servers = []
|
self.spare_servers = []
|
||||||
self.must_restart_before_config = False
|
self.must_restart_before_config = False
|
||||||
MainWindow.__init__(self, opts, parent)
|
|
||||||
# Initialize fontconfig in a separate thread as this can be a lengthy
|
# Initialize fontconfig in a separate thread as this can be a lengthy
|
||||||
# process if run for the first time on this machine
|
# process if run for the first time on this machine
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts import fontconfig
|
||||||
@ -537,19 +544,23 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.cover_cache = CoverCache(self.library_path)
|
self.cover_cache = CoverCache(self.library_path)
|
||||||
self.cover_cache.start()
|
self.cover_cache.start()
|
||||||
self.library_view.model().cover_cache = self.cover_cache
|
self.library_view.model().cover_cache = self.cover_cache
|
||||||
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_edit_categories)
|
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
|
||||||
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
|
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
|
||||||
self.tags_view.tags_marked.connect(self.search.search_from_tags)
|
self.tags_view.tags_marked.connect(self.search.search_from_tags)
|
||||||
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
|
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
|
||||||
self.tags_view.restriction_set.connect(x)
|
self.tags_view.restriction_set.connect(x)
|
||||||
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
|
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
|
||||||
|
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
||||||
|
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
||||||
|
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
||||||
self.search.search.connect(self.tags_view.model().reinit)
|
self.search.search.connect(self.tags_view.model().reinit)
|
||||||
for x in (self.location_view.count_changed, self.tags_view.recount,
|
for x in (self.location_view.count_changed, self.tags_view.recount,
|
||||||
self.restriction_count_changed):
|
self.restriction_count_changed):
|
||||||
self.library_view.model().count_changed_signal.connect(x)
|
self.library_view.model().count_changed_signal.connect(x)
|
||||||
|
|
||||||
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
||||||
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.saved_searches_changed, Qt.QueuedConnection)
|
self.connect(self.saved_search, SIGNAL('changed()'),
|
||||||
|
self.tags_view.saved_searches_changed, Qt.QueuedConnection)
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||||
@ -642,13 +653,28 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
|
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
|
||||||
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
def do_edit_categories(self):
|
def do_user_categories_edit(self, on_category=None):
|
||||||
d = TagCategories(self, self.library_view.model().db)
|
d = TagCategories(self, self.library_view.model().db, on_category)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
if d.result() == d.Accepted:
|
if d.result() == d.Accepted:
|
||||||
self.tags_view.set_new_model()
|
self.tags_view.set_new_model()
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
|
||||||
|
def do_tags_list_edit(self, tag):
|
||||||
|
d = TagListEditor(self, self.library_view.model().db, tag)
|
||||||
|
d.exec_()
|
||||||
|
if d.result() == d.Accepted:
|
||||||
|
self.tags_view.set_new_model()
|
||||||
|
self.tags_view.recount()
|
||||||
|
self.library_view.model().refresh()
|
||||||
|
|
||||||
|
def do_saved_search_edit(self, search):
|
||||||
|
d = SavedSearchEditor(self, search)
|
||||||
|
d.exec_()
|
||||||
|
if d.result() == d.Accepted:
|
||||||
|
self.tags_view.saved_searches_changed(recount=True)
|
||||||
|
self.saved_search.clear_to_help()
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
MainWindow.resizeEvent(self, ev)
|
MainWindow.resizeEvent(self, ev)
|
||||||
self.search.setMaximumWidth(self.width()-150)
|
self.search.setMaximumWidth(self.width()-150)
|
||||||
|
@ -17,7 +17,7 @@ from calibre.utils.config import tweaks
|
|||||||
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.utils.pyparsing import ParseException
|
from calibre.utils.pyparsing import ParseException
|
||||||
# from calibre.library.field_metadata import FieldMetadata
|
from calibre.ebooks.metadata import title_sort
|
||||||
|
|
||||||
class CoverCache(QThread):
|
class CoverCache(QThread):
|
||||||
|
|
||||||
@ -564,7 +564,8 @@ class ResultCache(SearchQueryParser):
|
|||||||
def seriescmp(self, x, y):
|
def seriescmp(self, x, y):
|
||||||
sidx = self.FIELD_MAP['series']
|
sidx = self.FIELD_MAP['series']
|
||||||
try:
|
try:
|
||||||
ans = cmp(self._data[x][sidx].lower(), self._data[y][sidx].lower())
|
ans = cmp(title_sort(self._data[x][sidx].lower()),
|
||||||
|
title_sort(self._data[y][sidx].lower()))
|
||||||
except AttributeError: # Some entries may be None
|
except AttributeError: # Some entries may be None
|
||||||
ans = cmp(self._data[x][sidx], self._data[y][sidx])
|
ans = cmp(self._data[x][sidx], self._data[y][sidx])
|
||||||
if ans != 0: return ans
|
if ans != 0: return ans
|
||||||
|
@ -648,6 +648,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
|
self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
|
||||||
self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag'))
|
self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag'))
|
||||||
self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series'))
|
self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series'))
|
||||||
|
for id_, tag in self.conn.get('SELECT id, name FROM tags', all=True):
|
||||||
|
if not tag.strip():
|
||||||
|
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?',
|
||||||
|
(id_,))
|
||||||
|
self.conn.execute('DELETE FROM tags WHERE id=?', (id_,))
|
||||||
self.clean_custom()
|
self.clean_custom()
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
@ -725,6 +730,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
||||||
icon=icon, tooltip = tooltip)
|
icon=icon, tooltip = tooltip)
|
||||||
for r in data if item_not_zero_func(r)]
|
for r in data if item_not_zero_func(r)]
|
||||||
|
if category == 'series':
|
||||||
|
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name),
|
||||||
|
title_sort(y.name)))
|
||||||
|
|
||||||
# We delayed computing the standard formats category because it does not
|
# We delayed computing the standard formats category because it does not
|
||||||
# use a view, but is computed dynamically
|
# use a view, but is computed dynamically
|
||||||
@ -977,6 +985,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if notify:
|
if notify:
|
||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
|
# Convenience method for tags_list_editor
|
||||||
|
def get_tags_with_ids(self):
|
||||||
|
result = self.conn.get('SELECT * FROM tags')
|
||||||
|
if not result:
|
||||||
|
return {}
|
||||||
|
r = []
|
||||||
|
for k,v in result:
|
||||||
|
r.append((k,v))
|
||||||
|
return r
|
||||||
|
|
||||||
|
def rename_tag(self, id, new):
|
||||||
|
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
def get_tags(self, id):
|
def get_tags(self, id):
|
||||||
result = self.conn.get(
|
result = self.conn.get(
|
||||||
'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)',
|
'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)',
|
||||||
|
@ -16,7 +16,7 @@ except ImportError:
|
|||||||
|
|
||||||
from calibre import fit_image, guess_type
|
from calibre import fit_image, guess_type
|
||||||
from calibre.utils.date import fromtimestamp
|
from calibre.utils.date import fromtimestamp
|
||||||
|
from calibre.ebooks.metadata import title_sort
|
||||||
|
|
||||||
class ContentServer(object):
|
class ContentServer(object):
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ class ContentServer(object):
|
|||||||
def seriescmp(self, x, y):
|
def seriescmp(self, x, y):
|
||||||
si = self.db.FIELD_MAP['series']
|
si = self.db.FIELD_MAP['series']
|
||||||
try:
|
try:
|
||||||
ans = cmp(x[si].lower(), y[si].lower())
|
ans = cmp(title_sort(x[si].lower()), title_sort(y[si].lower()))
|
||||||
except AttributeError: # Some entries may be None
|
except AttributeError: # Some entries may be None
|
||||||
ans = cmp(x[si], y[si])
|
ans = cmp(x[si], y[si])
|
||||||
if ans != 0: return ans
|
if ans != 0: return ans
|
||||||
|
@ -453,7 +453,7 @@ as HTML and then convert the resulting HTML file with |app|. When saving as HTML
|
|||||||
|
|
||||||
There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes
|
There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes
|
||||||
generating the Table of Contents much simpler. It is called BookCreator and is available for free
|
generating the Table of Contents much simpler. It is called BookCreator and is available for free
|
||||||
`here <http://www.mobileread.com/forums/showthread.php?t=28313>`_.
|
at `mobileread <http://www.mobileread.com/forums/showthread.php?t=28313>`_.
|
||||||
|
|
||||||
Convert TXT documents
|
Convert TXT documents
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@ -493,7 +493,7 @@ TXT input supports a number of options to differentiate how paragraphs are detec
|
|||||||
allows for basic formatting to be added to TXT documents, such as bold, italics, section headings, tables,
|
allows for basic formatting to be added to TXT documents, such as bold, italics, section headings, tables,
|
||||||
lists, a Table of Contents, etc. Marking chapter headings with a leading # and setting the chapter XPath detection
|
lists, a Table of Contents, etc. Marking chapter headings with a leading # and setting the chapter XPath detection
|
||||||
expression to "//h:h1" is the easiest way to have a proper table of contents generated from a TXT document.
|
expression to "//h:h1" is the easiest way to have a proper table of contents generated from a TXT document.
|
||||||
You can learn more about the markdown syntax `here <http://daringfireball.net/projects/markdown/syntax>`_.
|
You can learn more about the markdown syntax at `daringfireball <http://daringfireball.net/projects/markdown/syntax>`_.
|
||||||
|
|
||||||
|
|
||||||
Convert PDF documents
|
Convert PDF documents
|
||||||
@ -540,7 +540,7 @@ EPUB advanced formatting demo
|
|||||||
Various advanced formatting for EPUB files is demonstrated in this `demo file <http://calibre-ebook.com/downloads/demos/demo.epub>`_.
|
Various advanced formatting for EPUB files is demonstrated in this `demo file <http://calibre-ebook.com/downloads/demos/demo.epub>`_.
|
||||||
The file was created from hand coded HTML using calibre and is meant to be used as a template for your own EPUB creation efforts.
|
The file was created from hand coded HTML using calibre and is meant to be used as a template for your own EPUB creation efforts.
|
||||||
|
|
||||||
The source HTML it was created from is available `here <http://calibre-ebook.com/downloads/demos/demo.zip>`_. The settings used to create the
|
The source HTML it was created from is available `demo.zip <http://calibre-ebook.com/downloads/demos/demo.zip>`_. The settings used to create the
|
||||||
EPUB from the ZIP file are::
|
EPUB from the ZIP file are::
|
||||||
|
|
||||||
ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover
|
ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover
|
||||||
|
@ -133,7 +133,7 @@ Can I use the collections feature of the SONY reader?
|
|||||||
turned into a collection on the reader. Note that the PRS-500 does not support collections for books stored on the SD card. The PRS-505 does.
|
turned into a collection on the reader. Note that the PRS-500 does not support collections for books stored on the SD card. The PRS-505 does.
|
||||||
|
|
||||||
How do I use |app| with my iPad/iPhone/iTouch?
|
How do I use |app| with my iPad/iPhone/iTouch?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can access your calibre library on a iPad/iPhone/iTouch over the air using the calibre content server.
|
You can access your calibre library on a iPad/iPhone/iTouch over the air using the calibre content server.
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ Pre/post processing of downloaded HTML
|
|||||||
|
|
||||||
.. automethod:: BasicNewsRecipe.postprocess_html
|
.. automethod:: BasicNewsRecipe.postprocess_html
|
||||||
|
|
||||||
|
.. automethod:: BasicNewsRecipe.populate_article_metadata
|
||||||
|
|
||||||
|
|
||||||
Convenience methods
|
Convenience methods
|
||||||
|
@ -51,6 +51,8 @@ class FontMetrics(object):
|
|||||||
|
|
||||||
|
|
||||||
def get_font_metrics(image, d_wand, text):
|
def get_font_metrics(image, d_wand, text):
|
||||||
|
if isinstance(text, unicode):
|
||||||
|
text = text.encode('utf-8')
|
||||||
ret = p.MagickQueryFontMetrics(image, d_wand, text)
|
ret = p.MagickQueryFontMetrics(image, d_wand, text)
|
||||||
return FontMetrics(ret)
|
return FontMetrics(ret)
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
#: If True empty feeds are removed from the output.
|
#: If True empty feeds are removed from the output.
|
||||||
#: This option has no effect if parse_index is overriden in
|
#: This option has no effect if parse_index is overriden in
|
||||||
#: the sub class. It is meant only for recipes that return a list
|
#: the sub class. It is meant only for recipes that return a list
|
||||||
#: of feeds using :member:`feeds` or :method:`get_feeds`.
|
#: of feeds using `feeds` or :method:`get_feeds`.
|
||||||
remove_empty_feeds = False
|
remove_empty_feeds = False
|
||||||
|
|
||||||
#: List of regular expressions that determines which links to follow
|
#: List of regular expressions that determines which links to follow
|
||||||
@ -256,7 +256,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
|
|
||||||
#: The CSS that is used to style the templates, i.e., the navigation bars and
|
#: The CSS that is used to style the templates, i.e., the navigation bars and
|
||||||
#: the Tables of Contents. Rather than overriding this variable, you should
|
#: the Tables of Contents. Rather than overriding this variable, you should
|
||||||
#: use :member:`extra_css` in your recipe to customize look and feel.
|
#: use `extra_css` in your recipe to customize look and feel.
|
||||||
template_css = u'''
|
template_css = u'''
|
||||||
.article_date {
|
.article_date {
|
||||||
color: gray; font-family: monospace;
|
color: gray; font-family: monospace;
|
||||||
@ -506,7 +506,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
|
|
||||||
def get_obfuscated_article(self, url):
|
def get_obfuscated_article(self, url):
|
||||||
'''
|
'''
|
||||||
If you set :member:`articles_are_obfuscated` this method is called with
|
If you set `articles_are_obfuscated` this method is called with
|
||||||
every article URL. It should return the path to a file on the filesystem
|
every article URL. It should return the path to a file on the filesystem
|
||||||
that contains the article HTML. That file is processed by the recursive
|
that contains the article HTML. That file is processed by the recursive
|
||||||
HTML fetching engine, so it can contain links to pages/images on the web.
|
HTML fetching engine, so it can contain links to pages/images on the web.
|
||||||
@ -517,20 +517,18 @@ class BasicNewsRecipe(Recipe):
|
|||||||
'''
|
'''
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def extract_author(self, soup):
|
def populate_article_metadata(self, article, soup, first):
|
||||||
'''
|
'''
|
||||||
Parse downloaded articles for author, add to OEBBook object.
|
Called when each HTML page belonging to article is downloaded.
|
||||||
:param soup:
|
Intended to be used to get article metadata like author/summary/etc.
|
||||||
|
from the parsed HTML (soup).
|
||||||
|
:param article: A object of class :class:`calibre.web.feeds.Article`.
|
||||||
|
If you change the sumamry, remember to also change the
|
||||||
|
text_summary
|
||||||
|
:param soup: Parsed HTML belonging to this article
|
||||||
|
:param first: True iff the parsed HTML is the first page of the article.
|
||||||
'''
|
'''
|
||||||
return None
|
pass
|
||||||
|
|
||||||
def extract_description(self, soup):
|
|
||||||
'''
|
|
||||||
Parse downloaded articles for description, add to OEBBook object.
|
|
||||||
:param soup:
|
|
||||||
'''
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def postprocess_book(self, oeb, opts, log):
|
def postprocess_book(self, oeb, opts, log):
|
||||||
'''
|
'''
|
||||||
@ -559,8 +557,8 @@ class BasicNewsRecipe(Recipe):
|
|||||||
self.username = options.username
|
self.username = options.username
|
||||||
self.password = options.password
|
self.password = options.password
|
||||||
self.lrf = options.lrf
|
self.lrf = options.lrf
|
||||||
self.output_profile = options.output_profile.name
|
self.output_profile = options.output_profile
|
||||||
self.touchscreen = getattr(options.output_profile,'touchscreen',False)
|
self.touchscreen = getattr(self.output_profile, 'touchscreen', False)
|
||||||
|
|
||||||
self.output_dir = os.path.abspath(self.output_dir)
|
self.output_dir = os.path.abspath(self.output_dir)
|
||||||
if options.test:
|
if options.test:
|
||||||
@ -655,7 +653,15 @@ class BasicNewsRecipe(Recipe):
|
|||||||
for base in list(soup.findAll(['base', 'iframe'])):
|
for base in list(soup.findAll(['base', 'iframe'])):
|
||||||
base.extract()
|
base.extract()
|
||||||
|
|
||||||
return self.postprocess_html(soup, first_fetch)
|
ans = self.postprocess_html(soup, first_fetch)
|
||||||
|
try:
|
||||||
|
article = self.feed_objects[f].articles[a]
|
||||||
|
except:
|
||||||
|
self.log.exception('Failed to get article object for postprocessing')
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.populate_article_metadata(article, ans, first_fetch)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def download(self):
|
def download(self):
|
||||||
@ -879,6 +885,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
if hasattr(feed, 'reverse'):
|
if hasattr(feed, 'reverse'):
|
||||||
feed.reverse()
|
feed.reverse()
|
||||||
|
|
||||||
|
self.feed_objects = feeds
|
||||||
for f, feed in enumerate(feeds):
|
for f, feed in enumerate(feeds):
|
||||||
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
|
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
|
||||||
if not os.path.isdir(feed_dir):
|
if not os.path.isdir(feed_dir):
|
||||||
@ -927,41 +934,9 @@ class BasicNewsRecipe(Recipe):
|
|||||||
|
|
||||||
#feeds.restore_duplicates()
|
#feeds.restore_duplicates()
|
||||||
|
|
||||||
# GwR Populate any missing author/description fields in feed
|
|
||||||
for f, feed in enumerate(feeds):
|
for f, feed in enumerate(feeds):
|
||||||
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
|
|
||||||
for article in feed.articles:
|
|
||||||
if article.summary == '' or article.author == '':
|
|
||||||
file = os.path.join(self.output_dir,feed_dir, article.url)
|
|
||||||
if os.path.exists(file):
|
|
||||||
with open(file, 'rb') as fi:
|
|
||||||
src = fi.read().decode('utf-8')
|
|
||||||
soup = BeautifulSoup(src)
|
|
||||||
if article.author == '':
|
|
||||||
author = self.extract_author(soup)
|
|
||||||
if author and not isinstance(author, unicode):
|
|
||||||
author = author.decode('utf-8', 'replace')
|
|
||||||
article.author = author
|
|
||||||
|
|
||||||
if article.summary == '':
|
|
||||||
summary = article.summary = self.extract_description(soup)
|
|
||||||
if summary and not isinstance(summary, unicode):
|
|
||||||
summary = summary.decode('utf-8', 'replace')
|
|
||||||
if summary and '<' in summary:
|
|
||||||
try:
|
|
||||||
s = html.fragment_fromstring(summary, create_parent=True)
|
|
||||||
summary = html.tostring(s, method='text', encoding=unicode)
|
|
||||||
except:
|
|
||||||
print 'Failed to process article summary, deleting:'
|
|
||||||
print summary.encode('utf-8')
|
|
||||||
traceback.print_exc()
|
|
||||||
summary = u''
|
|
||||||
article.text_summary = summary
|
|
||||||
|
|
||||||
|
|
||||||
for f, feed in enumerate(feeds):
|
|
||||||
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
|
|
||||||
html = self.feed2index(feed)
|
html = self.feed2index(feed)
|
||||||
|
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
|
||||||
with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi:
|
with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi:
|
||||||
fi.write(html)
|
fi.write(html)
|
||||||
self.create_opf(feeds)
|
self.create_opf(feeds)
|
||||||
@ -1040,47 +1015,13 @@ class BasicNewsRecipe(Recipe):
|
|||||||
Create a generic cover for recipes that dont have a cover
|
Create a generic cover for recipes that dont have a cover
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
try:
|
from calibre.utils.magick_draw import create_cover_page, TextLine
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
Image, ImageDraw, ImageFont
|
|
||||||
except ImportError:
|
|
||||||
import Image, ImageDraw, ImageFont
|
|
||||||
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
|
|
||||||
title = self.title if isinstance(self.title, unicode) else \
|
title = self.title if isinstance(self.title, unicode) else \
|
||||||
self.title.decode(preferred_encoding, 'replace')
|
self.title.decode(preferred_encoding, 'replace')
|
||||||
date = strftime(self.timefmt)
|
date = strftime(self.timefmt)
|
||||||
app = '['+__appname__ +' '+__version__+']'
|
lines = [TextLine(title, 44), TextLine(date, 32)]
|
||||||
|
img_data = create_cover_page(lines, I('library.png'), output_format='jpg')
|
||||||
COVER_WIDTH, COVER_HEIGHT = 590, 750
|
cover_file.write(img_data)
|
||||||
img = Image.new('RGB', (COVER_WIDTH, COVER_HEIGHT), 'white')
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
# Title
|
|
||||||
font = ImageFont.truetype(font_path, 44)
|
|
||||||
width, height = draw.textsize(title, font=font)
|
|
||||||
left = max(int((COVER_WIDTH - width)/2.), 0)
|
|
||||||
top = 15
|
|
||||||
draw.text((left, top), title, fill=(0,0,0), font=font)
|
|
||||||
bottom = top + height
|
|
||||||
# Date
|
|
||||||
font = ImageFont.truetype(font_path, 32)
|
|
||||||
width, height = draw.textsize(date, font=font)
|
|
||||||
left = max(int((COVER_WIDTH - width)/2.), 0)
|
|
||||||
draw.text((left, bottom+15), date, fill=(0,0,0), font=font)
|
|
||||||
# Vanity
|
|
||||||
font = ImageFont.truetype(font_path, 28)
|
|
||||||
width, height = draw.textsize(app, font=font)
|
|
||||||
left = max(int((COVER_WIDTH - width)/2.), 0)
|
|
||||||
top = COVER_HEIGHT - height - 15
|
|
||||||
draw.text((left, top), app, fill=(0,0,0), font=font)
|
|
||||||
# Logo
|
|
||||||
logo = Image.open(I('library.png'), 'r')
|
|
||||||
width, height = logo.size
|
|
||||||
left = max(int((COVER_WIDTH - width)/2.), 0)
|
|
||||||
top = max(int((COVER_HEIGHT - height)/2.), 0)
|
|
||||||
img.paste(logo, (left, top))
|
|
||||||
img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE)
|
|
||||||
|
|
||||||
img.convert('RGB').save(cover_file, 'JPEG')
|
|
||||||
cover_file.flush()
|
cover_file.flush()
|
||||||
except:
|
except:
|
||||||
self.log.exception('Failed to generate default cover')
|
self.log.exception('Failed to generate default cover')
|
||||||
@ -1173,21 +1114,20 @@ class BasicNewsRecipe(Recipe):
|
|||||||
pw.DestroyMagickWand(x)
|
pw.DestroyMagickWand(x)
|
||||||
|
|
||||||
def create_opf(self, feeds, dir=None):
|
def create_opf(self, feeds, dir=None):
|
||||||
|
|
||||||
if dir is None:
|
if dir is None:
|
||||||
dir = self.output_dir
|
dir = self.output_dir
|
||||||
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
|
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
|
||||||
mi.author_sort = __appname__
|
|
||||||
if self.output_profile == 'iPad':
|
|
||||||
mi = MetaInformation(self.short_title(), [strftime('%A, %d %B %Y')])
|
|
||||||
mi.author_sort = strftime('%Y-%m-%d')
|
|
||||||
mi.publisher = __appname__
|
mi.publisher = __appname__
|
||||||
|
mi.author_sort = __appname__
|
||||||
|
if self.output_profile.name == 'iPad':
|
||||||
|
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
||||||
|
mi.authors = [date_as_author]
|
||||||
|
mi.author_sort = strftime('%Y-%m-%d')
|
||||||
mi.publication_type = 'periodical:'+self.publication_type
|
mi.publication_type = 'periodical:'+self.publication_type
|
||||||
mi.timestamp = nowf()
|
mi.timestamp = nowf()
|
||||||
mi.comments = self.description
|
mi.comments = self.description
|
||||||
if not isinstance(mi.comments, unicode):
|
if not isinstance(mi.comments, unicode):
|
||||||
mi.comments = mi.comments.decode('utf-8', 'replace')
|
mi.comments = mi.comments.decode('utf-8', 'replace')
|
||||||
mi.tags = ['News']
|
|
||||||
mi.pubdate = nowf()
|
mi.pubdate = nowf()
|
||||||
opf_path = os.path.join(dir, 'index.opf')
|
opf_path = os.path.join(dir, 'index.opf')
|
||||||
ncx_path = os.path.join(dir, 'index.ncx')
|
ncx_path = os.path.join(dir, 'index.ncx')
|
||||||
@ -1230,7 +1170,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
|
|
||||||
entries = ['index.html']
|
entries = ['index.html']
|
||||||
toc = TOC(base_path=dir)
|
toc = TOC(base_path=dir)
|
||||||
self.play_order_counter = 1
|
self.play_order_counter = 0
|
||||||
self.play_order_map = {}
|
self.play_order_map = {}
|
||||||
|
|
||||||
def feed_index(num, parent):
|
def feed_index(num, parent):
|
||||||
@ -1342,7 +1282,6 @@ class BasicNewsRecipe(Recipe):
|
|||||||
Create a list of articles from the list of feeds returned by :meth:`BasicNewsRecipe.get_feeds`.
|
Create a list of articles from the list of feeds returned by :meth:`BasicNewsRecipe.get_feeds`.
|
||||||
Return a list of :class:`Feed` objects.
|
Return a list of :class:`Feed` objects.
|
||||||
'''
|
'''
|
||||||
print "\nweb.feeds.news:parse_feeds()\n"
|
|
||||||
feeds = self.get_feeds()
|
feeds = self.get_feeds()
|
||||||
parsed_feeds = []
|
parsed_feeds = []
|
||||||
for obj in feeds:
|
for obj in feeds:
|
||||||
|
@ -120,6 +120,7 @@ class TouchscreenNavBarTemplate(Template):
|
|||||||
href = '%s%s/%s/index.html'%(prefix, up, next)
|
href = '%s%s/%s/index.html'%(prefix, up, next)
|
||||||
navbar.text = '| '
|
navbar.text = '| '
|
||||||
navbar.append(A('Next', href=href))
|
navbar.append(A('Next', href=href))
|
||||||
|
|
||||||
href = '%s../index.html#article_%d'%(prefix, art)
|
href = '%s../index.html#article_%d'%(prefix, art)
|
||||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||||
navbar.append(A('Section Menu', href=href))
|
navbar.append(A('Section Menu', href=href))
|
||||||
@ -130,6 +131,7 @@ class TouchscreenNavBarTemplate(Template):
|
|||||||
href = '%s../article_%d/index.html'%(prefix, art-1)
|
href = '%s../article_%d/index.html'%(prefix, art-1)
|
||||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||||
navbar.append(A('Previous', href=href))
|
navbar.append(A('Previous', href=href))
|
||||||
|
|
||||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||||
if not bottom:
|
if not bottom:
|
||||||
navbar.append(HR())
|
navbar.append(HR())
|
||||||
@ -165,8 +167,14 @@ class TouchscreenIndexTemplate(Template):
|
|||||||
def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
|
def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
|
||||||
if isinstance(datefmt, unicode):
|
if isinstance(datefmt, unicode):
|
||||||
datefmt = datefmt.encode(preferred_encoding)
|
datefmt = datefmt.encode(preferred_encoding)
|
||||||
date = strftime(datefmt)
|
date = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
||||||
masthead_img = IMG(src=masthead,alt="masthead")
|
masthead_p = etree.Element("p")
|
||||||
|
masthead_p.set("style","text-align:center")
|
||||||
|
masthead_img = etree.Element("img")
|
||||||
|
masthead_img.set("src",masthead)
|
||||||
|
masthead_img.set("alt","masthead")
|
||||||
|
masthead_p.append(masthead_img)
|
||||||
|
|
||||||
head = HEAD(TITLE(title))
|
head = HEAD(TITLE(title))
|
||||||
if style:
|
if style:
|
||||||
head.append(STYLE(style, type='text/css'))
|
head.append(STYLE(style, type='text/css'))
|
||||||
@ -178,11 +186,11 @@ class TouchscreenIndexTemplate(Template):
|
|||||||
if feed:
|
if feed:
|
||||||
tr = TR()
|
tr = TR()
|
||||||
tr.append(TD( CLASS('toc_item'), A(feed.title, href='feed_%d/index.html'%i)))
|
tr.append(TD( CLASS('toc_item'), A(feed.title, href='feed_%d/index.html'%i)))
|
||||||
tr.append(TD( CLASS('article_count'),'%d' % len(feed.articles)))
|
tr.append(TD( CLASS('article_count'),'%3.3s' % len(feed.articles)))
|
||||||
toc.append(tr)
|
toc.append(tr)
|
||||||
|
|
||||||
div = DIV(
|
div = DIV(
|
||||||
PT(masthead_img,style='text-align:center'),
|
masthead_p,
|
||||||
PT(date, style='text-align:center'),
|
PT(date, style='text-align:center'),
|
||||||
toc,
|
toc,
|
||||||
CLASS('calibre_rescale_100'))
|
CLASS('calibre_rescale_100'))
|
||||||
|
@ -329,7 +329,7 @@ class RecursiveFetcher(object):
|
|||||||
try:
|
try:
|
||||||
data = self.fetch_url(iurl)
|
data = self.fetch_url(iurl)
|
||||||
if data == 'GIF89a\x01':
|
if data == 'GIF89a\x01':
|
||||||
# Skip empty GIF files
|
# Skip empty GIF files as PIL errors on them anyway
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception('Could not fetch image %s'% iurl)
|
self.log.exception('Could not fetch image %s'% iurl)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user