mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
0a31b6ac00
BIN
resources/images/news/lwn_weekly.png
Normal file
BIN
resources/images/news/lwn_weekly.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 387 B |
31
resources/recipes/helsingin_sanomat.recipe
Normal file
31
resources/recipes/helsingin_sanomat.recipe
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1298137661(BasicNewsRecipe):
|
||||||
|
title = u'Helsingin Sanomat'
|
||||||
|
__author__ = 'oneillpt'
|
||||||
|
language = 'fi'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
conversion_options = {
|
||||||
|
'linearize_tables' : True
|
||||||
|
}
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='a', attrs={'id':'articleCommentUrl'}),
|
||||||
|
dict(name='p', attrs={'class':'newsSummary'}),
|
||||||
|
dict(name='div', attrs={'class':'headerTools'})
|
||||||
|
]
|
||||||
|
|
||||||
|
feeds = [(u'Uutiset - HS.fi', u'http://www.hs.fi/uutiset/rss/'), (u'Politiikka - HS.fi', u'http://www.hs.fi/politiikka/rss/'),
|
||||||
|
(u'Ulkomaat - HS.fi', u'http://www.hs.fi/ulkomaat/rss/'), (u'Kulttuuri - HS.fi', u'http://www.hs.fi/kulttuuri/rss/'),
|
||||||
|
(u'Kirjat - HS.fi', u'http://www.hs.fi/kulttuuri/kirjat/rss/'), (u'Elokuvat - HS.fi', u'http://www.hs.fi/kulttuuri/elokuvat/rss/')
|
||||||
|
]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
j = url.rfind("/")
|
||||||
|
s = url[j:]
|
||||||
|
i = s.rfind("?ref=rss")
|
||||||
|
if i > 0:
|
||||||
|
s = s[:i]
|
||||||
|
return "http://www.hs.fi/tulosta" + s
|
104
resources/recipes/lwn_weekly.recipe
Normal file
104
resources/recipes/lwn_weekly.recipe
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Davide Cavalca <davide125 at tiscali.it>'
|
||||||
|
'''
|
||||||
|
lwn.net
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
import re
|
||||||
|
|
||||||
|
class WeeklyLWN(BasicNewsRecipe):
|
||||||
|
title = 'LWN.net Weekly Edition'
|
||||||
|
description = 'Weekly summary of what has happened in the free software world.'
|
||||||
|
__author__ = 'Davide Cavalca'
|
||||||
|
language = 'en'
|
||||||
|
|
||||||
|
cover_url = 'http://lwn.net/images/lcorner.png'
|
||||||
|
#masthead_url = 'http://lwn.net/images/lcorner.png'
|
||||||
|
publication_type = 'magazine'
|
||||||
|
|
||||||
|
remove_tags_before = dict(attrs={'class':'PageHeadline'})
|
||||||
|
remove_tags_after = dict(attrs={'class':'ArticleText'})
|
||||||
|
remove_tags = [dict(name=['h2', 'form'])]
|
||||||
|
|
||||||
|
conversion_options = { 'linearize_tables' : True }
|
||||||
|
|
||||||
|
oldest_article = 7.0
|
||||||
|
needs_subscription = 'optional'
|
||||||
|
|
||||||
|
def get_browser(self):
|
||||||
|
br = BasicNewsRecipe.get_browser()
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
br.open('https://lwn.net/login')
|
||||||
|
br.select_form(name='loginform')
|
||||||
|
br['Username'] = self.username
|
||||||
|
br['Password'] = self.password
|
||||||
|
br.submit()
|
||||||
|
return br
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
index_url = 'http://lwn.net/current/bigpage'
|
||||||
|
else:
|
||||||
|
index_url = 'http://lwn.net/free/bigpage'
|
||||||
|
soup = self.index_to_soup(index_url)
|
||||||
|
body = soup.body
|
||||||
|
|
||||||
|
articles = {}
|
||||||
|
ans = []
|
||||||
|
url_re = re.compile('^http://lwn.net/Articles/')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
tag_title = body.findNext(name='p', attrs={'class':'SummaryHL'})
|
||||||
|
if tag_title == None:
|
||||||
|
break
|
||||||
|
|
||||||
|
tag_section = tag_title.findPrevious(name='p', attrs={'class':'Cat1HL'})
|
||||||
|
if tag_section == None:
|
||||||
|
section = 'Front Page'
|
||||||
|
else:
|
||||||
|
section = tag_section.string
|
||||||
|
|
||||||
|
tag_section2 = tag_title.findPrevious(name='p', attrs={'class':'Cat2HL'})
|
||||||
|
if tag_section2 != None:
|
||||||
|
if tag_section2.findPrevious(name='p', attrs={'class':'Cat1HL'}) == tag_section:
|
||||||
|
section = "%s: %s" %(section, tag_section2.string)
|
||||||
|
|
||||||
|
if section not in articles.keys():
|
||||||
|
articles[section] = []
|
||||||
|
if section not in ans:
|
||||||
|
ans.append(section)
|
||||||
|
|
||||||
|
body = tag_title
|
||||||
|
while True:
|
||||||
|
tag_url = body.findNext(name='a', attrs={'href':url_re})
|
||||||
|
if tag_url == None:
|
||||||
|
break
|
||||||
|
body = tag_url
|
||||||
|
if tag_url.string == None:
|
||||||
|
continue
|
||||||
|
elif tag_url.string == 'Full Story':
|
||||||
|
break
|
||||||
|
elif tag_url.string.startswith('Comments ('):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if tag_url == None:
|
||||||
|
break
|
||||||
|
|
||||||
|
article = dict(
|
||||||
|
title=tag_title.string,
|
||||||
|
url=tag_url['href'].split('#')[0],
|
||||||
|
description='', content='', date='')
|
||||||
|
articles[section].append(article)
|
||||||
|
|
||||||
|
ans = [(key, articles[key]) for key in ans if articles.has_key(key)]
|
||||||
|
if not ans:
|
||||||
|
raise Exception('Could not find any articles.')
|
||||||
|
|
||||||
|
return ans
|
||||||
|
|
||||||
|
# vim: expandtab:ts=4:sw=4
|
@ -11,7 +11,6 @@ http://www.macworld.co.uk/
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
|
||||||
|
|
||||||
temp_files = []
|
temp_files = []
|
||||||
articles_are_obfuscated = True
|
articles_are_obfuscated = True
|
||||||
@ -36,26 +35,17 @@ class macWorld(BasicNewsRecipe):
|
|||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
def get_obfuscated_article(self, url):
|
|
||||||
br = self.get_browser()
|
|
||||||
br.open(url+'&print')
|
|
||||||
|
|
||||||
response = br.follow_link(url, nr = 0)
|
|
||||||
html = response.read()
|
|
||||||
|
|
||||||
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
|
|
||||||
self.temp_files[-1].write(html)
|
|
||||||
self.temp_files[-1].close()
|
|
||||||
return self.temp_files[-1].name
|
|
||||||
|
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'id':'article'})
|
dict(name='div', attrs={'id':'content'})
|
||||||
]
|
]
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'class':['toolBar','mac_tags','toolBar btmTools','textAds']}),
|
{'class':['toolBar','mac_tags','toolBar btmTools','textAds']},
|
||||||
dict(name='p', attrs={'class':'breadcrumbs'}),
|
dict(name='p', attrs={'class':'breadcrumbs'}),
|
||||||
dict(name='div', attrs={'id':['breadcrumb','sidebar','comments']})
|
dict(id=['breadcrumb','sidebar','comments','topContentWrapper',
|
||||||
|
'rightColumn', 'aboveFootPromo', 'storyCarousel']),
|
||||||
|
{'class':lambda x: x and ('tools' in x or 'toolBar'
|
||||||
|
in x)}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -69,12 +69,16 @@ class SeattleTimes(BasicNewsRecipe):
|
|||||||
u'http://seattletimes.nwsource.com/rss/mostreadarticles.xml'),
|
u'http://seattletimes.nwsource.com/rss/mostreadarticles.xml'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
keep_only_tags = [dict(id='content')]
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name=['object','link','script'])
|
dict(name=['object','link','script']),
|
||||||
,dict(name='p', attrs={'class':'permission'})
|
{'class':['permission', 'note', 'bottomtools',
|
||||||
|
'homedelivery']},
|
||||||
|
dict(id=["rightcolumn", 'footer', 'adbottom']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
|
return url
|
||||||
start_url, sep, rest_url = url.rpartition('_')
|
start_url, sep, rest_url = url.rpartition('_')
|
||||||
rurl, rsep, article_id = start_url.rpartition('/')
|
rurl, rsep, article_id = start_url.rpartition('/')
|
||||||
return u'http://seattletimes.nwsource.com/cgi-bin/PrintStory.pl?document_id=' + article_id
|
return u'http://seattletimes.nwsource.com/cgi-bin/PrintStory.pl?document_id=' + article_id
|
||||||
|
@ -10,12 +10,14 @@ class AdvancedUserRecipe1278049615(BasicNewsRecipe):
|
|||||||
|
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
feeds = [(u'News', u'http://www.statesman.com/section-rss.do?source=news&includeSubSections=true'),
|
feeds = [(u'News',
|
||||||
(u'Business', u'http://www.statesman.com/section-rss.do?source=business&includeSubSections=true'),
|
u'http://www.statesman.com/section-rss.do?source=news&includeSubSections=true'),
|
||||||
(u'Life', u'http://www.statesman.com/section-rss.do?source=life&includesubsection=true'),
|
(u'Local', u'http://www.statesman.com/section-rss.do?source=local&includeSubSections=true'),
|
||||||
(u'Editorial', u'http://www.statesman.com/section-rss.do?source=opinion&includesubsections=true'),
|
(u'Business', u'http://www.statesman.com/section-rss.do?source=business&includeSubSections=true'),
|
||||||
(u'Sports', u'http://www.statesman.com/section-rss.do?source=sports&includeSubSections=true')
|
(u'Life', u'http://www.statesman.com/section-rss.do?source=life&includesubsection=true'),
|
||||||
]
|
(u'Editorial', u'http://www.statesman.com/section-rss.do?source=opinion&includesubsections=true'),
|
||||||
|
(u'Sports', u'http://www.statesman.com/section-rss.do?source=sports&includeSubSections=true')
|
||||||
|
]
|
||||||
masthead_url = "http://www.statesman.com/images/cmg-logo.gif"
|
masthead_url = "http://www.statesman.com/images/cmg-logo.gif"
|
||||||
#temp_files = []
|
#temp_files = []
|
||||||
#articles_are_obfuscated = True
|
#articles_are_obfuscated = True
|
||||||
@ -28,8 +30,11 @@ class AdvancedUserRecipe1278049615(BasicNewsRecipe):
|
|||||||
conversion_options = {'linearize_tables':True}
|
conversion_options = {'linearize_tables':True}
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'id':'cxArticleOptions'}),
|
dict(name='div', attrs={'id':'cxArticleOptions'}),
|
||||||
|
{'class':['perma', 'comments', 'trail', 'share-buttons',
|
||||||
|
'toggle_show_on']},
|
||||||
]
|
]
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'class':'cxArticleHeader'}),
|
dict(name='div', attrs={'class':'cxArticleHeader'}),
|
||||||
dict(name='div', attrs={'id':'cxArticleBodyText'}),
|
dict(name='div', attrs={'id':['cxArticleBodyText',
|
||||||
|
'content']}),
|
||||||
]
|
]
|
||||||
|
@ -7,6 +7,7 @@ swiatczytnikow.pl
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class swiatczytnikow(BasicNewsRecipe):
|
class swiatczytnikow(BasicNewsRecipe):
|
||||||
title = u'Swiat Czytnikow'
|
title = u'Swiat Czytnikow'
|
||||||
|
@ -265,16 +265,28 @@ class CSSPreProcessor(object):
|
|||||||
|
|
||||||
PAGE_PAT = re.compile(r'@page[^{]*?{[^}]*?}')
|
PAGE_PAT = re.compile(r'@page[^{]*?{[^}]*?}')
|
||||||
# Remove some of the broken CSS Microsoft products
|
# Remove some of the broken CSS Microsoft products
|
||||||
# create, slightly dangerous as it removes to end of line
|
# create
|
||||||
# rather than semi-colon
|
MS_PAT = re.compile(r'''
|
||||||
MS_PAT = re.compile(r'^\s*(mso-|panose-).+?$',
|
(?P<start>^|;|\{)\s* # The end of the previous rule or block start
|
||||||
re.MULTILINE|re.IGNORECASE)
|
(%s).+? # The invalid selectors
|
||||||
|
(?P<end>$|;|\}) # The end of the declaration
|
||||||
|
'''%'mso-|panose-|text-underline|tab-interval',
|
||||||
|
re.MULTILINE|re.IGNORECASE|re.VERBOSE)
|
||||||
|
|
||||||
|
def ms_sub(self, match):
|
||||||
|
end = match.group('end')
|
||||||
|
try:
|
||||||
|
start = match.group('start')
|
||||||
|
except:
|
||||||
|
start = ''
|
||||||
|
if end == ';':
|
||||||
|
end = ''
|
||||||
|
return start + end
|
||||||
|
|
||||||
def __call__(self, data, add_namespace=False):
|
def __call__(self, data, add_namespace=False):
|
||||||
from calibre.ebooks.oeb.base import XHTML_CSS_NAMESPACE
|
from calibre.ebooks.oeb.base import XHTML_CSS_NAMESPACE
|
||||||
data = self.PAGE_PAT.sub('', data)
|
data = self.PAGE_PAT.sub('', data)
|
||||||
if '\n' in data:
|
data = self.MS_PAT.sub(self.ms_sub, data)
|
||||||
data = self.MS_PAT.sub('', data)
|
|
||||||
if not add_namespace:
|
if not add_namespace:
|
||||||
return data
|
return data
|
||||||
ans, namespaced = [], False
|
ans, namespaced = [], False
|
||||||
|
@ -827,6 +827,24 @@ class Manifest(object):
|
|||||||
return None
|
return None
|
||||||
return etree.fromstring(data, parser=RECOVER_PARSER)
|
return etree.fromstring(data, parser=RECOVER_PARSER)
|
||||||
|
|
||||||
|
def clean_word_doc(self, data):
|
||||||
|
prefixes = []
|
||||||
|
for match in re.finditer(r'xmlns:(\S+?)=".*?microsoft.*?"', data):
|
||||||
|
prefixes.append(match.group(1))
|
||||||
|
if prefixes:
|
||||||
|
self.oeb.log.warn('Found microsoft markup, cleaning...')
|
||||||
|
# Remove empty tags as they are not rendered by browsers
|
||||||
|
# but can become renderable HTML tags like <p/> if the
|
||||||
|
# document is parsed by an HTML parser
|
||||||
|
pat = re.compile(
|
||||||
|
r'<(%s):([a-zA-Z0-9]+)[^>/]*?></\1:\2>'%('|'.join(prefixes)),
|
||||||
|
re.DOTALL)
|
||||||
|
data = pat.sub('', data)
|
||||||
|
pat = re.compile(
|
||||||
|
r'<(%s):([a-zA-Z0-9]+)[^>/]*?/>'%('|'.join(prefixes)))
|
||||||
|
data = pat.sub('', data)
|
||||||
|
return data
|
||||||
|
|
||||||
def _parse_xhtml(self, data):
|
def _parse_xhtml(self, data):
|
||||||
self.oeb.log.debug('Parsing', self.href, '...')
|
self.oeb.log.debug('Parsing', self.href, '...')
|
||||||
# Convert to Unicode and normalize line endings
|
# Convert to Unicode and normalize line endings
|
||||||
@ -884,6 +902,10 @@ class Manifest(object):
|
|||||||
except etree.XMLSyntaxError:
|
except etree.XMLSyntaxError:
|
||||||
data = etree.fromstring(data, parser=RECOVER_PARSER)
|
data = etree.fromstring(data, parser=RECOVER_PARSER)
|
||||||
return data
|
return data
|
||||||
|
try:
|
||||||
|
data = self.clean_word_doc(data)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
data = first_pass(data)
|
data = first_pass(data)
|
||||||
|
|
||||||
# Handle weird (non-HTML/fragment) files
|
# Handle weird (non-HTML/fragment) files
|
||||||
@ -907,6 +929,7 @@ class Manifest(object):
|
|||||||
parent.append(child)
|
parent.append(child)
|
||||||
data = nroot
|
data = nroot
|
||||||
|
|
||||||
|
|
||||||
# Force into the XHTML namespace
|
# Force into the XHTML namespace
|
||||||
if not namespace(data.tag):
|
if not namespace(data.tag):
|
||||||
self.oeb.log.warn('Forcing', self.href, 'into XHTML namespace')
|
self.oeb.log.warn('Forcing', self.href, 'into XHTML namespace')
|
||||||
|
@ -423,6 +423,7 @@ class Stylizer(object):
|
|||||||
|
|
||||||
class Style(object):
|
class Style(object):
|
||||||
UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$')
|
UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$')
|
||||||
|
MS_PAT = re.compile(r'^\s*(mso-|panose-|text-underline|tab-interval)')
|
||||||
|
|
||||||
def __init__(self, element, stylizer):
|
def __init__(self, element, stylizer):
|
||||||
self._element = element
|
self._element = element
|
||||||
@ -447,6 +448,8 @@ class Style(object):
|
|||||||
return
|
return
|
||||||
css = attrib['style'].split(';')
|
css = attrib['style'].split(';')
|
||||||
css = filter(None, (x.strip() for x in css))
|
css = filter(None, (x.strip() for x in css))
|
||||||
|
css = [x.strip() for x in css]
|
||||||
|
css = [x for x in css if self.MS_PAT.match(x) is None]
|
||||||
try:
|
try:
|
||||||
style = CSSStyleDeclaration('; '.join(css))
|
style = CSSStyleDeclaration('; '.join(css))
|
||||||
except CSSSyntaxError:
|
except CSSSyntaxError:
|
||||||
|
@ -19,11 +19,11 @@ single_shot = partial(QTimer.singleShot, 10)
|
|||||||
|
|
||||||
class MultiDeleter(QObject):
|
class MultiDeleter(QObject):
|
||||||
|
|
||||||
def __init__(self, gui, rows, callback):
|
def __init__(self, gui, ids, callback):
|
||||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||||
QObject.__init__(self, gui)
|
QObject.__init__(self, gui)
|
||||||
self.model = gui.library_view.model()
|
self.model = gui.library_view.model()
|
||||||
self.ids = list(map(self.model.id, rows))
|
self.ids = ids
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
self.failures = []
|
self.failures = []
|
||||||
self.deleted_ids = []
|
self.deleted_ids = []
|
||||||
@ -231,6 +231,7 @@ class DeleteAction(InterfaceAction):
|
|||||||
return
|
return
|
||||||
# Library view is visible.
|
# Library view is visible.
|
||||||
if self.gui.stack.currentIndex() == 0:
|
if self.gui.stack.currentIndex() == 0:
|
||||||
|
to_delete_ids = [view.model().id(r) for r in rows]
|
||||||
# Ask the user if they want to delete the book from the library or device if it is in both.
|
# Ask the user if they want to delete the book from the library or device if it is in both.
|
||||||
if self.gui.device_manager.is_device_connected:
|
if self.gui.device_manager.is_device_connected:
|
||||||
on_device = False
|
on_device = False
|
||||||
@ -264,10 +265,10 @@ class DeleteAction(InterfaceAction):
|
|||||||
if ci.isValid():
|
if ci.isValid():
|
||||||
row = ci.row()
|
row = ci.row()
|
||||||
if len(rows) < 5:
|
if len(rows) < 5:
|
||||||
ids_deleted = view.model().delete_books(rows)
|
view.model().delete_books_by_id(to_delete_ids)
|
||||||
self.library_ids_deleted(ids_deleted, row)
|
self.library_ids_deleted(to_delete_ids, row)
|
||||||
else:
|
else:
|
||||||
self.__md = MultiDeleter(self.gui, rows,
|
self.__md = MultiDeleter(self.gui, to_delete_ids,
|
||||||
partial(self.library_ids_deleted, current_row=row))
|
partial(self.library_ids_deleted, current_row=row))
|
||||||
# Device view is visible.
|
# Device view is visible.
|
||||||
else:
|
else:
|
||||||
|
@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import textwrap, os, re
|
import textwrap, os, re
|
||||||
|
|
||||||
from PyQt4.Qt import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, \
|
from PyQt4.Qt import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, \
|
||||||
QDialog, QPixmap, QGraphicsScene, QIcon, QSize
|
QDialog, QPixmap, QIcon, QSize
|
||||||
|
|
||||||
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
||||||
from calibre.gui2 import dynamic, open_local_file, open_url
|
from calibre.gui2 import dynamic, open_local_file, open_url
|
||||||
@ -14,12 +14,14 @@ from calibre import fit_image
|
|||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
|
|
||||||
class BookInfo(QDialog, Ui_BookInfo):
|
class BookInfo(QDialog, Ui_BookInfo):
|
||||||
|
|
||||||
def __init__(self, parent, view, row, view_func):
|
def __init__(self, parent, view, row, view_func):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
Ui_BookInfo.__init__(self)
|
Ui_BookInfo.__init__(self)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
self.gui = parent
|
||||||
self.cover_pixmap = None
|
self.cover_pixmap = None
|
||||||
self.comments.sizeHint = self.comments_size_hint
|
self.comments.sizeHint = self.comments_size_hint
|
||||||
self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks)
|
self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks)
|
||||||
@ -38,11 +40,26 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path)
|
self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path)
|
||||||
self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
|
self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
|
||||||
self.cover.resizeEvent = self.cover_view_resized
|
self.cover.resizeEvent = self.cover_view_resized
|
||||||
|
self.cover.cover_changed.connect(self.cover_changed)
|
||||||
|
|
||||||
desktop = QCoreApplication.instance().desktop()
|
desktop = QCoreApplication.instance().desktop()
|
||||||
screen_height = desktop.availableGeometry().height() - 100
|
screen_height = desktop.availableGeometry().height() - 100
|
||||||
self.resize(self.size().width(), screen_height)
|
self.resize(self.size().width(), screen_height)
|
||||||
|
|
||||||
|
def cover_changed(self, data):
|
||||||
|
if self.current_row is not None:
|
||||||
|
id_ = self.view.model().id(self.current_row)
|
||||||
|
self.view.model().db.set_cover(id_, data)
|
||||||
|
if self.gui.cover_flow:
|
||||||
|
self.gui.cover_flow.dataChanged()
|
||||||
|
ci = self.view.currentIndex()
|
||||||
|
if ci.isValid():
|
||||||
|
self.view.model().current_changed(ci, ci)
|
||||||
|
self.cover_pixmap = QPixmap()
|
||||||
|
self.cover_pixmap.loadFromData(data)
|
||||||
|
if self.fit_cover.isChecked():
|
||||||
|
self.resize_cover()
|
||||||
|
|
||||||
def link_clicked(self, url):
|
def link_clicked(self, url):
|
||||||
open_url(url)
|
open_url(url)
|
||||||
|
|
||||||
@ -83,7 +100,6 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
if self.cover_pixmap is None:
|
if self.cover_pixmap is None:
|
||||||
return
|
return
|
||||||
self.setWindowIcon(QIcon(self.cover_pixmap))
|
self.setWindowIcon(QIcon(self.cover_pixmap))
|
||||||
self.scene = QGraphicsScene()
|
|
||||||
pixmap = self.cover_pixmap
|
pixmap = self.cover_pixmap
|
||||||
if self.fit_cover.isChecked():
|
if self.fit_cover.isChecked():
|
||||||
scaled, new_width, new_height = fit_image(pixmap.width(),
|
scaled, new_width, new_height = fit_image(pixmap.width(),
|
||||||
@ -92,8 +108,7 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
if scaled:
|
if scaled:
|
||||||
pixmap = pixmap.scaled(new_width, new_height,
|
pixmap = pixmap.scaled(new_width, new_height,
|
||||||
Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
self.scene.addPixmap(pixmap)
|
self.cover.set_pixmap(pixmap)
|
||||||
self.cover.setScene(self.scene)
|
|
||||||
|
|
||||||
def refresh(self, row):
|
def refresh(self, row):
|
||||||
if isinstance(row, QModelIndex):
|
if isinstance(row, QModelIndex):
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QGraphicsView" name="cover"/>
|
<widget class="CoverView" name="cover"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="1" column="1">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
@ -115,6 +115,11 @@
|
|||||||
<extends>QWidget</extends>
|
<extends>QWidget</extends>
|
||||||
<header>QtWebKit/QWebView</header>
|
<header>QtWebKit/QWebView</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>CoverView</class>
|
||||||
|
<extends>QGraphicsView</extends>
|
||||||
|
<header>calibre/gui2/widgets.h</header>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../../resources/images.qrc"/>
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
@ -141,6 +141,15 @@ class Stack(QStackedWidget): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
class UpdateLabel(QLabel): # {{{
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
QLabel.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, e):
|
||||||
|
pass
|
||||||
|
# }}}
|
||||||
|
|
||||||
class StatusBar(QStatusBar): # {{{
|
class StatusBar(QStatusBar): # {{{
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -148,7 +157,7 @@ class StatusBar(QStatusBar): # {{{
|
|||||||
self.default_message = __appname__ + ' ' + _('version') + ' ' + \
|
self.default_message = __appname__ + ' ' + _('version') + ' ' + \
|
||||||
self.get_version() + ' ' + _('created by Kovid Goyal')
|
self.get_version() + ' ' + _('created by Kovid Goyal')
|
||||||
self.device_string = ''
|
self.device_string = ''
|
||||||
self.update_label = QLabel('')
|
self.update_label = UpdateLabel('')
|
||||||
self.addPermanentWidget(self.update_label)
|
self.addPermanentWidget(self.update_label)
|
||||||
self.update_label.setVisible(False)
|
self.update_label.setVisible(False)
|
||||||
self._font = QFont()
|
self._font = QFont()
|
||||||
|
@ -11,9 +11,9 @@ from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
|
|||||||
QPixmap, QSplitterHandle, QToolButton, \
|
QPixmap, QSplitterHandle, QToolButton, \
|
||||||
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, \
|
||||||
QRegExp, QSettings, QSize, QSplitter, \
|
QRegExp, QSettings, QSize, QSplitter, \
|
||||||
QPainter, QLineEdit, QComboBox, QPen, \
|
QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, \
|
||||||
QMenu, QStringListModel, QCompleter, QStringList, \
|
QMenu, QStringListModel, QCompleter, QStringList, \
|
||||||
QTimer, QRect, QFontDatabase
|
QTimer, QRect, QFontDatabase, QGraphicsView
|
||||||
|
|
||||||
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
|
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
|
||||||
from calibre.gui2.filename_pattern_ui import Ui_Form
|
from calibre.gui2.filename_pattern_ui import Ui_Form
|
||||||
@ -181,22 +181,16 @@ class FormatList(QListWidget):
|
|||||||
else:
|
else:
|
||||||
return QListWidget.keyPressEvent(self, event)
|
return QListWidget.keyPressEvent(self, event)
|
||||||
|
|
||||||
|
class ImageDropMixin(object): # {{{
|
||||||
class ImageView(QWidget):
|
'''
|
||||||
|
Adds support for dropping images onto widgets and a contect menu for
|
||||||
BORDER_WIDTH = 1
|
copy/pasting images.
|
||||||
cover_changed = pyqtSignal(object)
|
'''
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
QWidget.__init__(self, parent)
|
|
||||||
self._pixmap = QPixmap(self)
|
|
||||||
self.setMinimumSize(QSize(150, 200))
|
|
||||||
self.setAcceptDrops(True)
|
|
||||||
self.draw_border = True
|
|
||||||
|
|
||||||
# Drag 'n drop {{{
|
|
||||||
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS
|
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def paths_from_event(cls, event):
|
def paths_from_event(cls, event):
|
||||||
'''
|
'''
|
||||||
@ -223,14 +217,58 @@ class ImageView(QWidget):
|
|||||||
pmap = QPixmap()
|
pmap = QPixmap()
|
||||||
pmap.load(path)
|
pmap.load(path)
|
||||||
if not pmap.isNull():
|
if not pmap.isNull():
|
||||||
self.setPixmap(pmap)
|
self.handle_image_drop(path, pmap)
|
||||||
event.accept()
|
event.accept()
|
||||||
self.cover_changed.emit(open(path, 'rb').read())
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def handle_image_drop(self, path, pmap):
|
||||||
|
self.set_pixmap(pmap)
|
||||||
|
self.cover_changed.emit(open(path, 'rb').read())
|
||||||
|
|
||||||
def dragMoveEvent(self, event):
|
def dragMoveEvent(self, event):
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
# }}}
|
|
||||||
|
def get_pixmap(self):
|
||||||
|
return self.pixmap()
|
||||||
|
|
||||||
|
def set_pixmap(self, pmap):
|
||||||
|
self.setPixmap(pmap)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, ev):
|
||||||
|
cm = QMenu(self)
|
||||||
|
copy = cm.addAction(_('Copy Image'))
|
||||||
|
paste = cm.addAction(_('Paste Image'))
|
||||||
|
if not QApplication.instance().clipboard().mimeData().hasImage():
|
||||||
|
paste.setEnabled(False)
|
||||||
|
copy.triggered.connect(self.copy_to_clipboard)
|
||||||
|
paste.triggered.connect(self.paste_from_clipboard)
|
||||||
|
cm.exec_(ev.globalPos())
|
||||||
|
|
||||||
|
def copy_to_clipboard(self):
|
||||||
|
QApplication.instance().clipboard().setPixmap(self.get_pixmap())
|
||||||
|
|
||||||
|
def paste_from_clipboard(self):
|
||||||
|
cb = QApplication.instance().clipboard()
|
||||||
|
pmap = cb.pixmap()
|
||||||
|
if pmap.isNull() and cb.supportsSelection():
|
||||||
|
pmap = cb.pixmap(cb.Selection)
|
||||||
|
if not pmap.isNull():
|
||||||
|
self.set_pixmap(pmap)
|
||||||
|
self.cover_changed.emit(
|
||||||
|
pixmap_to_data(pmap))
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class ImageView(QWidget, ImageDropMixin):
|
||||||
|
|
||||||
|
BORDER_WIDTH = 1
|
||||||
|
cover_changed = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self._pixmap = QPixmap(self)
|
||||||
|
self.setMinimumSize(QSize(150, 200))
|
||||||
|
ImageDropMixin.__init__(self)
|
||||||
|
self.draw_border = True
|
||||||
|
|
||||||
def setPixmap(self, pixmap):
|
def setPixmap(self, pixmap):
|
||||||
if not isinstance(pixmap, QPixmap):
|
if not isinstance(pixmap, QPixmap):
|
||||||
@ -272,32 +310,23 @@ class ImageView(QWidget):
|
|||||||
p.drawRect(target)
|
p.drawRect(target)
|
||||||
p.end()
|
p.end()
|
||||||
|
|
||||||
|
class CoverView(QGraphicsView, ImageDropMixin):
|
||||||
|
|
||||||
# Clipboard copy/paste # {{{
|
cover_changed = pyqtSignal(object)
|
||||||
def contextMenuEvent(self, ev):
|
|
||||||
cm = QMenu(self)
|
|
||||||
copy = cm.addAction(_('Copy Image'))
|
|
||||||
paste = cm.addAction(_('Paste Image'))
|
|
||||||
if not QApplication.instance().clipboard().mimeData().hasImage():
|
|
||||||
paste.setEnabled(False)
|
|
||||||
copy.triggered.connect(self.copy_to_clipboard)
|
|
||||||
paste.triggered.connect(self.paste_from_clipboard)
|
|
||||||
cm.exec_(ev.globalPos())
|
|
||||||
|
|
||||||
def copy_to_clipboard(self):
|
def __init__(self, *args, **kwargs):
|
||||||
QApplication.instance().clipboard().setPixmap(self.pixmap())
|
QGraphicsView.__init__(self, *args, **kwargs)
|
||||||
|
ImageDropMixin.__init__(self)
|
||||||
|
|
||||||
def paste_from_clipboard(self):
|
def get_pixmap(self):
|
||||||
cb = QApplication.instance().clipboard()
|
for item in self.scene().items():
|
||||||
pmap = cb.pixmap()
|
if hasattr(item, 'pixmap'):
|
||||||
if pmap.isNull() and cb.supportsSelection():
|
return item.pixmap()
|
||||||
pmap = cb.pixmap(cb.Selection)
|
|
||||||
if not pmap.isNull():
|
|
||||||
self.setPixmap(pmap)
|
|
||||||
self.cover_changed.emit(
|
|
||||||
pixmap_to_data(pmap))
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
|
def set_pixmap(self, pmap):
|
||||||
|
self.scene = QGraphicsScene()
|
||||||
|
self.scene.addPixmap(pmap)
|
||||||
|
self.setScene(self.scene)
|
||||||
|
|
||||||
class FontFamilyModel(QAbstractListModel):
|
class FontFamilyModel(QAbstractListModel):
|
||||||
|
|
||||||
|
@ -217,16 +217,16 @@ The following functions are available in addition to those described in single-f
|
|||||||
* ``field(name)`` -- returns the metadata field named by ``name``.
|
* ``field(name)`` -- returns the metadata field named by ``name``.
|
||||||
* ``format_date(x, date_format)`` -- format_date(val, format_string) -- format the value, which must be a date field, using the format_string, returning a string. The formatting codes are::
|
* ``format_date(x, date_format)`` -- format_date(val, format_string) -- format the value, which must be a date field, using the format_string, returning a string. The formatting codes are::
|
||||||
|
|
||||||
d : the day as number without a leading zero (1 to 31)
|
d : the day as number without a leading zero (1 to 31)
|
||||||
dd : the day as number with a leading zero (01 to 31) '
|
dd : the day as number with a leading zero (01 to 31) '
|
||||||
ddd : the abbreviated localized day name (e.g. "Mon" to "Sun"). '
|
ddd : the abbreviated localized day name (e.g. "Mon" to "Sun"). '
|
||||||
dddd : the long localized day name (e.g. "Monday" to "Sunday"). '
|
dddd : the long localized day name (e.g. "Monday" to "Sunday"). '
|
||||||
M : the month as number without a leading zero (1 to 12). '
|
M : the month as number without a leading zero (1 to 12). '
|
||||||
MM : the month as number with a leading zero (01 to 12) '
|
MM : the month as number with a leading zero (01 to 12) '
|
||||||
MMM : the abbreviated localized month name (e.g. "Jan" to "Dec"). '
|
MMM : the abbreviated localized month name (e.g. "Jan" to "Dec"). '
|
||||||
MMMM : the long localized month name (e.g. "January" to "December"). '
|
MMMM : the long localized month name (e.g. "January" to "December"). '
|
||||||
yy : the year as two digit number (00 to 99). '
|
yy : the year as two digit number (00 to 99). '
|
||||||
yyyy : the year as four digit number.'
|
yyyy : the year as four digit number.'
|
||||||
|
|
||||||
* ``eval(string)`` -- evaluates the string as a program, passing the local variables (those ``assign`` ed to). This permits using the template processor to construct complex results from local variables.
|
* ``eval(string)`` -- evaluates the string as a program, passing the local variables (those ``assign`` ed to). This permits using the template processor to construct complex results from local variables.
|
||||||
* ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers.
|
* ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user