mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Merge
This commit is contained in:
commit
9c91892d04
BIN
resources/images/news/latimes.png
Normal file
BIN
resources/images/news/latimes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 358 B |
@ -585,7 +585,6 @@ application/vnd.osa.netdeploy
|
|||||||
application/vnd.osgi.bundle
|
application/vnd.osgi.bundle
|
||||||
application/vnd.osgi.dp dp
|
application/vnd.osgi.dp dp
|
||||||
application/vnd.otps.ct-kip+xml
|
application/vnd.otps.ct-kip+xml
|
||||||
application/vnd.palm oprc pdb pqa
|
|
||||||
application/vnd.paos.xml
|
application/vnd.paos.xml
|
||||||
application/vnd.pg.format str
|
application/vnd.pg.format str
|
||||||
application/vnd.pg.osasli ei6
|
application/vnd.pg.osasli ei6
|
||||||
@ -1082,7 +1081,6 @@ chemical/x-ncbi-asn1 asn
|
|||||||
chemical/x-ncbi-asn1-ascii ent prt
|
chemical/x-ncbi-asn1-ascii ent prt
|
||||||
chemical/x-ncbi-asn1-binary aso val
|
chemical/x-ncbi-asn1-binary aso val
|
||||||
chemical/x-ncbi-asn1-spec asn
|
chemical/x-ncbi-asn1-spec asn
|
||||||
chemical/x-pdb ent pdb
|
|
||||||
chemical/x-rosdal ros
|
chemical/x-rosdal ros
|
||||||
chemical/x-swissprot sw
|
chemical/x-swissprot sw
|
||||||
chemical/x-vamas-iso14976 vms
|
chemical/x-vamas-iso14976 vms
|
||||||
@ -1379,3 +1377,5 @@ application/x-cbr cbr
|
|||||||
application/x-cb7 cb7
|
application/x-cb7 cb7
|
||||||
application/x-koboreader-ebook kobo
|
application/x-koboreader-ebook kobo
|
||||||
image/wmf wmf
|
image/wmf wmf
|
||||||
|
application/ereader pdb
|
||||||
|
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
# -*- coding: utf-8
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__author__ = 'Luis Hernandez'
|
__author__ = 'Luis Hernandez'
|
||||||
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
||||||
description = 'Periódico gratuito en español - v0.8 - 27 Jan 2011'
|
__version__ = 'v0.85'
|
||||||
|
__date__ = '31 January 2011'
|
||||||
|
|
||||||
'''
|
'''
|
||||||
www.20minutos.es
|
www.20minutos.es
|
||||||
'''
|
'''
|
||||||
|
import re
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||||
|
|
||||||
title = u'20 Minutos'
|
title = u'20 Minutos new'
|
||||||
publisher = u'Grupo 20 Minutos'
|
publisher = u'Grupo 20 Minutos'
|
||||||
|
|
||||||
__author__ = 'Luis Hernández'
|
__author__ = 'Luis Hernandez'
|
||||||
description = 'Periódico gratuito en español'
|
description = 'Free spanish newspaper'
|
||||||
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif'
|
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif'
|
||||||
|
|
||||||
oldest_article = 5
|
oldest_article = 2
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
@ -29,6 +29,7 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
encoding = 'ISO-8859-1'
|
encoding = 'ISO-8859-1'
|
||||||
language = 'es'
|
language = 'es'
|
||||||
timefmt = '[%a, %d %b, %Y]'
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'id':['content','vinetas',]})
|
dict(name='div', attrs={'id':['content','vinetas',]})
|
||||||
@ -43,13 +44,21 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='ol', attrs={'class':['navigation',]})
|
dict(name='ol', attrs={'class':['navigation',]})
|
||||||
,dict(name='span', attrs={'class':['action']})
|
,dict(name='span', attrs={'class':['action']})
|
||||||
,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col','photo-gallery','calendario','article-comment','postto estirar','otras_vinetas estirar','kment','user-actions']})
|
,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col','photo-gallery','photo-gallery side-art-block','calendario','article-comment','postto estirar','otras_vinetas estirar','kment','user-actions']})
|
||||||
,dict(name='div', attrs={'id':['twitter-destacados','eco-tabs','inner','vineta_calendario','vinetistas clearfix','otras_vinetas estirar','MIN1','main','SUP1','INT']})
|
,dict(name='div', attrs={'id':['twitter-destacados','eco-tabs','inner','vineta_calendario','vinetistas clearfix','otras_vinetas estirar','MIN1','main','SUP1','INT']})
|
||||||
,dict(name='ul', attrs={'class':['article-user-actions','stripped-list']})
|
,dict(name='ul', attrs={'class':['article-user-actions','stripped-list']})
|
||||||
,dict(name='ul', attrs={'id':['site-links']})
|
,dict(name='ul', attrs={'id':['site-links']})
|
||||||
,dict(name='li', attrs={'class':['puntuacion','enviar','compartir']})
|
,dict(name='li', attrs={'class':['puntuacion','enviar','compartir']})
|
||||||
]
|
]
|
||||||
|
|
||||||
|
extra_css = """
|
||||||
|
p{text-align: justify; font-size: 100%}
|
||||||
|
body{ text-align: left; font-size:100% }
|
||||||
|
h3{font-family: sans-serif; font-size:150%; font-weight:bold; text-align: justify; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
preprocess_regexps = [(re.compile(r'<a href="http://estaticos.*?[0-999]px;" target="_blank">', re.DOTALL), lambda m: '')]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Portada' , u'http://www.20minutos.es/rss/')
|
(u'Portada' , u'http://www.20minutos.es/rss/')
|
||||||
,(u'Nacional' , u'http://www.20minutos.es/rss/nacional/')
|
,(u'Nacional' , u'http://www.20minutos.es/rss/nacional/')
|
||||||
@ -65,6 +74,6 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/')
|
,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/')
|
||||||
,(u'Cine' , u'http://www.20minutos.es/rss/cine/')
|
,(u'Cine' , u'http://www.20minutos.es/rss/cine/')
|
||||||
,(u'Musica' , u'http://www.20minutos.es/rss/musica/')
|
,(u'Musica' , u'http://www.20minutos.es/rss/musica/')
|
||||||
,(u'Vinetas' , u'http://www.20minutos.es/rss/vinetas/')
|
,(u'Vinetas' , u'http://www.20minutos.es/rss/vinetas/')
|
||||||
,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/')
|
,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/')
|
||||||
]
|
]
|
||||||
|
71
resources/recipes/cinco_dias.recipe
Normal file
71
resources/recipes/cinco_dias.recipe
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'Luis Hernandez'
|
||||||
|
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
||||||
|
__version__ = 'v1.2'
|
||||||
|
__date__ = '31 January 2011'
|
||||||
|
|
||||||
|
'''
|
||||||
|
http://www.cincodias.com/
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'Cinco Dias'
|
||||||
|
publisher = u'Grupo Prisa'
|
||||||
|
|
||||||
|
__author__ = 'Luis Hernandez'
|
||||||
|
description = 'spanish web about money and bussiness, free edition'
|
||||||
|
|
||||||
|
cover_url = 'http://www.prisa.com/images/logos/logo_cinco_dias.gif'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
|
||||||
|
language = 'es'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
encoding = 'ISO-8859-1'
|
||||||
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':['cab_articulo cab_noticia','pos_3','txt_noticia','mod_despiece']})
|
||||||
|
,dict(name='p', attrs={'class':['cintillo']})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='div' , attrs={'class':['publi_h']})
|
||||||
|
remove_tags_after = dict(name='div' , attrs={'class':['tab_util util_estadisticas']})
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':['util-1','util-2','util-3','inner estirar','inner1','inner2','inner3','cont','tab_util util_estadisticas','tab_util util_enviar','mod_list_inf','mod_similares','mod_divisas','mod_sectores','mod_termometro','mod post','mod_img','mod_txt','nivel estirar','barra estirar','info_brujula btnBrujula','utilidad_brujula estirar']})
|
||||||
|
,dict(name='li', attrs={'class':['lnk-fcbook','lnk-retweet','lnk-meneame','desplegable','comentarios','list-options','estirar']})
|
||||||
|
,dict(name='ul', attrs={'class':['lista-izquierda','list-options','estirar']})
|
||||||
|
,dict(name='p', attrs={'class':['autor']})
|
||||||
|
]
|
||||||
|
|
||||||
|
extra_css = """
|
||||||
|
p{text-align: justify; font-size: 100%}
|
||||||
|
body{ text-align: left; font-size:100% }
|
||||||
|
h1{font-family: sans-serif; font-size:150%; font-weight:bold; text-align: justify; }
|
||||||
|
h3{font-family: sans-serif; font-size:100%; font-style: italic; text-align: justify; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Ultima Hora' , u'http://www.cincodias.com/rss/feed.html?feedId=17029')
|
||||||
|
,(u'Empresas' , u'http://www.cincodias.com/rss/feed.html?feedId=19')
|
||||||
|
,(u'Mercados' , u'http://www.cincodias.com/rss/feed.html?feedId=20')
|
||||||
|
,(u'Economia' , u'http://www.cincodias.com/rss/feed.html?feedId=21')
|
||||||
|
,(u'Tecnorama' , u'http://www.cincodias.com/rss/feed.html?feedId=17230')
|
||||||
|
,(u'Tecnologia' , u'http://www.cincodias.com/rss/feed.html?feedId=17106')
|
||||||
|
,(u'Finanzas Personales' , u'http://www.cincodias.com/rss/feed.html?feedId=22')
|
||||||
|
,(u'Fiscalidad' , u'http://www.cincodias.com/rss/feed.html?feedId=17107')
|
||||||
|
,(u'Vivienda' , u'http://www.cincodias.com/rss/feed.html?feedId=17108')
|
||||||
|
,(u'Tendencias' , u'http://www.cincodias.com/rss/feed.html?feedId=17109')
|
||||||
|
,(u'Empleo' , u'http://www.cincodias.com/rss/feed.html?feedId=17110')
|
||||||
|
,(u'IBEX 35' , u'http://www.cincodias.com/rss/feed.html?feedId=17125')
|
||||||
|
,(u'Sectores' , u'http://www.cincodias.com/rss/feed.html?feedId=17126')
|
||||||
|
,(u'Opinion' , u'http://www.cincodias.com/rss/feed.html?feedId=17105')
|
||||||
|
]
|
@ -15,12 +15,26 @@ class LeTemps(BasicNewsRecipe):
|
|||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
__author__ = 'Sujata Raman'
|
__author__ = 'Sujata Raman'
|
||||||
|
description = 'French news. Needs a subscription from http://www.letemps.ch'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
recursions = 1
|
recursions = 1
|
||||||
encoding = 'UTF-8'
|
encoding = 'UTF-8'
|
||||||
match_regexps = [r'http://www.letemps.ch/Page/Uuid/[-0-9a-f]+\|[1-9]']
|
match_regexps = [r'http://www.letemps.ch/Page/Uuid/[-0-9a-f]+\|[1-9]']
|
||||||
language = 'fr'
|
language = 'fr'
|
||||||
|
needs_subscription = True
|
||||||
|
|
||||||
|
def get_browser(self):
|
||||||
|
br = BasicNewsRecipe.get_browser(self)
|
||||||
|
br.open('http://www.letemps.ch/login')
|
||||||
|
br['username'] = self.username
|
||||||
|
br['password'] = self.password
|
||||||
|
raw = br.submit().read()
|
||||||
|
if '>Login' in raw:
|
||||||
|
raise ValueError('Failed to login to letemp.ch. Check '
|
||||||
|
'your username and password')
|
||||||
|
return br
|
||||||
|
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':'content'}),
|
keep_only_tags = [dict(name='div', attrs={'id':'content'}),
|
||||||
dict(name='div', attrs={'class':'story'})
|
dict(name='div', attrs={'class':'story'})
|
||||||
|
@ -35,7 +35,7 @@ class WallStreetJournal(BasicNewsRecipe):
|
|||||||
|
|
||||||
remove_tags_before = dict(name='h1')
|
remove_tags_before = dict(name='h1')
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]),
|
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow","articleTabs_tab_quotes","articleTabs_tab_document"]),
|
||||||
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
|
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
|
||||||
dict(rel='shortcut icon'),
|
dict(rel='shortcut icon'),
|
||||||
]
|
]
|
||||||
@ -101,7 +101,7 @@ class WallStreetJournal(BasicNewsRecipe):
|
|||||||
title = 'Front Section'
|
title = 'Front Section'
|
||||||
url = 'http://online.wsj.com' + a['href']
|
url = 'http://online.wsj.com' + a['href']
|
||||||
feeds = self.wsj_add_feed(feeds,title,url)
|
feeds = self.wsj_add_feed(feeds,title,url)
|
||||||
title = 'What''s News'
|
title = "What's News"
|
||||||
url = url.replace('pageone','whatsnews')
|
url = url.replace('pageone','whatsnews')
|
||||||
feeds = self.wsj_add_feed(feeds,title,url)
|
feeds = self.wsj_add_feed(feeds,title,url)
|
||||||
else:
|
else:
|
||||||
|
@ -10,7 +10,10 @@ class WallStreetJournal(BasicNewsRecipe):
|
|||||||
|
|
||||||
title = 'Wall Street Journal (free)'
|
title = 'Wall Street Journal (free)'
|
||||||
__author__ = 'Kovid Goyal, Sujata Raman, Joshua Oster-Morris, Starson17'
|
__author__ = 'Kovid Goyal, Sujata Raman, Joshua Oster-Morris, Starson17'
|
||||||
description = 'News and current affairs'
|
description = '''News and current affairs. This recipe only fetches complete
|
||||||
|
versions of the articles that are available free on the wsj.com website.
|
||||||
|
To get the rest of the articles, subscribe to the WSJ and use the other WSJ
|
||||||
|
recipe.'''
|
||||||
language = 'en'
|
language = 'en'
|
||||||
cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG'
|
cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG'
|
||||||
max_articles_per_feed = 1000
|
max_articles_per_feed = 1000
|
||||||
@ -151,6 +154,4 @@ class WallStreetJournal(BasicNewsRecipe):
|
|||||||
|
|
||||||
return articles
|
return articles
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
|
|
||||||
|
|
||||||
|
@ -22,13 +22,15 @@ Run an embedded python interpreter.
|
|||||||
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
|
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
|
||||||
help='Debug the specified device driver.')
|
help='Debug the specified device driver.')
|
||||||
parser.add_option('-g', '--gui', default=False, action='store_true',
|
parser.add_option('-g', '--gui', default=False, action='store_true',
|
||||||
help='Run the GUI',)
|
help='Run the GUI with debugging enabled. Debug output is '
|
||||||
|
'printed to stdout and stderr.')
|
||||||
parser.add_option('--gui-debug', default=None,
|
parser.add_option('--gui-debug', default=None,
|
||||||
help='Run the GUI with a debug console, logging to the'
|
help='Run the GUI with a debug console, logging to the'
|
||||||
' specified path',)
|
' specified path. For internal use only, use the -g'
|
||||||
|
' option to run the GUI in debug mode',)
|
||||||
parser.add_option('--show-gui-debug', default=None,
|
parser.add_option('--show-gui-debug', default=None,
|
||||||
help='Display the specified log file.',)
|
help='Display the specified log file. For internal use'
|
||||||
|
' only.',)
|
||||||
parser.add_option('-w', '--viewer', default=False, action='store_true',
|
parser.add_option('-w', '--viewer', default=False, action='store_true',
|
||||||
help='Run the ebook viewer',)
|
help='Run the ebook viewer',)
|
||||||
parser.add_option('--paths', default=False, action='store_true',
|
parser.add_option('--paths', default=False, action='store_true',
|
||||||
|
@ -35,6 +35,16 @@ class DevicePlugin(Plugin):
|
|||||||
|
|
||||||
#: Height for thumbnails on the device
|
#: Height for thumbnails on the device
|
||||||
THUMBNAIL_HEIGHT = 68
|
THUMBNAIL_HEIGHT = 68
|
||||||
|
#: Width for thumbnails on the device. Setting this will force thumbnails
|
||||||
|
#: to this size, not preserving aspect ratio. If it is not set, then
|
||||||
|
#: the aspect ratio will be preserved and the thumbnail will be no higher
|
||||||
|
#: than THUMBNAIL_HEIGHT
|
||||||
|
# THUMBNAIL_WIDTH = 68
|
||||||
|
|
||||||
|
#: Set this to True if the device supports updating cover thumbnails during
|
||||||
|
#: sync_booklists. Setting it to true will ask device.py to refresh the
|
||||||
|
#: cover thumbnails during book matching
|
||||||
|
WANTS_UPDATED_THUMBNAILS = False
|
||||||
|
|
||||||
#: Whether the metadata on books can be set via the GUI.
|
#: Whether the metadata on books can be set via the GUI.
|
||||||
CAN_SET_METADATA = ['title', 'authors', 'collections']
|
CAN_SET_METADATA = ['title', 'authors', 'collections']
|
||||||
|
@ -8,5 +8,5 @@ CACHE_XML = 'Sony Reader/database/cache.xml'
|
|||||||
CACHE_EXT = 'Sony Reader/database/cacheExt.xml'
|
CACHE_EXT = 'Sony Reader/database/cacheExt.xml'
|
||||||
|
|
||||||
MEDIA_THUMBNAIL = 'database/thumbnail'
|
MEDIA_THUMBNAIL = 'database/thumbnail'
|
||||||
CACHE_THUMBNAIL = 'Sony Reader/database/thumbnail'
|
CACHE_THUMBNAIL = 'Sony Reader/thumbnail'
|
||||||
|
|
||||||
|
@ -81,12 +81,19 @@ class PRS505(USBMS):
|
|||||||
_('Set this option to have separate book covers uploaded '
|
_('Set this option to have separate book covers uploaded '
|
||||||
'every time you connect your device. Unset this option if '
|
'every time you connect your device. Unset this option if '
|
||||||
'you have so many books on the reader that performance is '
|
'you have so many books on the reader that performance is '
|
||||||
'unacceptable.')
|
'unacceptable.'),
|
||||||
|
_('Preserve cover aspect ratio when building thumbnails') +
|
||||||
|
':::' +
|
||||||
|
_('Set this option if you want the cover thumbnails to have '
|
||||||
|
'the same aspect ratio (width to height) as the cover. '
|
||||||
|
'Unset it if you want the thumbnail to be the maximum size, '
|
||||||
|
'ignoring aspect ratio.')
|
||||||
]
|
]
|
||||||
EXTRA_CUSTOMIZATION_DEFAULT = [
|
EXTRA_CUSTOMIZATION_DEFAULT = [
|
||||||
', '.join(['series', 'tags']),
|
', '.join(['series', 'tags']),
|
||||||
False,
|
False,
|
||||||
False
|
False,
|
||||||
|
True
|
||||||
]
|
]
|
||||||
|
|
||||||
OPT_COLLECTIONS = 0
|
OPT_COLLECTIONS = 0
|
||||||
@ -96,7 +103,7 @@ class PRS505(USBMS):
|
|||||||
plugboard = None
|
plugboard = None
|
||||||
plugboard_func = None
|
plugboard_func = None
|
||||||
|
|
||||||
THUMBNAIL_HEIGHT = 200
|
THUMBNAIL_HEIGHT = 217
|
||||||
|
|
||||||
MAX_PATH_LEN = 201 # 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) +
|
MAX_PATH_LEN = 201 # 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) +
|
||||||
# len('main_thumbnail.jpg') + 1)
|
# len('main_thumbnail.jpg') + 1)
|
||||||
@ -138,6 +145,13 @@ class PRS505(USBMS):
|
|||||||
if not write_cache(self._card_b_prefix):
|
if not write_cache(self._card_b_prefix):
|
||||||
self._card_b_prefix = None
|
self._card_b_prefix = None
|
||||||
self.booklist_class.rebuild_collections = self.rebuild_collections
|
self.booklist_class.rebuild_collections = self.rebuild_collections
|
||||||
|
# Set the thumbnail width to the theoretical max if the user has asked
|
||||||
|
# that we do not preserve aspect ratio
|
||||||
|
if not self.settings().extra_customization[3]:
|
||||||
|
self.THUMBNAIL_WIDTH = 168
|
||||||
|
# Set WANTS_UPDATED_THUMBNAILS if the user has asked that thumbnails be
|
||||||
|
# updated on every connect
|
||||||
|
self.WANTS_UPDATED_THUMBNAILS = self.settings().extra_customization[2]
|
||||||
|
|
||||||
def get_device_information(self, end_session=True):
|
def get_device_information(self, end_session=True):
|
||||||
return (self.gui_name, '', '', '')
|
return (self.gui_name, '', '', '')
|
||||||
|
@ -139,6 +139,13 @@ class CHMReader(CHMFile):
|
|||||||
if self.hhc_path not in files and files:
|
if self.hhc_path not in files and files:
|
||||||
self.hhc_path = files[0]
|
self.hhc_path = files[0]
|
||||||
|
|
||||||
|
if self.hhc_path == '.hhc' and self.hhc_path not in files:
|
||||||
|
from calibre import walk
|
||||||
|
for x in walk(output_dir):
|
||||||
|
if os.path.basename(x).lower() in ('index.htm', 'index.html'):
|
||||||
|
self.hhc_path = os.path.relpath(x, output_dir)
|
||||||
|
break
|
||||||
|
|
||||||
def _reformat(self, data, htmlpath):
|
def _reformat(self, data, htmlpath):
|
||||||
try:
|
try:
|
||||||
data = xml_to_unicode(data, strip_encoding_pats=True)[0]
|
data = xml_to_unicode(data, strip_encoding_pats=True)[0]
|
||||||
|
@ -46,7 +46,8 @@ HEURISTIC_OPTIONS = ['markup_chapter_headings',
|
|||||||
'italicize_common_cases', 'fix_indents',
|
'italicize_common_cases', 'fix_indents',
|
||||||
'html_unwrap_factor', 'unwrap_lines',
|
'html_unwrap_factor', 'unwrap_lines',
|
||||||
'delete_blank_paragraphs', 'format_scene_breaks',
|
'delete_blank_paragraphs', 'format_scene_breaks',
|
||||||
'dehyphenate', 'renumber_headings']
|
'dehyphenate', 'renumber_headings',
|
||||||
|
'replace_scene_breaks']
|
||||||
|
|
||||||
def print_help(parser, log):
|
def print_help(parser, log):
|
||||||
help = parser.format_help().encode(preferred_encoding, 'replace')
|
help = parser.format_help().encode(preferred_encoding, 'replace')
|
||||||
|
@ -531,6 +531,11 @@ OptionRecommendation(name='format_scene_breaks',
|
|||||||
'Replace soft scene breaks that use multiple blank lines with'
|
'Replace soft scene breaks that use multiple blank lines with'
|
||||||
'horizontal rules.')),
|
'horizontal rules.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='replace_scene_breaks',
|
||||||
|
recommended_value='', level=OptionRecommendation.LOW,
|
||||||
|
help=_('Replace scene breaks with the specified text. By default, the '
|
||||||
|
'text from the input document is used.')),
|
||||||
|
|
||||||
OptionRecommendation(name='dehyphenate',
|
OptionRecommendation(name='dehyphenate',
|
||||||
recommended_value=True, level=OptionRecommendation.LOW,
|
recommended_value=True, level=OptionRecommendation.LOW,
|
||||||
help=_('Analyze hyphenated words throughout the document. The '
|
help=_('Analyze hyphenated words throughout the document. The '
|
||||||
|
@ -26,9 +26,14 @@ class HeuristicProcessor(object):
|
|||||||
self.blanks_deleted = False
|
self.blanks_deleted = False
|
||||||
self.blanks_between_paragraphs = False
|
self.blanks_between_paragraphs = False
|
||||||
self.linereg = re.compile('(?<=<p).*?(?=</p>)', re.IGNORECASE|re.DOTALL)
|
self.linereg = re.compile('(?<=<p).*?(?=</p>)', re.IGNORECASE|re.DOTALL)
|
||||||
self.blankreg = re.compile(r'\s*(?P<openline><p(?!\sclass=\"(softbreak|spacer)\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
|
self.blankreg = re.compile(r'\s*(?P<openline><p(?!\sclass=\"(softbreak|whitespace)\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
|
||||||
self.anyblank = re.compile(r'\s*(?P<openline><p[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
|
self.anyblank = re.compile(r'\s*(?P<openline><p[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
|
||||||
self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}(?!\s*<h\d)', re.IGNORECASE)
|
self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}(?!\s*<h\d)', re.IGNORECASE)
|
||||||
|
self.any_multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}', re.IGNORECASE)
|
||||||
|
self.line_open = "<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*"
|
||||||
|
self.line_close = "(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>"
|
||||||
|
self.single_blank = re.compile(r'(\s*<p[^>]*>\s*</p>)', re.IGNORECASE)
|
||||||
|
self.scene_break_open = '<p class="scenebreak" style="text-align:center; text-indent:0%; margin-top:1em; margin-bottom:1em; page-break-before:avoid">'
|
||||||
|
|
||||||
def is_pdftohtml(self, src):
|
def is_pdftohtml(self, src):
|
||||||
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
|
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
|
||||||
@ -187,19 +192,17 @@ class HeuristicProcessor(object):
|
|||||||
|
|
||||||
# Build the Regular Expressions in pieces
|
# Build the Regular Expressions in pieces
|
||||||
init_lookahead = "(?=<(p|div))"
|
init_lookahead = "(?=<(p|div))"
|
||||||
chapter_line_open = "<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*"
|
chapter_line_open = self.line_open
|
||||||
title_line_open = "<(?P<outer2>p|div)[^>]*>\s*(<(?P<inner4>font|span|[ibu])[^>]*>)?\s*(<(?P<inner5>font|span|[ibu])[^>]*>)?\s*(<(?P<inner6>font|span|[ibu])[^>]*>)?\s*"
|
title_line_open = "<(?P<outer2>p|div)[^>]*>\s*(<(?P<inner4>font|span|[ibu])[^>]*>)?\s*(<(?P<inner5>font|span|[ibu])[^>]*>)?\s*(<(?P<inner6>font|span|[ibu])[^>]*>)?\s*"
|
||||||
chapter_header_open = r"(?P<chap>"
|
chapter_header_open = r"(?P<chap>"
|
||||||
title_header_open = r"(?P<title>"
|
title_header_open = r"(?P<title>"
|
||||||
chapter_header_close = ")\s*"
|
chapter_header_close = ")\s*"
|
||||||
title_header_close = ")"
|
title_header_close = ")"
|
||||||
chapter_line_close = "(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>"
|
chapter_line_close = self.line_close
|
||||||
title_line_close = "(</(?P=inner6)>)?\s*(</(?P=inner5)>)?\s*(</(?P=inner4)>)?\s*</(?P=outer2)>"
|
title_line_close = "(</(?P=inner6)>)?\s*(</(?P=inner5)>)?\s*(</(?P=inner4)>)?\s*</(?P=outer2)>"
|
||||||
|
|
||||||
is_pdftohtml = self.is_pdftohtml(html)
|
is_pdftohtml = self.is_pdftohtml(html)
|
||||||
if is_pdftohtml:
|
if is_pdftohtml:
|
||||||
chapter_line_open = "<(?P<outer>p)[^>]*>(\s*<[ibu][^>]*>)?\s*"
|
|
||||||
chapter_line_close = "\s*(</[ibu][^>]*>\s*)?</(?P=outer)>"
|
|
||||||
title_line_open = "<(?P<outer2>p)[^>]*>\s*"
|
title_line_open = "<(?P<outer2>p)[^>]*>\s*"
|
||||||
title_line_close = "\s*</(?P=outer2)>"
|
title_line_close = "\s*</(?P=outer2)>"
|
||||||
|
|
||||||
@ -374,13 +377,15 @@ class HeuristicProcessor(object):
|
|||||||
html = re.sub(ur'\s*<o:p>\s*</o:p>', ' ', html)
|
html = re.sub(ur'\s*<o:p>\s*</o:p>', ' ', html)
|
||||||
# Delete microsoft 'smart' tags
|
# Delete microsoft 'smart' tags
|
||||||
html = re.sub('(?i)</?st1:\w+>', '', html)
|
html = re.sub('(?i)</?st1:\w+>', '', html)
|
||||||
# Delete self closing paragraph tags
|
# Re-open self closing paragraph tags
|
||||||
html = re.sub('<p\s?/>', '', html)
|
html = re.sub('<p[^>/]*/>', '<p> </p>', html)
|
||||||
# Get rid of empty span, bold, font, em, & italics tags
|
# Get rid of empty span, bold, font, em, & italics tags
|
||||||
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]*>\s*</span>){0,2}\s*</span>\s*", " ", html)
|
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]*>\s*</span>){0,2}\s*</span>\s*", " ", html)
|
||||||
html = re.sub(r"\s*<(font|[ibu]|em|strong)[^>]*>\s*(<(font|[ibu]|em|strong)[^>]*>\s*</(font|[ibu]|em|strong)>\s*){0,2}\s*</(font|[ibu]|em|strong)>", " ", html)
|
html = re.sub(r"\s*<(font|[ibu]|em|strong)[^>]*>\s*(<(font|[ibu]|em|strong)[^>]*>\s*</(font|[ibu]|em|strong)>\s*){0,2}\s*</(font|[ibu]|em|strong)>", " ", html)
|
||||||
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]>\s*</span>){0,2}\s*</span>\s*", " ", html)
|
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]>\s*</span>){0,2}\s*</span>\s*", " ", html)
|
||||||
html = re.sub(r"\s*<(font|[ibu]|em|strong)[^>]*>\s*(<(font|[ibu]|em|strong)[^>]*>\s*</(font|[ibu]|em|strong)>\s*){0,2}\s*</(font|[ibu]|em|strong)>", " ", html)
|
html = re.sub(r"\s*<(font|[ibu]|em|strong)[^>]*>\s*(<(font|[ibu]|em|strong)[^>]*>\s*</(font|[ibu]|em|strong)>\s*){0,2}\s*</(font|[ibu]|em|strong)>", " ", html)
|
||||||
|
# Empty heading tags
|
||||||
|
html = re.sub(r'(?i)<h\d+>\s*</h\d+>', '', html)
|
||||||
self.deleted_nbsps = True
|
self.deleted_nbsps = True
|
||||||
return html
|
return html
|
||||||
|
|
||||||
@ -419,32 +424,99 @@ class HeuristicProcessor(object):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def detect_blank_formatting(self, html):
|
def merge_blanks(self, html, blanks_count=None):
|
||||||
blanks_before_headings = re.compile(r'(\s*<p[^>]*>\s*</p>){1,}(?=\s*<h\d)', re.IGNORECASE)
|
base_em = .5 # Baseline is 1.5em per blank line, 1st line is .5 em css and 1em for the nbsp
|
||||||
blanks_after_headings = re.compile(r'(?<=</h\d>)(\s*<p[^>]*>\s*</p>){1,}', re.IGNORECASE)
|
em_per_line = 1.5 # Add another 1.5 em for each additional blank
|
||||||
|
|
||||||
def markup_spacers(match):
|
def merge_matches(match):
|
||||||
blanks = match.group(0)
|
to_merge = match.group(0)
|
||||||
blanks = self.blankreg.sub('\n<p class="spacer"> </p>', blanks)
|
lines = float(len(self.single_blank.findall(to_merge))) - 1.
|
||||||
return blanks
|
em = base_em + (em_per_line * lines)
|
||||||
html = blanks_before_headings.sub(markup_spacers, html)
|
if to_merge.find('whitespace'):
|
||||||
html = blanks_after_headings.sub(markup_spacers, html)
|
newline = self.any_multi_blank.sub('\n<p class="whitespace'+str(int(em * 10))+'" style="text-align:center; margin-top:'+str(em)+'em"> </p>', match.group(0))
|
||||||
|
else:
|
||||||
|
newline = self.any_multi_blank.sub('\n<p class="softbreak'+str(int(em * 10))+'" style="text-align:center; margin-top:'+str(em)+'em"> </p>', match.group(0))
|
||||||
|
return newline
|
||||||
|
|
||||||
|
html = self.any_multi_blank.sub(merge_matches, html)
|
||||||
|
return html
|
||||||
|
|
||||||
|
def detect_whitespace(self, html):
|
||||||
|
blanks_around_headings = re.compile(r'(?P<initparas>(<p[^>]*>\s*</p>\s*){1,}\s*)?(?P<heading><h(?P<hnum>\d+)[^>]*>.*?</h(?P=hnum)>)(?P<endparas>\s*(<p[^>]*>\s*</p>\s*){1,})?', re.IGNORECASE)
|
||||||
|
blanks_n_nopunct = re.compile(r'(?P<initparas>(<p[^>]*>\s*</p>\s*){1,}\s*)?<p[^>]*>\s*(<(span|[ibu]|em|strong|font)[^>]*>\s*)*.{1,100}?[^\W](</(span|[ibu]|em|strong|font)>\s*)*</p>(?P<endparas>\s*(<p[^>]*>\s*</p>\s*){1,})?', re.IGNORECASE)
|
||||||
|
|
||||||
|
def merge_header_whitespace(match):
|
||||||
|
initblanks = match.group('initparas')
|
||||||
|
endblanks = match.group('initparas')
|
||||||
|
heading = match.group('heading')
|
||||||
|
top_margin = ''
|
||||||
|
bottom_margin = ''
|
||||||
|
if initblanks is not None:
|
||||||
|
top_margin = 'margin-top:'+str(len(self.single_blank.findall(initblanks)))+'em;'
|
||||||
|
if endblanks is not None:
|
||||||
|
bottom_margin = 'margin-bottom:'+str(len(self.single_blank.findall(initblanks)))+'em;'
|
||||||
|
|
||||||
|
if initblanks == None and endblanks == None:
|
||||||
|
return heading
|
||||||
|
else:
|
||||||
|
heading = re.sub('(?i)<h(?P<hnum>\d+)[^>]*>', '\n\n<h'+'\g<hnum>'+' style="'+top_margin+bottom_margin+'">', heading)
|
||||||
|
return heading
|
||||||
|
|
||||||
|
html = blanks_around_headings.sub(merge_header_whitespace, html)
|
||||||
|
|
||||||
|
def markup_whitespaces(match):
|
||||||
|
blanks = match.group(0)
|
||||||
|
blanks = self.blankreg.sub('\n<p class="whitespace" style="text-align:center; margin-top:0em; margin-bottom:0em"> </p>', blanks)
|
||||||
|
return blanks
|
||||||
|
|
||||||
|
html = blanks_n_nopunct.sub(markup_whitespaces, html)
|
||||||
if self.html_preprocess_sections > self.min_chapters:
|
if self.html_preprocess_sections > self.min_chapters:
|
||||||
html = re.sub('(?si)^.*?(?=<h\d)', markup_spacers, html)
|
html = re.sub('(?si)^.*?(?=<h\d)', markup_whitespaces, html)
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
||||||
def detect_soft_breaks(self, html):
|
def detect_soft_breaks(self, html):
|
||||||
if not self.blanks_deleted and self.blanks_between_paragraphs:
|
if not self.blanks_deleted and self.blanks_between_paragraphs:
|
||||||
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1.25em; margin-bottom:1.25em; page-break-before:avoid"> </p>', html)
|
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1em; page-break-before:avoid; text-align:center"> </p>', html)
|
||||||
else:
|
else:
|
||||||
html = self.blankreg.sub('\n<p class="softbreak" style="margin-top:1.25em; margin-bottom:1.25em; page-break-before:avoid"> </p>', html)
|
html = self.blankreg.sub('\n<p class="softbreak" style="margin-top:.5em; page-break-before:avoid; text-align:center"> </p>', html)
|
||||||
return html
|
return html
|
||||||
|
|
||||||
|
def markup_user_break(self, replacement_break):
|
||||||
|
'''
|
||||||
|
Takes string a user supplies and wraps it in markup that will be centered with
|
||||||
|
appropriate margins. <hr> and <img> tags are allowed. If the user specifies
|
||||||
|
a style with width attributes in the <hr> tag then the appropriate margins are
|
||||||
|
applied to wrapping divs. This is because many ebook devices don't support margin:auto
|
||||||
|
All other html is converted to text.
|
||||||
|
'''
|
||||||
|
hr_open = '<div id="scenebreak" style="margin-left: 45%; margin-right: 45%; margin-top:1.5em; margin-bottom:1.5em; page-break-before:avoid">'
|
||||||
|
if re.findall('(<|>)', replacement_break):
|
||||||
|
if re.match('^<hr', replacement_break):
|
||||||
|
if replacement_break.find('width') != -1:
|
||||||
|
width = int(re.sub('.*?width(:|=)(?P<wnum>\d+).*', '\g<wnum>', replacement_break))
|
||||||
|
replacement_break = re.sub('(?i)(width=\d+\%?|width:\s*\d+(\%|px|pt|em)?;?)', '', replacement_break)
|
||||||
|
divpercent = (100 - width) / 2
|
||||||
|
hr_open = re.sub('45', str(divpercent), hr_open)
|
||||||
|
scene_break = hr_open+replacement_break+'</div>'
|
||||||
|
else:
|
||||||
|
scene_break = hr_open+'<hr style="height: 3px; background:#505050" /></div>'
|
||||||
|
elif re.match('^<img', replacement_break):
|
||||||
|
scene_break = self.scene_break_open+replacement_break+'</p>'
|
||||||
|
else:
|
||||||
|
from calibre.utils.html2text import html2text
|
||||||
|
replacement_break = html2text(replacement_break)
|
||||||
|
replacement_break = re.sub('\s', ' ', replacement_break)
|
||||||
|
scene_break = self.scene_break_open+replacement_break+'</p>'
|
||||||
|
else:
|
||||||
|
replacement_break = re.sub('\s', ' ', replacement_break)
|
||||||
|
scene_break = self.scene_break_open+replacement_break+'</p>'
|
||||||
|
|
||||||
|
return scene_break
|
||||||
|
|
||||||
|
|
||||||
def __call__(self, html):
|
def __call__(self, html):
|
||||||
self.log.debug("********* Heuristic processing HTML *********")
|
self.log.debug("********* Heuristic processing HTML *********")
|
||||||
|
|
||||||
# Count the words in the document to estimate how many chapters to look for and whether
|
# Count the words in the document to estimate how many chapters to look for and whether
|
||||||
# other types of processing are attempted
|
# other types of processing are attempted
|
||||||
try:
|
try:
|
||||||
@ -458,7 +530,7 @@ class HeuristicProcessor(object):
|
|||||||
|
|
||||||
# Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
|
# Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
|
||||||
html = self.arrange_htm_line_endings(html)
|
html = self.arrange_htm_line_endings(html)
|
||||||
|
#self.dump(html, 'after_arrange_line_endings')
|
||||||
if self.cleanup_required():
|
if self.cleanup_required():
|
||||||
###### Check Markup ######
|
###### Check Markup ######
|
||||||
#
|
#
|
||||||
@ -478,6 +550,11 @@ class HeuristicProcessor(object):
|
|||||||
# fix indents must run before this step, as it removes non-breaking spaces
|
# fix indents must run before this step, as it removes non-breaking spaces
|
||||||
html = self.cleanup_markup(html)
|
html = self.cleanup_markup(html)
|
||||||
|
|
||||||
|
is_pdftohtml = self.is_pdftohtml(html)
|
||||||
|
if is_pdftohtml:
|
||||||
|
self.line_open = "<(?P<outer>p)[^>]*>(\s*<[ibu][^>]*>)?\s*"
|
||||||
|
self.line_close = "\s*(</[ibu][^>]*>\s*)?</(?P=outer)>"
|
||||||
|
|
||||||
# ADE doesn't render <br />, change to empty paragraphs
|
# ADE doesn't render <br />, change to empty paragraphs
|
||||||
#html = re.sub('<br[^>]*>', u'<p>\u00a0</p>', html)
|
#html = re.sub('<br[^>]*>', u'<p>\u00a0</p>', html)
|
||||||
|
|
||||||
@ -489,6 +566,7 @@ class HeuristicProcessor(object):
|
|||||||
|
|
||||||
if getattr(self.extra_opts, 'markup_chapter_headings', False):
|
if getattr(self.extra_opts, 'markup_chapter_headings', False):
|
||||||
html = self.markup_chapters(html, self.totalwords, self.blanks_between_paragraphs)
|
html = self.markup_chapters(html, self.totalwords, self.blanks_between_paragraphs)
|
||||||
|
#self.dump(html, 'after_chapter_markup')
|
||||||
|
|
||||||
if getattr(self.extra_opts, 'italicize_common_cases', False):
|
if getattr(self.extra_opts, 'italicize_common_cases', False):
|
||||||
html = self.markup_italicis(html)
|
html = self.markup_italicis(html)
|
||||||
@ -498,7 +576,7 @@ class HeuristicProcessor(object):
|
|||||||
if self.blanks_between_paragraphs and getattr(self.extra_opts, 'delete_blank_paragraphs', False):
|
if self.blanks_between_paragraphs and getattr(self.extra_opts, 'delete_blank_paragraphs', False):
|
||||||
self.log.debug("deleting blank lines")
|
self.log.debug("deleting blank lines")
|
||||||
self.blanks_deleted = True
|
self.blanks_deleted = True
|
||||||
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1.25em; margin-bottom:1.25em; page-break-before:avoid"> </p>', html)
|
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:.5em; page-break-before:avoid; text-align:center"> </p>', html)
|
||||||
html = self.blankreg.sub('', html)
|
html = self.blankreg.sub('', html)
|
||||||
|
|
||||||
# Determine line ending type
|
# Determine line ending type
|
||||||
@ -539,7 +617,7 @@ class HeuristicProcessor(object):
|
|||||||
if self.html_preprocess_sections < self.min_chapters and getattr(self.extra_opts, 'markup_chapter_headings', False):
|
if self.html_preprocess_sections < self.min_chapters and getattr(self.extra_opts, 'markup_chapter_headings', False):
|
||||||
self.log.debug("Looking for more split points based on punctuation,"
|
self.log.debug("Looking for more split points based on punctuation,"
|
||||||
" currently have " + unicode(self.html_preprocess_sections))
|
" currently have " + unicode(self.html_preprocess_sections))
|
||||||
chapdetect3 = re.compile(r'<(?P<styles>(p|div)[^>]*)>\s*(?P<section>(<span[^>]*>)?\s*(?!([*#•]+\s*)+)(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*.?(?=[a-z#\-*\s]+<)([a-z#-*]+\s*){1,5}\s*\s*(</span>)?(</[ibu]>){0,2}\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</span>)?\s*</(p|div)>)', re.IGNORECASE)
|
chapdetect3 = re.compile(r'<(?P<styles>(p|div)[^>]*)>\s*(?P<section>(<span[^>]*>)?\s*(?!([\W]+\s*)+)(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*.?(?=[a-z#\-*\s]+<)([a-z#-*]+\s*){1,5}\s*\s*(</span>)?(</[ibu]>){0,2}\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</span>)?\s*</(p|div)>)', re.IGNORECASE)
|
||||||
html = chapdetect3.sub(self.chapter_break, html)
|
html = chapdetect3.sub(self.chapter_break, html)
|
||||||
|
|
||||||
if getattr(self.extra_opts, 'renumber_headings', False):
|
if getattr(self.extra_opts, 'renumber_headings', False):
|
||||||
@ -549,14 +627,32 @@ class HeuristicProcessor(object):
|
|||||||
doubleheading = re.compile(r'(?P<firsthead><h(1|2)[^>]*>.+?</h(1|2)>\s*(<(?!h\d)[^>]*>\s*)*)<h(1|2)(?P<secondhead>[^>]*>.+?)</h(1|2)>', re.IGNORECASE)
|
doubleheading = re.compile(r'(?P<firsthead><h(1|2)[^>]*>.+?</h(1|2)>\s*(<(?!h\d)[^>]*>\s*)*)<h(1|2)(?P<secondhead>[^>]*>.+?)</h(1|2)>', re.IGNORECASE)
|
||||||
html = doubleheading.sub('\g<firsthead>'+'\n<h3'+'\g<secondhead>'+'</h3>', html)
|
html = doubleheading.sub('\g<firsthead>'+'\n<h3'+'\g<secondhead>'+'</h3>', html)
|
||||||
|
|
||||||
|
# If scene break formatting is enabled, find all blank paragraphs that definitely aren't scenebreaks,
|
||||||
|
# style it with the 'whitespace' class. All remaining blank lines are styled as softbreaks.
|
||||||
|
# Multiple sequential blank paragraphs are merged with appropriate margins
|
||||||
|
# If non-blank scene breaks exist they are center aligned and styled with appropriate margins.
|
||||||
if getattr(self.extra_opts, 'format_scene_breaks', False):
|
if getattr(self.extra_opts, 'format_scene_breaks', False):
|
||||||
html = self.detect_blank_formatting(html)
|
html = self.detect_whitespace(html)
|
||||||
html = self.detect_soft_breaks(html)
|
html = self.detect_soft_breaks(html)
|
||||||
# Center separator lines
|
blanks_count = len(self.any_multi_blank.findall(html))
|
||||||
html = re.sub(u'<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*(?P<break>([*#•=✦]+\s*)+)\s*(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>', '<p style="text-align:center; margin-top:1.25em; margin-bottom:1.25em; page-break-before:avoid">' + '\g<break>' + '</p>', html)
|
if blanks_count >= 1:
|
||||||
#html = re.sub('<p\s+class="softbreak"[^>]*>\s*</p>', '<div id="softbreak" style="margin-left: 45%; margin-right: 45%; margin-top:1.5em; margin-bottom:1.5em"><hr style="height: 3px; background:#505050" /></div>', html)
|
html = self.merge_blanks(html, blanks_count)
|
||||||
|
scene_break_regex = self.line_open+'(?![\w\'\"])(?P<break>((?P<break_char>((?!\s)\W))\s*(?P=break_char)?)+)\s*'+self.line_close
|
||||||
|
scene_break = re.compile(r'%s' % scene_break_regex, re.IGNORECASE|re.UNICODE)
|
||||||
|
# If the user has enabled scene break replacement, then either softbreaks
|
||||||
|
# or 'hard' scene breaks are replaced, depending on which is in use
|
||||||
|
# Otherwise separator lines are centered, use a bit larger margin in this case
|
||||||
|
replacement_break = getattr(self.extra_opts, 'replace_scene_breaks', None)
|
||||||
|
if replacement_break:
|
||||||
|
replacement_break = self.markup_user_break(replacement_break)
|
||||||
|
if len(scene_break.findall(html)) >= 1:
|
||||||
|
html = scene_break.sub(replacement_break, html)
|
||||||
|
else:
|
||||||
|
html = re.sub('<p\s+class="softbreak"[^>]*>\s*</p>', replacement_break, html)
|
||||||
|
else:
|
||||||
|
html = scene_break.sub(self.scene_break_open+'\g<break>'+'</p>', html)
|
||||||
|
|
||||||
if self.deleted_nbsps:
|
if self.deleted_nbsps:
|
||||||
# put back non-breaking spaces in empty paragraphs to preserve original formatting
|
# put back non-breaking spaces in empty paragraphs so they render correctly
|
||||||
html = self.anyblank.sub('\n'+r'\g<openline>'+u'\u00a0'+r'\g<closeline>', html)
|
html = self.anyblank.sub('\n'+r'\g<openline>'+u'\u00a0'+r'\g<closeline>', html)
|
||||||
return html
|
return html
|
||||||
|
@ -175,6 +175,19 @@ class EPUBInput(InputFormatPlugin):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
'EPUB files with DTBook markup are not supported')
|
'EPUB files with DTBook markup are not supported')
|
||||||
|
|
||||||
|
for x in list(opf.iterspine()):
|
||||||
|
ref = x.get('idref', None)
|
||||||
|
if ref is None:
|
||||||
|
x.getparent().remove(x)
|
||||||
|
continue
|
||||||
|
for y in opf.itermanifest():
|
||||||
|
if y.get('id', None) == ref and y.get('media-type', None) in \
|
||||||
|
('application/vnd.adobe-page-template+xml',):
|
||||||
|
p = x.getparent()
|
||||||
|
if p is not None:
|
||||||
|
p.remove(x)
|
||||||
|
break
|
||||||
|
|
||||||
with open('content.opf', 'wb') as nopf:
|
with open('content.opf', 'wb') as nopf:
|
||||||
nopf.write(opf.render())
|
nopf.write(opf.render())
|
||||||
|
|
||||||
|
@ -422,6 +422,33 @@ class MetadataField(object):
|
|||||||
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
|
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
|
||||||
obj.set_text(elem, self.renderer(val))
|
obj.set_text(elem, self.renderer(val))
|
||||||
|
|
||||||
|
class TitleSortField(MetadataField):
|
||||||
|
|
||||||
|
def __get__(self, obj, type=None):
|
||||||
|
c = self.__real_get__(obj, type)
|
||||||
|
if c is None:
|
||||||
|
matches = obj.title_path(obj.metadata)
|
||||||
|
if matches:
|
||||||
|
for match in matches:
|
||||||
|
ans = match.get('{%s}file-as'%obj.NAMESPACES['opf'], None)
|
||||||
|
if not ans:
|
||||||
|
ans = match.get('file-as', None)
|
||||||
|
if ans:
|
||||||
|
c = ans
|
||||||
|
if not c:
|
||||||
|
c = self.none_is
|
||||||
|
else:
|
||||||
|
c = c.strip()
|
||||||
|
return c
|
||||||
|
|
||||||
|
def __set__(self, obj, val):
|
||||||
|
MetadataField.__set__(self, obj, val)
|
||||||
|
matches = obj.title_path(obj.metadata)
|
||||||
|
if matches:
|
||||||
|
for match in matches:
|
||||||
|
for attr in list(match.attrib):
|
||||||
|
if attr.endswith('file-as'):
|
||||||
|
del match.attrib[attr]
|
||||||
|
|
||||||
def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
|
def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
|
||||||
from calibre.utils.config import to_json
|
from calibre.utils.config import to_json
|
||||||
@ -490,6 +517,7 @@ class OPF(object): # {{{
|
|||||||
rights = MetadataField('rights')
|
rights = MetadataField('rights')
|
||||||
series = MetadataField('series', is_dc=False)
|
series = MetadataField('series', is_dc=False)
|
||||||
series_index = MetadataField('series_index', is_dc=False, formatter=float, none_is=1)
|
series_index = MetadataField('series_index', is_dc=False, formatter=float, none_is=1)
|
||||||
|
title_sort = TitleSortField('title_sort', is_dc=False)
|
||||||
rating = MetadataField('rating', is_dc=False, formatter=int)
|
rating = MetadataField('rating', is_dc=False, formatter=int)
|
||||||
pubdate = MetadataField('date', formatter=parse_date,
|
pubdate = MetadataField('date', formatter=parse_date,
|
||||||
renderer=isoformat)
|
renderer=isoformat)
|
||||||
@ -776,30 +804,6 @@ class OPF(object): # {{{
|
|||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def title_sort(self):
|
|
||||||
|
|
||||||
def fget(self):
|
|
||||||
matches = self.title_path(self.metadata)
|
|
||||||
if matches:
|
|
||||||
for match in matches:
|
|
||||||
ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
|
|
||||||
if not ans:
|
|
||||||
ans = match.get('file-as', None)
|
|
||||||
if ans:
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def fset(self, val):
|
|
||||||
matches = self.title_path(self.metadata)
|
|
||||||
if matches:
|
|
||||||
for key in matches[0].attrib:
|
|
||||||
if key.endswith('file-as'):
|
|
||||||
matches[0].attrib.pop(key)
|
|
||||||
matches[0].set('{%s}file-as'%self.NAMESPACES['opf'], unicode(val))
|
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
|
||||||
|
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def tags(self):
|
def tags(self):
|
||||||
|
|
||||||
@ -1129,8 +1133,6 @@ class OPFCreator(Metadata):
|
|||||||
metadata = M.metadata()
|
metadata = M.metadata()
|
||||||
a = metadata.append
|
a = metadata.append
|
||||||
role = {}
|
role = {}
|
||||||
if self.title_sort:
|
|
||||||
role = {'file-as':self.title_sort}
|
|
||||||
a(DC_ELEM('title', self.title if self.title else _('Unknown'),
|
a(DC_ELEM('title', self.title if self.title else _('Unknown'),
|
||||||
opf_attrs=role))
|
opf_attrs=role))
|
||||||
for i, author in enumerate(self.authors):
|
for i, author in enumerate(self.authors):
|
||||||
@ -1165,6 +1167,8 @@ class OPFCreator(Metadata):
|
|||||||
a(CAL_ELEM('calibre:series', self.series))
|
a(CAL_ELEM('calibre:series', self.series))
|
||||||
if self.series_index is not None:
|
if self.series_index is not None:
|
||||||
a(CAL_ELEM('calibre:series_index', self.format_series_index()))
|
a(CAL_ELEM('calibre:series_index', self.format_series_index()))
|
||||||
|
if self.title_sort:
|
||||||
|
a(CAL_ELEM('calibre:title_sort', self.title_sort))
|
||||||
if self.rating is not None:
|
if self.rating is not None:
|
||||||
a(CAL_ELEM('calibre:rating', str(self.rating)))
|
a(CAL_ELEM('calibre:rating', str(self.rating)))
|
||||||
if self.timestamp is not None:
|
if self.timestamp is not None:
|
||||||
@ -1320,7 +1324,6 @@ def test_m2o():
|
|||||||
mi.author_sort = 'author sort'
|
mi.author_sort = 'author sort'
|
||||||
mi.pubdate = nowf()
|
mi.pubdate = nowf()
|
||||||
mi.language = 'en'
|
mi.language = 'en'
|
||||||
mi.category = 'test'
|
|
||||||
mi.comments = 'what a fun book\n\n'
|
mi.comments = 'what a fun book\n\n'
|
||||||
mi.publisher = 'publisher'
|
mi.publisher = 'publisher'
|
||||||
mi.isbn = 'boooo'
|
mi.isbn = 'boooo'
|
||||||
@ -1335,11 +1338,11 @@ def test_m2o():
|
|||||||
opf = metadata_to_opf(mi)
|
opf = metadata_to_opf(mi)
|
||||||
print opf
|
print opf
|
||||||
newmi = MetaInformation(OPF(StringIO(opf)))
|
newmi = MetaInformation(OPF(StringIO(opf)))
|
||||||
for attr in ('author_sort', 'title_sort', 'comments', 'category',
|
for attr in ('author_sort', 'title_sort', 'comments',
|
||||||
'publisher', 'series', 'series_index', 'rating',
|
'publisher', 'series', 'series_index', 'rating',
|
||||||
'isbn', 'tags', 'cover_data', 'application_id',
|
'isbn', 'tags', 'cover_data', 'application_id',
|
||||||
'language', 'cover',
|
'language', 'cover',
|
||||||
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
|
'book_producer', 'timestamp',
|
||||||
'pubdate', 'rights', 'publication_type'):
|
'pubdate', 'rights', 'publication_type'):
|
||||||
o, n = getattr(mi, attr), getattr(newmi, attr)
|
o, n = getattr(mi, attr), getattr(newmi, attr)
|
||||||
if o != n and o.strip() != n.strip():
|
if o != n and o.strip() != n.strip():
|
||||||
@ -1441,4 +1444,6 @@ def test_user_metadata():
|
|||||||
print opf.render()
|
print opf.render()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test_user_metadata()
|
#test_user_metadata()
|
||||||
|
#test_m2o()
|
||||||
|
test()
|
||||||
|
61
src/calibre/ebooks/metadata/sources/base.py
Normal file
61
src/calibre/ebooks/metadata/sources/base.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from calibre.customize import Plugin
|
||||||
|
|
||||||
|
class Source(Plugin):
|
||||||
|
|
||||||
|
type = _('Metadata source')
|
||||||
|
author = 'Kovid Goyal'
|
||||||
|
|
||||||
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
|
||||||
|
result_of_identify_is_complete = True
|
||||||
|
|
||||||
|
def get_author_tokens(self, authors):
|
||||||
|
'Take a list of authors and return a list of tokens useful for a '
|
||||||
|
'AND search query'
|
||||||
|
# Leave ' in there for Irish names
|
||||||
|
pat = re.compile(r'[-,:;+!@#$%^&*(){}.`~"\s\[\]/]')
|
||||||
|
for au in authors:
|
||||||
|
for tok in au.split():
|
||||||
|
yield pat.sub('', tok)
|
||||||
|
|
||||||
|
def split_jobs(self, jobs, num):
|
||||||
|
'Split a list of jobs into at most num groups, as evenly as possible'
|
||||||
|
groups = [[] for i in range(num)]
|
||||||
|
jobs = list(jobs)
|
||||||
|
while jobs:
|
||||||
|
for gr in groups:
|
||||||
|
try:
|
||||||
|
job = jobs.pop()
|
||||||
|
except IndexError:
|
||||||
|
break
|
||||||
|
gr.append(job)
|
||||||
|
return [g for g in groups if g]
|
||||||
|
|
||||||
|
def identify(self, log, result_queue, abort, title=None, authors=None, identifiers={}):
|
||||||
|
'''
|
||||||
|
Identify a book by its title/author/isbn/etc.
|
||||||
|
|
||||||
|
:param log: A log object, use it to output debugging information/errors
|
||||||
|
:param result_queue: A result Queue, results should be put into it.
|
||||||
|
Each result is a Metadata object
|
||||||
|
:param abort: If abort.is_set() returns True, abort further processing
|
||||||
|
and return as soon as possible
|
||||||
|
:param title: The title of the book, can be None
|
||||||
|
:param authors: A list of authors of the book, can be None
|
||||||
|
:param identifiers: A dictionary of other identifiers, most commonly
|
||||||
|
{'isbn':'1234...'}
|
||||||
|
:return: None if no errors occurred, otherwise a unicode representation
|
||||||
|
of the error suitable for showing to the user
|
||||||
|
|
||||||
|
'''
|
||||||
|
return None
|
||||||
|
|
215
src/calibre/ebooks/metadata/sources/google.py
Normal file
215
src/calibre/ebooks/metadata/sources/google.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import time
|
||||||
|
from urllib import urlencode
|
||||||
|
from functools import partial
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from calibre.ebooks.metadata.sources import Source
|
||||||
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
|
from calibre.utils.date import parse_date, utcnow
|
||||||
|
from calibre import browser, as_unicode
|
||||||
|
|
||||||
|
NAMESPACES = {
|
||||||
|
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
|
||||||
|
'atom' : 'http://www.w3.org/2005/Atom',
|
||||||
|
'dc': 'http://purl.org/dc/terms'
|
||||||
|
}
|
||||||
|
XPath = partial(etree.XPath, namespaces=NAMESPACES)
|
||||||
|
|
||||||
|
total_results = XPath('//openSearch:totalResults')
|
||||||
|
start_index = XPath('//openSearch:startIndex')
|
||||||
|
items_per_page = XPath('//openSearch:itemsPerPage')
|
||||||
|
entry = XPath('//atom:entry')
|
||||||
|
entry_id = XPath('descendant::atom:id')
|
||||||
|
creator = XPath('descendant::dc:creator')
|
||||||
|
identifier = XPath('descendant::dc:identifier')
|
||||||
|
title = XPath('descendant::dc:title')
|
||||||
|
date = XPath('descendant::dc:date')
|
||||||
|
publisher = XPath('descendant::dc:publisher')
|
||||||
|
subject = XPath('descendant::dc:subject')
|
||||||
|
description = XPath('descendant::dc:description')
|
||||||
|
language = XPath('descendant::dc:language')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def to_metadata(browser, log, entry_):
|
||||||
|
|
||||||
|
def get_text(extra, x):
|
||||||
|
try:
|
||||||
|
ans = x(extra)
|
||||||
|
if ans:
|
||||||
|
ans = ans[0].text
|
||||||
|
if ans and ans.strip():
|
||||||
|
return ans.strip()
|
||||||
|
except:
|
||||||
|
log.exception('Programming error:')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
id_url = entry_id(entry_)[0].text
|
||||||
|
title_ = ': '.join([x.text for x in title(entry_)]).strip()
|
||||||
|
authors = [x.text.strip() for x in creator(entry_) if x.text]
|
||||||
|
if not authors:
|
||||||
|
authors = [_('Unknown')]
|
||||||
|
if not id_url or not title:
|
||||||
|
# Silently discard this entry
|
||||||
|
return None
|
||||||
|
|
||||||
|
mi = Metadata(title_, authors)
|
||||||
|
try:
|
||||||
|
raw = browser.open_novisit(id_url).read()
|
||||||
|
feed = etree.fromstring(raw)
|
||||||
|
extra = entry(feed)[0]
|
||||||
|
except:
|
||||||
|
log.exception('Failed to get additional details for', mi.title)
|
||||||
|
return mi
|
||||||
|
|
||||||
|
mi.comments = get_text(extra, description)
|
||||||
|
#mi.language = get_text(extra, language)
|
||||||
|
mi.publisher = get_text(extra, publisher)
|
||||||
|
|
||||||
|
# Author sort
|
||||||
|
for x in creator(extra):
|
||||||
|
for key, val in x.attrib.items():
|
||||||
|
if key.endswith('file-as') and val and val.strip():
|
||||||
|
mi.author_sort = val
|
||||||
|
break
|
||||||
|
# ISBN
|
||||||
|
isbns = []
|
||||||
|
for x in identifier(extra):
|
||||||
|
t = str(x.text).strip()
|
||||||
|
if t[:5].upper() in ('ISBN:', 'LCCN:', 'OCLC:'):
|
||||||
|
if t[:5].upper() == 'ISBN:':
|
||||||
|
isbns.append(t[5:])
|
||||||
|
if isbns:
|
||||||
|
mi.isbn = sorted(isbns, key=len)[-1]
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
try:
|
||||||
|
btags = [x.text for x in subject(extra) if x.text]
|
||||||
|
tags = []
|
||||||
|
for t in btags:
|
||||||
|
tags.extend([y.strip() for y in t.split('/')])
|
||||||
|
tags = list(sorted(list(set(tags))))
|
||||||
|
except:
|
||||||
|
log.exception('Failed to parse tags:')
|
||||||
|
tags = []
|
||||||
|
if tags:
|
||||||
|
mi.tags = [x.replace(',', ';') for x in tags]
|
||||||
|
|
||||||
|
# pubdate
|
||||||
|
pubdate = get_text(extra, date)
|
||||||
|
if pubdate:
|
||||||
|
try:
|
||||||
|
default = utcnow().replace(day=15)
|
||||||
|
mi.pubdate = parse_date(pubdate, assume_utc=True, default=default)
|
||||||
|
except:
|
||||||
|
log.exception('Failed to parse pubdate')
|
||||||
|
|
||||||
|
|
||||||
|
return mi
|
||||||
|
|
||||||
|
class Worker(Thread):
|
||||||
|
|
||||||
|
def __init__(self, log, entries, abort, result_queue):
|
||||||
|
self.browser, self.log, self.entries = browser(), log, entries
|
||||||
|
self.abort, self.result_queue = abort, result_queue
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for i in self.entries:
|
||||||
|
try:
|
||||||
|
ans = to_metadata(self.browser, self.log, i)
|
||||||
|
if isinstance(ans, Metadata):
|
||||||
|
self.result_queue.put(ans)
|
||||||
|
except:
|
||||||
|
self.log.exception(
|
||||||
|
'Failed to get metadata for identify entry:',
|
||||||
|
etree.tostring(i))
|
||||||
|
if self.abort.is_set():
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleBooks(Source):
|
||||||
|
|
||||||
|
name = 'Google Books'
|
||||||
|
|
||||||
|
def create_query(self, log, title=None, authors=None, identifiers={},
|
||||||
|
start_index=1):
|
||||||
|
BASE_URL = 'http://books.google.com/books/feeds/volumes?'
|
||||||
|
isbn = identifiers.get('isbn', None)
|
||||||
|
q = ''
|
||||||
|
if isbn is not None:
|
||||||
|
q += 'isbn:'+isbn
|
||||||
|
elif title or authors:
|
||||||
|
def build_term(prefix, parts):
|
||||||
|
return ' '.join('in'+prefix + ':' + x for x in parts)
|
||||||
|
if title is not None:
|
||||||
|
q += build_term('title', title.split())
|
||||||
|
if authors:
|
||||||
|
q += ('+' if q else '')+build_term('author',
|
||||||
|
self.get_author_tokens(authors))
|
||||||
|
|
||||||
|
if isinstance(q, unicode):
|
||||||
|
q = q.encode('utf-8')
|
||||||
|
if not q:
|
||||||
|
return None
|
||||||
|
return BASE_URL+urlencode({
|
||||||
|
'q':q,
|
||||||
|
'max-results':20,
|
||||||
|
'start-index':start_index,
|
||||||
|
'min-viewability':'none',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def identify(self, log, result_queue, abort, title=None, authors=None, identifiers={}):
|
||||||
|
query = self.create_query(log, title=title, authors=authors,
|
||||||
|
identifiers=identifiers)
|
||||||
|
try:
|
||||||
|
raw = browser().open_novisit(query).read()
|
||||||
|
except Exception, e:
|
||||||
|
log.exception('Failed to make identify query: %r'%query)
|
||||||
|
return as_unicode(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
parser = etree.XMLParser(recover=True, no_network=True)
|
||||||
|
feed = etree.fromstring(raw, parser=parser)
|
||||||
|
entries = entry(feed)
|
||||||
|
except Exception, e:
|
||||||
|
log.exception('Failed to parse identify results')
|
||||||
|
return as_unicode(e)
|
||||||
|
|
||||||
|
|
||||||
|
groups = self.split_jobs(entries, 5) # At most 5 threads
|
||||||
|
if not groups:
|
||||||
|
return
|
||||||
|
workers = [Worker(log, entries, abort, result_queue) for entries in
|
||||||
|
groups]
|
||||||
|
|
||||||
|
if abort.is_set():
|
||||||
|
return
|
||||||
|
|
||||||
|
for worker in workers: worker.start()
|
||||||
|
|
||||||
|
has_alive_worker = True
|
||||||
|
while has_alive_worker and not abort.is_set():
|
||||||
|
has_alive_worker = False
|
||||||
|
for worker in workers:
|
||||||
|
if worker.is_alive():
|
||||||
|
has_alive_worker = True
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -103,6 +103,8 @@ class EXTHHeader(object):
|
|||||||
pass
|
pass
|
||||||
elif id == 108:
|
elif id == 108:
|
||||||
pass # Producer
|
pass # Producer
|
||||||
|
elif id == 113:
|
||||||
|
pass # ASIN or UUID
|
||||||
#else:
|
#else:
|
||||||
# print 'unhandled metadata record', id, repr(content)
|
# print 'unhandled metadata record', id, repr(content)
|
||||||
|
|
||||||
|
@ -1547,6 +1547,31 @@ class MobiWriter(object):
|
|||||||
rights = 'Unknown'
|
rights = 'Unknown'
|
||||||
exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8))
|
exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8))
|
||||||
exth.write(rights)
|
exth.write(rights)
|
||||||
|
nrecs += 1
|
||||||
|
|
||||||
|
# Write UUID as ASIN
|
||||||
|
uuid = None
|
||||||
|
from calibre.ebooks.oeb.base import OPF
|
||||||
|
for x in oeb.metadata['identifier']:
|
||||||
|
if x.get(OPF('scheme'), None).lower() == 'uuid' or unicode(x).startswith('urn:uuid:'):
|
||||||
|
uuid = unicode(x).split(':')[-1]
|
||||||
|
break
|
||||||
|
if uuid is None:
|
||||||
|
from uuid import uuid4
|
||||||
|
uuid = str(uuid4())
|
||||||
|
|
||||||
|
if isinstance(uuid, unicode):
|
||||||
|
uuid = uuid.encode('utf-8')
|
||||||
|
exth.write(pack('>II', 113, len(uuid) + 8))
|
||||||
|
exth.write(uuid)
|
||||||
|
nrecs += 1
|
||||||
|
|
||||||
|
# Write cdetype
|
||||||
|
if not self.opts.mobi_periodical:
|
||||||
|
data = 'EBOK'
|
||||||
|
exth.write(pack('>II', 501, len(data)+8))
|
||||||
|
exth.write(data)
|
||||||
|
nrecs += 1
|
||||||
|
|
||||||
# Add a publication date entry
|
# Add a publication date entry
|
||||||
if oeb.metadata['date'] != [] :
|
if oeb.metadata['date'] != [] :
|
||||||
|
@ -218,7 +218,7 @@ class TXTMLizer(object):
|
|||||||
|
|
||||||
if tag in SPACE_TAGS:
|
if tag in SPACE_TAGS:
|
||||||
text.append(u' ')
|
text.append(u' ')
|
||||||
|
|
||||||
# Scene breaks.
|
# Scene breaks.
|
||||||
if tag == 'hr':
|
if tag == 'hr':
|
||||||
text.append('\n\n* * *\n\n')
|
text.append('\n\n* * *\n\n')
|
||||||
|
@ -74,23 +74,29 @@ class ShareConnMenu(QMenu): # {{{
|
|||||||
opts = email_config().parse()
|
opts = email_config().parse()
|
||||||
if opts.accounts:
|
if opts.accounts:
|
||||||
self.email_to_menu = QMenu(_('Email to')+'...', self)
|
self.email_to_menu = QMenu(_('Email to')+'...', self)
|
||||||
|
ac = self.addMenu(self.email_to_menu)
|
||||||
|
self.email_actions.append(ac)
|
||||||
|
self.email_to_and_delete_menu = QMenu(
|
||||||
|
_('Email to and delete from library')+'...', self)
|
||||||
keys = sorted(opts.accounts.keys())
|
keys = sorted(opts.accounts.keys())
|
||||||
for account in keys:
|
for account in keys:
|
||||||
formats, auto, default = opts.accounts[account]
|
formats, auto, default = opts.accounts[account]
|
||||||
dest = 'mail:'+account+';'+formats
|
dest = 'mail:'+account+';'+formats
|
||||||
action1 = DeviceAction(dest, False, False, I('mail.png'),
|
action1 = DeviceAction(dest, False, False, I('mail.png'),
|
||||||
_('Email to')+' '+account)
|
account)
|
||||||
action2 = DeviceAction(dest, True, False, I('mail.png'),
|
action2 = DeviceAction(dest, True, False, I('mail.png'),
|
||||||
_('Email to')+' '+account+ _(' and delete from library'))
|
account + ' ' + _('(delete from library)'))
|
||||||
map(self.email_to_menu.addAction, (action1, action2))
|
self.email_to_menu.addAction(action1)
|
||||||
|
self.email_to_and_delete_menu.addAction(action2)
|
||||||
map(self.memory.append, (action1, action2))
|
map(self.memory.append, (action1, action2))
|
||||||
if default:
|
if default:
|
||||||
map(self.addAction, (action1, action2))
|
ac = DeviceAction(dest, False, False,
|
||||||
map(self.email_actions.append, (action1, action2))
|
I('mail.png'), _('Email to') + ' ' +account)
|
||||||
self.email_to_menu.addSeparator()
|
self.addAction(ac)
|
||||||
|
self.email_actions.append(ac)
|
||||||
action1.a_s.connect(sync_menu.action_triggered)
|
action1.a_s.connect(sync_menu.action_triggered)
|
||||||
action2.a_s.connect(sync_menu.action_triggered)
|
action2.a_s.connect(sync_menu.action_triggered)
|
||||||
ac = self.addMenu(self.email_to_menu)
|
ac = self.addMenu(self.email_to_and_delete_menu)
|
||||||
self.email_actions.append(ac)
|
self.email_actions.append(ac)
|
||||||
else:
|
else:
|
||||||
ac = self.addAction(_('Setup email based sharing of books'))
|
ac = self.addAction(_('Setup email based sharing of books'))
|
||||||
|
356
src/calibre/gui2/complete.py
Normal file
356
src/calibre/gui2/complete.py
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, QTimer, \
|
||||||
|
QApplication, QPoint, QItemDelegate, QStyleOptionViewItem, \
|
||||||
|
QStyle, QEvent, pyqtSignal
|
||||||
|
|
||||||
|
from calibre.utils.icu import sort_key, lower
|
||||||
|
from calibre.gui2 import NONE
|
||||||
|
from calibre.gui2.widgets import EnComboBox
|
||||||
|
|
||||||
|
class CompleterItemDelegate(QItemDelegate): # {{{
|
||||||
|
|
||||||
|
''' Renders the current item as thought it were selected '''
|
||||||
|
|
||||||
|
def __init__(self, view):
|
||||||
|
self.view = view
|
||||||
|
QItemDelegate.__init__(self, view)
|
||||||
|
|
||||||
|
def paint(self, p, opt, idx):
|
||||||
|
opt = QStyleOptionViewItem(opt)
|
||||||
|
opt.showDecorationSelected = True
|
||||||
|
if self.view.currentIndex() == idx:
|
||||||
|
opt.state |= QStyle.State_HasFocus
|
||||||
|
QItemDelegate.paint(self, p, opt, idx)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CompleteWindow(QListView): # {{{
|
||||||
|
|
||||||
|
'''
|
||||||
|
The completion popup. For keyboard and mouse handling see
|
||||||
|
:meth:`eventFilter`.
|
||||||
|
'''
|
||||||
|
|
||||||
|
#: This signal is emitted when the user selects one of the listed
|
||||||
|
#: completions, by mouse or keyboard
|
||||||
|
completion_selected = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self, widget, model):
|
||||||
|
self.widget = widget
|
||||||
|
QListView.__init__(self)
|
||||||
|
self.setVisible(False)
|
||||||
|
self.setParent(None, Qt.Popup)
|
||||||
|
self.setAlternatingRowColors(True)
|
||||||
|
self.setFocusPolicy(Qt.NoFocus)
|
||||||
|
self._d = CompleterItemDelegate(self)
|
||||||
|
self.setItemDelegate(self._d)
|
||||||
|
self.setModel(model)
|
||||||
|
self.setFocusProxy(widget)
|
||||||
|
self.installEventFilter(self)
|
||||||
|
self.clicked.connect(self.do_selected)
|
||||||
|
self.entered.connect(self.do_entered)
|
||||||
|
self.setMouseTracking(True)
|
||||||
|
|
||||||
|
def do_entered(self, idx):
|
||||||
|
if idx.isValid():
|
||||||
|
self.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
def do_selected(self, idx=None):
|
||||||
|
idx = self.currentIndex() if idx is None else idx
|
||||||
|
if idx.isValid():
|
||||||
|
data = unicode(self.model().data(idx, Qt.DisplayRole))
|
||||||
|
self.completion_selected.emit(data)
|
||||||
|
self.hide()
|
||||||
|
|
||||||
|
def eventFilter(self, o, e):
|
||||||
|
if o is not self:
|
||||||
|
return False
|
||||||
|
if e.type() == e.KeyPress:
|
||||||
|
key = e.key()
|
||||||
|
if key in (Qt.Key_Escape, Qt.Key_Backtab) or \
|
||||||
|
(key == Qt.Key_F4 and (e.modifiers() & Qt.AltModifier)):
|
||||||
|
self.hide()
|
||||||
|
return True
|
||||||
|
elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
|
||||||
|
if key == Qt.Key_Tab and not self.currentIndex().isValid():
|
||||||
|
if self.model().rowCount() > 0:
|
||||||
|
self.setCurrentIndex(self.model().index(0))
|
||||||
|
self.do_selected()
|
||||||
|
return True
|
||||||
|
elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp,
|
||||||
|
Qt.Key_PageDown):
|
||||||
|
return False
|
||||||
|
# Send key event to associated line edit
|
||||||
|
self.widget.eat_focus_out = False
|
||||||
|
try:
|
||||||
|
self.widget.event(e)
|
||||||
|
finally:
|
||||||
|
self.widget.eat_focus_out = True
|
||||||
|
if not self.widget.hasFocus():
|
||||||
|
# Line edit lost focus
|
||||||
|
self.hide()
|
||||||
|
if e.isAccepted():
|
||||||
|
# Line edit consumed event
|
||||||
|
return True
|
||||||
|
elif e.type() == e.MouseButtonPress:
|
||||||
|
# Hide popup if user clicks outside it, otherwise
|
||||||
|
# pass event to popup
|
||||||
|
if not self.underMouse():
|
||||||
|
self.hide()
|
||||||
|
return True
|
||||||
|
elif e.type() in (e.InputMethod, e.ShortcutOverride):
|
||||||
|
QApplication.sendEvent(self.widget, e)
|
||||||
|
|
||||||
|
return False # Do not filter this event
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CompleteModel(QAbstractListModel):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QAbstractListModel.__init__(self, parent)
|
||||||
|
self.sep = ','
|
||||||
|
self.space_before_sep = False
|
||||||
|
self.items = []
|
||||||
|
self.lowered_items = []
|
||||||
|
self.matches = []
|
||||||
|
|
||||||
|
def set_items(self, items):
|
||||||
|
items = [unicode(x.strip()) for x in items]
|
||||||
|
self.items = list(sorted(items, key=lambda x: sort_key(x)))
|
||||||
|
self.lowered_items = [lower(x) for x in self.items]
|
||||||
|
self.matches = []
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def rowCount(self, *args):
|
||||||
|
return len(self.matches)
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
r = index.row()
|
||||||
|
try:
|
||||||
|
return self.matches[r]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def get_matches(self, prefix):
|
||||||
|
'''
|
||||||
|
Return all matches that (case insensitively) start with prefix
|
||||||
|
'''
|
||||||
|
prefix = lower(prefix)
|
||||||
|
ans = []
|
||||||
|
if prefix:
|
||||||
|
for i, test in enumerate(self.lowered_items):
|
||||||
|
if test.startswith(prefix):
|
||||||
|
ans.append(self.items[i])
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def update_matches(self, matches):
|
||||||
|
self.matches = matches
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
class MultiCompleteLineEdit(QLineEdit):
|
||||||
|
'''
|
||||||
|
A line edit that completes on multiple items separated by a
|
||||||
|
separator. Use the :meth:`update_items_cache` to set the list of
|
||||||
|
all possible completions. Separator can be controlled with the
|
||||||
|
:meth:`set_separator` and :meth:`set_space_before_sep` methods.
|
||||||
|
|
||||||
|
A call to self.set_separator(None) will allow this widget to be used
|
||||||
|
to complete non multiple fields as well.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
self.eat_focus_out = True
|
||||||
|
self.max_visible_items = 7
|
||||||
|
self.current_prefix = None
|
||||||
|
QLineEdit.__init__(self, parent)
|
||||||
|
|
||||||
|
self._model = CompleteModel(parent=self)
|
||||||
|
self.complete_window = CompleteWindow(self, self._model)
|
||||||
|
self.textEdited.connect(self.text_edited)
|
||||||
|
self.complete_window.completion_selected.connect(self.completion_selected)
|
||||||
|
self.installEventFilter(self)
|
||||||
|
|
||||||
|
# Interface {{{
|
||||||
|
def update_items_cache(self, complete_items):
|
||||||
|
self.all_items = complete_items
|
||||||
|
|
||||||
|
def set_separator(self, sep):
|
||||||
|
self.sep = sep
|
||||||
|
|
||||||
|
def set_space_before_sep(self, space_before):
|
||||||
|
self.space_before_sep = space_before
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def eventFilter(self, o, e):
|
||||||
|
if self.eat_focus_out and o is self and e.type() == QEvent.FocusOut:
|
||||||
|
if self.complete_window.isVisible():
|
||||||
|
return True # Filter this event since the cw is visible
|
||||||
|
return QLineEdit.eventFilter(self, o, e)
|
||||||
|
|
||||||
|
|
||||||
|
def text_edited(self, *args):
|
||||||
|
self.update_completions()
|
||||||
|
|
||||||
|
def update_completions(self):
|
||||||
|
' Update the list of completions '
|
||||||
|
if not self.complete_window.isVisible() and not self.hasFocus():
|
||||||
|
return
|
||||||
|
cpos = self.cursorPosition()
|
||||||
|
text = unicode(self.text())
|
||||||
|
prefix = text[:cpos]
|
||||||
|
self.current_prefix = prefix
|
||||||
|
complete_prefix = prefix.lstrip()
|
||||||
|
if self.sep:
|
||||||
|
complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip()
|
||||||
|
|
||||||
|
matches = self._model.get_matches(complete_prefix)
|
||||||
|
self.update_complete_window(matches)
|
||||||
|
|
||||||
|
def get_completed_text(self, text):
|
||||||
|
'''
|
||||||
|
Get completed text from current cursor position and the completion
|
||||||
|
text
|
||||||
|
'''
|
||||||
|
if self.sep is None:
|
||||||
|
return -1, text
|
||||||
|
else:
|
||||||
|
cursor_pos = self.cursorPosition()
|
||||||
|
before_text = unicode(self.text())[:cursor_pos]
|
||||||
|
after_text = unicode(self.text())[cursor_pos:]
|
||||||
|
after_parts = after_text.split(self.sep)
|
||||||
|
if len(after_parts) < 3 and not after_parts[-1].strip():
|
||||||
|
after_text = u''
|
||||||
|
prefix_len = len(before_text.split(self.sep)[-1].lstrip())
|
||||||
|
return prefix_len, \
|
||||||
|
before_text[:cursor_pos - prefix_len] + text + after_text
|
||||||
|
|
||||||
|
def completion_selected(self, text):
|
||||||
|
prefix_len, ctext = self.get_completed_text(text)
|
||||||
|
if self.sep is None:
|
||||||
|
self.setText(ctext)
|
||||||
|
self.setCursorPosition(len(ctext))
|
||||||
|
else:
|
||||||
|
cursor_pos = self.cursorPosition()
|
||||||
|
self.setText(ctext)
|
||||||
|
self.setCursorPosition(cursor_pos - prefix_len + len(text))
|
||||||
|
|
||||||
|
def update_complete_window(self, matches):
|
||||||
|
self._model.update_matches(matches)
|
||||||
|
if matches:
|
||||||
|
self.show_complete_window()
|
||||||
|
else:
|
||||||
|
self.complete_window.hide()
|
||||||
|
|
||||||
|
|
||||||
|
def position_complete_window(self):
|
||||||
|
popup = self.complete_window
|
||||||
|
screen = QApplication.desktop().availableGeometry(self)
|
||||||
|
h = (popup.sizeHintForRow(0) * min(self.max_visible_items,
|
||||||
|
popup.model().rowCount()) + 3) + 3
|
||||||
|
hsb = popup.horizontalScrollBar()
|
||||||
|
if hsb and hsb.isVisible():
|
||||||
|
h += hsb.sizeHint().height()
|
||||||
|
|
||||||
|
rh = self.height()
|
||||||
|
pos = self.mapToGlobal(QPoint(0, self.height() - 2))
|
||||||
|
w = self.width()
|
||||||
|
|
||||||
|
if w > screen.width():
|
||||||
|
w = screen.width()
|
||||||
|
if (pos.x() + w) > (screen.x() + screen.width()):
|
||||||
|
pos.setX(screen.x() + screen.width() - w)
|
||||||
|
if (pos.x() < screen.x()):
|
||||||
|
pos.setX(screen.x())
|
||||||
|
|
||||||
|
top = pos.y() - rh - screen.top() + 2
|
||||||
|
bottom = screen.bottom() - pos.y()
|
||||||
|
h = max(h, popup.minimumHeight())
|
||||||
|
if h > bottom:
|
||||||
|
h = min(max(top, bottom), h)
|
||||||
|
if top > bottom:
|
||||||
|
pos.setY(pos.y() - h - rh + 2)
|
||||||
|
|
||||||
|
popup.setGeometry(pos.x(), pos.y(), w, h)
|
||||||
|
|
||||||
|
|
||||||
|
def show_complete_window(self):
|
||||||
|
self.position_complete_window()
|
||||||
|
self.complete_window.show()
|
||||||
|
|
||||||
|
def moveEvent(self, ev):
|
||||||
|
ret = QLineEdit.moveEvent(self, ev)
|
||||||
|
QTimer.singleShot(0, self.position_complete_window)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def resizeEvent(self, ev):
|
||||||
|
ret = QLineEdit.resizeEvent(self, ev)
|
||||||
|
QTimer.singleShot(0, self.position_complete_window)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def all_items(self):
|
||||||
|
def fget(self):
|
||||||
|
return self._model.items
|
||||||
|
def fset(self, items):
|
||||||
|
self._model.set_items(items)
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def sep(self):
|
||||||
|
def fget(self):
|
||||||
|
return self._model.sep
|
||||||
|
def fset(self, val):
|
||||||
|
self._model.sep = val
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def space_before_sep(self):
|
||||||
|
def fget(self):
|
||||||
|
return self._model.space_before_sep
|
||||||
|
def fset(self, val):
|
||||||
|
self._model.space_before_sep = val
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
class MultiCompleteComboBox(EnComboBox):
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
EnComboBox.__init__(self, *args)
|
||||||
|
self.setLineEdit(MultiCompleteLineEdit(self))
|
||||||
|
# Needed to allow changing the case of an existing item
|
||||||
|
# otherwise on focus out, the text is changed to the
|
||||||
|
# item that matches case insensitively
|
||||||
|
c = self.lineEdit().completer()
|
||||||
|
c.setCaseSensitivity(Qt.CaseSensitive)
|
||||||
|
|
||||||
|
def update_items_cache(self, complete_items):
|
||||||
|
self.lineEdit().update_items_cache(complete_items)
|
||||||
|
|
||||||
|
def set_separator(self, sep):
|
||||||
|
self.lineEdit().set_separator(sep)
|
||||||
|
|
||||||
|
def set_space_before_sep(self, space_before):
|
||||||
|
self.lineEdit().set_space_before_sep(space_before)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QDialog, QVBoxLayout
|
||||||
|
app = QApplication([])
|
||||||
|
d = QDialog()
|
||||||
|
d.setLayout(QVBoxLayout())
|
||||||
|
le = MultiCompleteLineEdit(d)
|
||||||
|
d.layout().addWidget(le)
|
||||||
|
le.all_items = ['one', 'otwo', 'othree', 'ooone', 'ootwo', 'oothree']
|
||||||
|
d.exec_()
|
@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
from PyQt4.Qt import Qt
|
from PyQt4.Qt import Qt
|
||||||
|
|
||||||
|
from calibre.gui2 import gprefs
|
||||||
from calibre.gui2.convert.heuristics_ui import Ui_Form
|
from calibre.gui2.convert.heuristics_ui import Ui_Form
|
||||||
from calibre.gui2.convert import Widget
|
from calibre.gui2.convert import Widget
|
||||||
|
|
||||||
@ -21,17 +22,38 @@ class HeuristicsWidget(Widget, Ui_Form):
|
|||||||
['enable_heuristics', 'markup_chapter_headings',
|
['enable_heuristics', 'markup_chapter_headings',
|
||||||
'italicize_common_cases', 'fix_indents',
|
'italicize_common_cases', 'fix_indents',
|
||||||
'html_unwrap_factor', 'unwrap_lines',
|
'html_unwrap_factor', 'unwrap_lines',
|
||||||
'delete_blank_paragraphs', 'format_scene_breaks',
|
'delete_blank_paragraphs',
|
||||||
|
'format_scene_breaks', 'replace_scene_breaks',
|
||||||
'dehyphenate', 'renumber_headings']
|
'dehyphenate', 'renumber_headings']
|
||||||
)
|
)
|
||||||
self.db, self.book_id = db, book_id
|
self.db, self.book_id = db, book_id
|
||||||
|
self.rssb_defaults = [u'', u'<hr />', u'∗ ∗ ∗', u'• • •', u'♦ ♦ ♦',
|
||||||
|
u'† †', u'‡ ‡ ‡', u'∞ ∞ ∞', u'¤ ¤ ¤', u'§']
|
||||||
self.initialize_options(get_option, get_help, db, book_id)
|
self.initialize_options(get_option, get_help, db, book_id)
|
||||||
|
|
||||||
|
self.load_histories()
|
||||||
|
|
||||||
self.opt_enable_heuristics.stateChanged.connect(self.enable_heuristics)
|
self.opt_enable_heuristics.stateChanged.connect(self.enable_heuristics)
|
||||||
self.opt_unwrap_lines.stateChanged.connect(self.enable_unwrap)
|
self.opt_unwrap_lines.stateChanged.connect(self.enable_unwrap)
|
||||||
|
|
||||||
self.enable_heuristics(self.opt_enable_heuristics.checkState())
|
self.enable_heuristics(self.opt_enable_heuristics.checkState())
|
||||||
|
|
||||||
|
def restore_defaults(self, get_option):
|
||||||
|
Widget.restore_defaults(self, get_option)
|
||||||
|
|
||||||
|
self.save_histories()
|
||||||
|
rssb_hist = gprefs['replace_scene_breaks_history']
|
||||||
|
for x in self.rssb_defaults:
|
||||||
|
if x in rssb_hist:
|
||||||
|
del rssb_hist[rssb_hist.index(x)]
|
||||||
|
gprefs['replace_scene_breaks_history'] = self.rssb_defaults + gprefs['replace_scene_breaks_history']
|
||||||
|
self.load_histories()
|
||||||
|
|
||||||
|
def commit_options(self, save_defaults=False):
|
||||||
|
self.save_histories()
|
||||||
|
|
||||||
|
return Widget.commit_options(self, save_defaults)
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
Widget.break_cycles(self)
|
Widget.break_cycles(self)
|
||||||
|
|
||||||
@ -45,6 +67,33 @@ class HeuristicsWidget(Widget, Ui_Form):
|
|||||||
if val is None and g is self.opt_html_unwrap_factor:
|
if val is None and g is self.opt_html_unwrap_factor:
|
||||||
g.setValue(0.0)
|
g.setValue(0.0)
|
||||||
return True
|
return True
|
||||||
|
if not val and g is self.opt_replace_scene_breaks:
|
||||||
|
g.lineEdit().setText('')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def load_histories(self):
|
||||||
|
self.opt_replace_scene_breaks.clear()
|
||||||
|
self.opt_replace_scene_breaks.lineEdit().setText('')
|
||||||
|
|
||||||
|
val = unicode(self.opt_replace_scene_breaks.currentText())
|
||||||
|
rssb_hist = gprefs.get('replace_scene_breaks_history', self.rssb_defaults)
|
||||||
|
if val in rssb_hist:
|
||||||
|
del rssb_hist[rssb_hist.index(val)]
|
||||||
|
rssb_hist.insert(0, val)
|
||||||
|
for v in rssb_hist:
|
||||||
|
# Ensure we don't have duplicate items.
|
||||||
|
if self.opt_replace_scene_breaks.findText(v) == -1:
|
||||||
|
self.opt_replace_scene_breaks.addItem(v)
|
||||||
|
self.opt_replace_scene_breaks.setCurrentIndex(0)
|
||||||
|
|
||||||
|
def save_histories(self):
|
||||||
|
rssb_history = []
|
||||||
|
history_pats = [unicode(self.opt_replace_scene_breaks.lineEdit().text())] + [unicode(self.opt_replace_scene_breaks.itemText(i)) for i in xrange(self.opt_replace_scene_breaks.count())]
|
||||||
|
for p in history_pats[:10]:
|
||||||
|
# Ensure we don't have duplicate items.
|
||||||
|
if p not in rssb_history:
|
||||||
|
rssb_history.append(p)
|
||||||
|
gprefs['replace_scene_breaks_history'] = rssb_history
|
||||||
|
|
||||||
def enable_heuristics(self, state):
|
def enable_heuristics(self, state):
|
||||||
state = state == Qt.Checked
|
state = state == Qt.Checked
|
||||||
|
@ -150,6 +150,45 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="sizeConstraint">
|
||||||
|
<enum>QLayout::SetDefaultConstraint</enum>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Replace soft scene &breaks:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_replace_scene_breaks</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="opt_replace_scene_breaks">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="editable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="insertPolicy">
|
||||||
|
<enum>QComboBox::InsertAtTop</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="opt_dehyphenate">
|
<widget class="QCheckBox" name="opt_dehyphenate">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -70,9 +70,6 @@ class MetadataWidget(Widget, Ui_Form):
|
|||||||
def initialize_metadata_options(self):
|
def initialize_metadata_options(self):
|
||||||
self.initialize_combos()
|
self.initialize_combos()
|
||||||
self.author.editTextChanged.connect(self.deduce_author_sort)
|
self.author.editTextChanged.connect(self.deduce_author_sort)
|
||||||
self.author.set_separator('&')
|
|
||||||
self.author.set_space_before_sep(True)
|
|
||||||
self.author.update_items_cache(self.db.all_author_names())
|
|
||||||
|
|
||||||
mi = self.db.get_metadata(self.book_id, index_is_id=True)
|
mi = self.db.get_metadata(self.book_id, index_is_id=True)
|
||||||
self.title.setText(mi.title)
|
self.title.setText(mi.title)
|
||||||
@ -109,6 +106,9 @@ class MetadataWidget(Widget, Ui_Form):
|
|||||||
def initalize_authors(self):
|
def initalize_authors(self):
|
||||||
all_authors = self.db.all_authors()
|
all_authors = self.db.all_authors()
|
||||||
all_authors.sort(key=lambda x : sort_key(x[1]))
|
all_authors.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.author.set_separator('&')
|
||||||
|
self.author.set_space_before_sep(True)
|
||||||
|
self.author.update_items_cache(self.db.all_author_names())
|
||||||
|
|
||||||
for i in all_authors:
|
for i in all_authors:
|
||||||
id, name = i
|
id, name = i
|
||||||
@ -124,6 +124,8 @@ class MetadataWidget(Widget, Ui_Form):
|
|||||||
def initialize_series(self):
|
def initialize_series(self):
|
||||||
all_series = self.db.all_series()
|
all_series = self.db.all_series()
|
||||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
all_series.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.series.set_separator(None)
|
||||||
|
self.series.update_items_cache([x[1] for x in all_series])
|
||||||
|
|
||||||
for i in all_series:
|
for i in all_series:
|
||||||
id, name = i
|
id, name = i
|
||||||
@ -133,6 +135,8 @@ class MetadataWidget(Widget, Ui_Form):
|
|||||||
def initialize_publisher(self):
|
def initialize_publisher(self):
|
||||||
all_publishers = self.db.all_publishers()
|
all_publishers = self.db.all_publishers()
|
||||||
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.publisher.set_separator(None)
|
||||||
|
self.publisher.update_items_cache([x[1] for x in all_publishers])
|
||||||
|
|
||||||
for i in all_publishers:
|
for i in all_publishers:
|
||||||
id, name = i
|
id, name = i
|
||||||
|
@ -190,7 +190,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="CompleteLineEdit" name="tags">
|
<widget class="MultiCompleteLineEdit" name="tags">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.</string>
|
<string>Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.</string>
|
||||||
</property>
|
</property>
|
||||||
@ -213,7 +213,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="EnComboBox" name="series">
|
<widget class="MultiCompleteComboBox" name="series">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||||
<horstretch>10</horstretch>
|
<horstretch>10</horstretch>
|
||||||
@ -248,14 +248,14 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="EnComboBox" name="publisher">
|
<widget class="MultiCompleteComboBox" name="publisher">
|
||||||
<property name="editable">
|
<property name="editable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="CompleteComboBox" name="author">
|
<widget class="MultiCompleteComboBox" name="author">
|
||||||
<property name="editable">
|
<property name="editable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -277,19 +277,14 @@
|
|||||||
<header>widgets.h</header>
|
<header>widgets.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>EnComboBox</class>
|
<class>MultiCompleteComboBox</class>
|
||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
<header>widgets.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>CompleteComboBox</class>
|
<class>MultiCompleteLineEdit</class>
|
||||||
<extends>QComboBox</extends>
|
|
||||||
<header>widgets.h</header>
|
|
||||||
</customwidget>
|
|
||||||
<customwidget>
|
|
||||||
<class>CompleteLineEdit</class>
|
|
||||||
<extends>QLineEdit</extends>
|
<extends>QLineEdit</extends>
|
||||||
<header>widgets.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>ImageView</class>
|
<class>ImageView</class>
|
||||||
|
@ -48,10 +48,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0" colspan="3">
|
<item row="6" column="0" colspan="3">
|
||||||
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0" colspan="3">
|
<item row="7" column="0" colspan="3">
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
@ -77,6 +77,16 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="4" column="0" colspan="3">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
@ -14,7 +14,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
|
|||||||
QPushButton
|
QPushButton
|
||||||
|
|
||||||
from calibre.utils.date import qt_to_dt, now
|
from calibre.utils.date import qt_to_dt, now
|
||||||
from calibre.gui2.widgets import CompleteLineEdit, EnComboBox
|
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
|
||||||
from calibre.gui2.comments_editor import Editor as CommentsEditor
|
from calibre.gui2.comments_editor import Editor as CommentsEditor
|
||||||
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
|
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
@ -228,10 +228,12 @@ class Text(Base):
|
|||||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
values.sort(key=sort_key)
|
values.sort(key=sort_key)
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
w = CompleteLineEdit(parent, values)
|
w = MultiCompleteLineEdit(parent)
|
||||||
|
w.update_items_cache(values)
|
||||||
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||||
else:
|
else:
|
||||||
w = EnComboBox(parent)
|
w = MultiCompleteComboBox(parent)
|
||||||
|
w.set_separator(None)
|
||||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||||
w.setMinimumContentsLength(25)
|
w.setMinimumContentsLength(25)
|
||||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
||||||
@ -240,9 +242,10 @@ class Text(Base):
|
|||||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||||
self.initial_val = val
|
self.initial_val = val
|
||||||
val = self.normalize_db_val(val)
|
val = self.normalize_db_val(val)
|
||||||
|
self.widgets[1].update_items_cache(self.all_values)
|
||||||
|
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
self.setter(val)
|
self.setter(val)
|
||||||
self.widgets[1].update_items_cache(self.all_values)
|
|
||||||
else:
|
else:
|
||||||
idx = None
|
idx = None
|
||||||
for i, c in enumerate(self.all_values):
|
for i, c in enumerate(self.all_values):
|
||||||
@ -276,7 +279,7 @@ class Series(Base):
|
|||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
values.sort(key=sort_key)
|
values.sort(key=sort_key)
|
||||||
w = EnComboBox(parent)
|
w = MultiCompleteComboBox(parent)
|
||||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||||
w.setMinimumContentsLength(25)
|
w.setMinimumContentsLength(25)
|
||||||
self.name_widget = w
|
self.name_widget = w
|
||||||
@ -305,6 +308,7 @@ class Series(Base):
|
|||||||
if c == val:
|
if c == val:
|
||||||
idx = i
|
idx = i
|
||||||
self.name_widget.addItem(c)
|
self.name_widget.addItem(c)
|
||||||
|
self.name_widget.update_items_cache(self.all_values)
|
||||||
self.name_widget.setEditText('')
|
self.name_widget.setEditText('')
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
self.widgets[1].setCurrentIndex(idx)
|
self.widgets[1].setCurrentIndex(idx)
|
||||||
@ -670,7 +674,7 @@ class BulkDateTime(BulkBase):
|
|||||||
class BulkSeries(BulkBase):
|
class BulkSeries(BulkBase):
|
||||||
|
|
||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
self.make_widgets(parent, EnComboBox)
|
self.make_widgets(parent, MultiCompleteComboBox)
|
||||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
values.sort(key=sort_key)
|
values.sort(key=sort_key)
|
||||||
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
||||||
@ -705,6 +709,8 @@ class BulkSeries(BulkBase):
|
|||||||
|
|
||||||
def initialize(self, book_id):
|
def initialize(self, book_id):
|
||||||
self.idx_widget.setChecked(False)
|
self.idx_widget.setChecked(False)
|
||||||
|
self.main_widget.set_separator(None)
|
||||||
|
self.main_widget.update_items_cache(self.all_values)
|
||||||
for c in self.all_values:
|
for c in self.all_values:
|
||||||
self.main_widget.addItem(c)
|
self.main_widget.addItem(c)
|
||||||
self.main_widget.setEditText('')
|
self.main_widget.setEditText('')
|
||||||
@ -795,7 +801,8 @@ class RemoveTags(QWidget):
|
|||||||
layout.setSpacing(5)
|
layout.setSpacing(5)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.tags_box = CompleteLineEdit(parent, values)
|
self.tags_box = MultiCompleteLineEdit(parent)
|
||||||
|
self.tags_box.update_items_cache(values)
|
||||||
layout.addWidget(self.tags_box, stretch=3)
|
layout.addWidget(self.tags_box, stretch=3)
|
||||||
self.checkbox = QCheckBox(_('Remove all tags'), parent)
|
self.checkbox = QCheckBox(_('Remove all tags'), parent)
|
||||||
layout.addWidget(self.checkbox)
|
layout.addWidget(self.checkbox)
|
||||||
@ -816,7 +823,7 @@ class BulkText(BulkBase):
|
|||||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
values.sort(key=sort_key)
|
values.sort(key=sort_key)
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
self.make_widgets(parent, CompleteLineEdit,
|
self.make_widgets(parent, MultiCompleteLineEdit,
|
||||||
extra_label_text=_('tags to add'))
|
extra_label_text=_('tags to add'))
|
||||||
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||||
self.adding_widget = self.main_widget
|
self.adding_widget = self.main_widget
|
||||||
@ -829,16 +836,16 @@ class BulkText(BulkBase):
|
|||||||
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
|
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
|
||||||
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
|
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
|
||||||
else:
|
else:
|
||||||
self.make_widgets(parent, EnComboBox)
|
self.make_widgets(parent, MultiCompleteComboBox)
|
||||||
|
self.main_widget.set_separator(None)
|
||||||
self.main_widget.setSizeAdjustPolicy(
|
self.main_widget.setSizeAdjustPolicy(
|
||||||
self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
||||||
self.main_widget.setMinimumContentsLength(25)
|
self.main_widget.setMinimumContentsLength(25)
|
||||||
self.ignore_change_signals = False
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
def initialize(self, book_ids):
|
def initialize(self, book_ids):
|
||||||
if self.col_metadata['is_multiple']:
|
self.main_widget.update_items_cache(self.all_values)
|
||||||
self.main_widget.update_items_cache(self.all_values)
|
if not self.col_metadata['is_multiple']:
|
||||||
else:
|
|
||||||
val = self.get_initial_value(book_ids)
|
val = self.get_initial_value(book_ids)
|
||||||
self.initial_val = val = self.normalize_db_val(val)
|
self.initial_val = val = self.normalize_db_val(val)
|
||||||
idx = None
|
idx = None
|
||||||
|
@ -838,9 +838,10 @@ class DeviceMixin(object): # {{{
|
|||||||
format_count[f] = 1
|
format_count[f] = 1
|
||||||
for f in self.device_manager.device.settings().format_map:
|
for f in self.device_manager.device.settings().format_map:
|
||||||
if f in format_count.keys():
|
if f in format_count.keys():
|
||||||
formats.append((f, _('%i of %i Books') % (format_count[f], len(rows))), True if f in aval_out_formats else False)
|
formats.append((f, _('%i of %i Books') % (format_count[f],
|
||||||
|
len(rows)), True if f in aval_out_formats else False))
|
||||||
elif f in aval_out_formats:
|
elif f in aval_out_formats:
|
||||||
formats.append((f, _('0 of %i Books') % len(rows)), True)
|
formats.append((f, _('0 of %i Books') % len(rows), True))
|
||||||
d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats)
|
d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats)
|
||||||
if d.exec_() != QDialog.Accepted:
|
if d.exec_() != QDialog.Accepted:
|
||||||
return
|
return
|
||||||
@ -871,6 +872,16 @@ class DeviceMixin(object): # {{{
|
|||||||
self.send_by_mail(to, fmts, delete)
|
self.send_by_mail(to, fmts, delete)
|
||||||
|
|
||||||
def cover_to_thumbnail(self, data):
|
def cover_to_thumbnail(self, data):
|
||||||
|
if self.device_manager.device and \
|
||||||
|
hasattr(self.device_manager.device, 'THUMBNAIL_WIDTH'):
|
||||||
|
try:
|
||||||
|
return thumbnail(data,
|
||||||
|
self.device_manager.device.THUMBNAIL_WIDTH,
|
||||||
|
self.device_manager.device.THUMBNAIL_HEIGHT,
|
||||||
|
preserve_aspect_ratio=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return
|
||||||
ht = self.device_manager.device.THUMBNAIL_HEIGHT \
|
ht = self.device_manager.device.THUMBNAIL_HEIGHT \
|
||||||
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
|
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
|
||||||
try:
|
try:
|
||||||
@ -1272,6 +1283,8 @@ class DeviceMixin(object): # {{{
|
|||||||
x = x.lower() if x else ''
|
x = x.lower() if x else ''
|
||||||
return string_pat.sub('', x)
|
return string_pat.sub('', x)
|
||||||
|
|
||||||
|
update_metadata = prefs['manage_device_metadata'] == 'on_connect'
|
||||||
|
|
||||||
# Force a reset if the caches are not initialized
|
# Force a reset if the caches are not initialized
|
||||||
if reset or not hasattr(self, 'db_book_title_cache'):
|
if reset or not hasattr(self, 'db_book_title_cache'):
|
||||||
# Build a cache (map) of the library, so the search isn't On**2
|
# Build a cache (map) of the library, so the search isn't On**2
|
||||||
@ -1284,8 +1297,13 @@ class DeviceMixin(object): # {{{
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
get_covers = False
|
||||||
|
if update_metadata and self.device_manager.is_device_connected:
|
||||||
|
if self.device_manager.device.WANTS_UPDATED_THUMBNAILS:
|
||||||
|
get_covers = True
|
||||||
|
|
||||||
for id in db.data.iterallids():
|
for id in db.data.iterallids():
|
||||||
mi = db.get_metadata(id, index_is_id=True)
|
mi = db.get_metadata(id, index_is_id=True, get_cover=get_covers)
|
||||||
title = clean_string(mi.title)
|
title = clean_string(mi.title)
|
||||||
if title not in db_book_title_cache:
|
if title not in db_book_title_cache:
|
||||||
db_book_title_cache[title] = \
|
db_book_title_cache[title] = \
|
||||||
@ -1311,7 +1329,6 @@ class DeviceMixin(object): # {{{
|
|||||||
# the application_id to the db_id of the matching book. This value
|
# the application_id to the db_id of the matching book. This value
|
||||||
# will be used by books_on_device to indicate matches.
|
# will be used by books_on_device to indicate matches.
|
||||||
|
|
||||||
update_metadata = prefs['manage_device_metadata'] == 'on_connect'
|
|
||||||
for booklist in booklists:
|
for booklist in booklists:
|
||||||
for book in booklist:
|
for book in booklist:
|
||||||
book.in_library = None
|
book.in_library = None
|
||||||
@ -1382,6 +1399,12 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
if self.device_manager.is_device_connected:
|
if self.device_manager.is_device_connected:
|
||||||
|
if self.device_manager.device.WANTS_UPDATED_THUMBNAILS:
|
||||||
|
for blist in booklists:
|
||||||
|
for book in blist:
|
||||||
|
if book.cover and os.access(book.cover, os.R_OK):
|
||||||
|
book.thumbnail = \
|
||||||
|
self.cover_to_thumbnail(open(book.cover, 'rb').read())
|
||||||
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
||||||
self.device_manager.sync_booklists(
|
self.device_manager.sync_booklists(
|
||||||
Dispatcher(self.metadata_synced), booklists,
|
Dispatcher(self.metadata_synced), booklists,
|
||||||
|
@ -7,8 +7,8 @@ __license__ = 'GPL v3'
|
|||||||
from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
|
from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
|
||||||
QApplication, QSpinBox, QToolButton, QIcon
|
QApplication, QSpinBox, QToolButton, QIcon
|
||||||
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
||||||
from calibre.gui2.widgets import CompleteComboBox
|
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
|
from calibre.gui2.complete import MultiCompleteComboBox
|
||||||
|
|
||||||
class AddEmptyBookDialog(QDialog):
|
class AddEmptyBookDialog(QDialog):
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ class AddEmptyBookDialog(QDialog):
|
|||||||
self.author_label = QLabel(_('Set the author of the new books to:'))
|
self.author_label = QLabel(_('Set the author of the new books to:'))
|
||||||
self._layout.addWidget(self.author_label, 2, 0, 1, 2)
|
self._layout.addWidget(self.author_label, 2, 0, 1, 2)
|
||||||
|
|
||||||
self.authors_combo = CompleteComboBox(self)
|
self.authors_combo = MultiCompleteComboBox(self)
|
||||||
self.authors_combo.setSizeAdjustPolicy(
|
self.authors_combo.setSizeAdjustPolicy(
|
||||||
self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
|
self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
|
||||||
self.authors_combo.setEditable(True)
|
self.authors_combo.setEditable(True)
|
||||||
|
@ -764,6 +764,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
def initialize_series(self):
|
def initialize_series(self):
|
||||||
all_series = self.db.all_series()
|
all_series = self.db.all_series()
|
||||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
all_series.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.series.set_separator(None)
|
||||||
|
self.series.update_items_cache([x[1] for x in all_series])
|
||||||
|
|
||||||
for i in all_series:
|
for i in all_series:
|
||||||
id, name = i
|
id, name = i
|
||||||
@ -773,6 +775,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
def initialize_publisher(self):
|
def initialize_publisher(self):
|
||||||
all_publishers = self.db.all_publishers()
|
all_publishers = self.db.all_publishers()
|
||||||
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.publisher.set_separator(None)
|
||||||
|
self.publisher.update_items_cache([x[1] for x in all_publishers])
|
||||||
|
|
||||||
for i in all_publishers:
|
for i in all_publishers:
|
||||||
id, name = i
|
id, name = i
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="CompleteComboBox" name="authors">
|
<widget class="MultiCompleteComboBox" name="authors">
|
||||||
<property name="editable">
|
<property name="editable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -175,7 +175,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="EnComboBox" name="publisher">
|
<widget class="MultiCompleteComboBox" name="publisher">
|
||||||
<property name="editable">
|
<property name="editable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -195,7 +195,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="CompleteLineEdit" name="tags">
|
<widget class="MultiCompleteLineEdit" name="tags">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.</string>
|
<string>Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.</string>
|
||||||
</property>
|
</property>
|
||||||
@ -229,7 +229,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="CompleteLineEdit" name="remove_tags">
|
<widget class="MultiCompleteLineEdit" name="remove_tags">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Comma separated list of tags to remove from the books. </string>
|
<string>Comma separated list of tags to remove from the books. </string>
|
||||||
</property>
|
</property>
|
||||||
@ -262,7 +262,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="7" column="1">
|
||||||
<widget class="EnComboBox" name="series">
|
<widget class="MultiCompleteComboBox" name="series">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
@ -1072,19 +1072,14 @@ not multiple and the destination field is multiple</string>
|
|||||||
<header>widgets.h</header>
|
<header>widgets.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>EnComboBox</class>
|
<class>MultiCompleteComboBox</class>
|
||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
<header>widgets.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>CompleteComboBox</class>
|
<class>MultiCompleteLineEdit</class>
|
||||||
<extends>QComboBox</extends>
|
|
||||||
<header>widgets.h</header>
|
|
||||||
</customwidget>
|
|
||||||
<customwidget>
|
|
||||||
<class>CompleteLineEdit</class>
|
|
||||||
<extends>QLineEdit</extends>
|
<extends>QLineEdit</extends>
|
||||||
<header>widgets.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>HistoryLineEdit</class>
|
<class>HistoryLineEdit</class>
|
||||||
|
@ -429,10 +429,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
old_extensions.add(ext)
|
old_extensions.add(ext)
|
||||||
for ext in new_extensions:
|
for ext in new_extensions:
|
||||||
self.db.add_format(self.row, ext, open(paths[ext], 'rb'), notify=False)
|
self.db.add_format(self.row, ext, open(paths[ext], 'rb'), notify=False)
|
||||||
db_extensions = set([f.lower() for f in self.db.formats(self.row).split(',')])
|
dbfmts = self.db.formats(self.row)
|
||||||
|
db_extensions = set([f.lower() for f in (dbfmts.split(',') if dbfmts
|
||||||
|
else [])])
|
||||||
extensions = new_extensions.union(old_extensions)
|
extensions = new_extensions.union(old_extensions)
|
||||||
for ext in db_extensions:
|
for ext in db_extensions:
|
||||||
if ext not in extensions:
|
if ext not in extensions and ext in self.original_formats:
|
||||||
self.db.remove_format(self.row, ext, notify=False)
|
self.db.remove_format(self.row, ext, notify=False)
|
||||||
|
|
||||||
def show_format(self, item, *args):
|
def show_format(self, item, *args):
|
||||||
@ -576,6 +578,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.orig_date = qt_to_dt(self.date.date())
|
self.orig_date = qt_to_dt(self.date.date())
|
||||||
|
|
||||||
exts = self.db.formats(row)
|
exts = self.db.formats(row)
|
||||||
|
self.original_formats = []
|
||||||
if exts:
|
if exts:
|
||||||
exts = exts.split(',')
|
exts = exts.split(',')
|
||||||
for ext in exts:
|
for ext in exts:
|
||||||
@ -586,6 +589,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
if size is None:
|
if size is None:
|
||||||
continue
|
continue
|
||||||
Format(self.formats, ext, size, timestamp=timestamp)
|
Format(self.formats, ext, size, timestamp=timestamp)
|
||||||
|
self.original_formats.append(ext.lower())
|
||||||
|
|
||||||
|
|
||||||
self.initialize_combos()
|
self.initialize_combos()
|
||||||
@ -735,6 +739,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
|
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
|
||||||
all_series = self.db.all_series()
|
all_series = self.db.all_series()
|
||||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
all_series.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.series.set_separator(None)
|
||||||
|
self.series.update_items_cache([x[1] for x in all_series])
|
||||||
series_id = self.db.series_id(self.row)
|
series_id = self.db.series_id(self.row)
|
||||||
idx, c = None, 0
|
idx, c = None, 0
|
||||||
for i in all_series:
|
for i in all_series:
|
||||||
@ -752,6 +758,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
def initialize_publisher(self):
|
def initialize_publisher(self):
|
||||||
all_publishers = self.db.all_publishers()
|
all_publishers = self.db.all_publishers()
|
||||||
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.publisher.set_separator(None)
|
||||||
|
self.publisher.update_items_cache([x[1] for x in all_publishers])
|
||||||
publisher_id = self.db.publisher_id(self.row)
|
publisher_id = self.db.publisher_id(self.row)
|
||||||
idx, c = None, 0
|
idx, c = None, 0
|
||||||
for i in all_publishers:
|
for i in all_publishers:
|
||||||
|
@ -240,7 +240,7 @@ Using this button to create author sort will change author sort from red to gree
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="CompleteComboBox" name="authors">
|
<widget class="MultiCompleteComboBox" name="authors">
|
||||||
<property name="editable">
|
<property name="editable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -313,7 +313,7 @@ If the box is colored green, then text matches the individual author's sort stri
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1" colspan="2">
|
<item row="5" column="1" colspan="2">
|
||||||
<widget class="EnComboBox" name="publisher">
|
<widget class="MultiCompleteComboBox" name="publisher">
|
||||||
<property name="editable">
|
<property name="editable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -335,7 +335,7 @@ If the box is colored green, then text matches the individual author's sort stri
|
|||||||
<item row="6" column="1">
|
<item row="6" column="1">
|
||||||
<layout class="QHBoxLayout" name="_2">
|
<layout class="QHBoxLayout" name="_2">
|
||||||
<item>
|
<item>
|
||||||
<widget class="CompleteLineEdit" name="tags">
|
<widget class="MultiCompleteLineEdit" name="tags">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.</string>
|
<string>Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.</string>
|
||||||
</property>
|
</property>
|
||||||
@ -379,7 +379,7 @@ If the box is colored green, then text matches the individual author's sort stri
|
|||||||
<number>5</number>
|
<number>5</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="EnComboBox" name="series">
|
<widget class="MultiCompleteComboBox" name="series">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>List of known series. You can add new series.</string>
|
<string>List of known series. You can add new series.</string>
|
||||||
</property>
|
</property>
|
||||||
@ -837,19 +837,14 @@ If the box is colored green, then text matches the individual author's sort stri
|
|||||||
<header>widgets.h</header>
|
<header>widgets.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>EnComboBox</class>
|
<class>MultiCompleteLineEdit</class>
|
||||||
<extends>QComboBox</extends>
|
|
||||||
<header>widgets.h</header>
|
|
||||||
</customwidget>
|
|
||||||
<customwidget>
|
|
||||||
<class>CompleteLineEdit</class>
|
|
||||||
<extends>QLineEdit</extends>
|
<extends>QLineEdit</extends>
|
||||||
<header>widgets.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>CompleteComboBox</class>
|
<class>MultiCompleteComboBox</class>
|
||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
<header>widgets.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>FormatList</class>
|
<class>FormatList</class>
|
||||||
|
@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import re, copy
|
import re, copy
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, QDialogButtonBox, QCompleter, Qt
|
from PyQt4.Qt import QDialog, QDialogButtonBox
|
||||||
|
|
||||||
from calibre.gui2.dialogs.search_ui import Ui_Dialog
|
from calibre.gui2.dialogs.search_ui import Ui_Dialog
|
||||||
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
|
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
|
||||||
@ -29,20 +29,18 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
name = name.strip().replace('|', ',')
|
name = name.strip().replace('|', ',')
|
||||||
self.authors_box.addItem(name)
|
self.authors_box.addItem(name)
|
||||||
self.authors_box.setEditText('')
|
self.authors_box.setEditText('')
|
||||||
self.authors_box.completer().setCompletionMode(QCompleter.PopupCompletion)
|
|
||||||
self.authors_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
|
|
||||||
self.authors_box.set_separator('&')
|
self.authors_box.set_separator('&')
|
||||||
self.authors_box.set_space_before_sep(True)
|
self.authors_box.set_space_before_sep(True)
|
||||||
self.authors_box.update_items_cache(db.all_author_names())
|
self.authors_box.update_items_cache(db.all_author_names())
|
||||||
|
|
||||||
all_series = db.all_series()
|
all_series = db.all_series()
|
||||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
all_series.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.series_box.set_separator(None)
|
||||||
|
self.series_box.update_items_cache([x[1] for x in all_series])
|
||||||
for i in all_series:
|
for i in all_series:
|
||||||
id, name = i
|
id, name = i
|
||||||
self.series_box.addItem(name)
|
self.series_box.addItem(name)
|
||||||
self.series_box.setEditText('')
|
self.series_box.setEditText('')
|
||||||
self.series_box.completer().setCompletionMode(QCompleter.PopupCompletion)
|
|
||||||
self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
|
|
||||||
|
|
||||||
all_tags = db.all_tags()
|
all_tags = db.all_tags()
|
||||||
self.tags_box.update_items_cache(all_tags)
|
self.tags_box.update_items_cache(all_tags)
|
||||||
|
@ -265,21 +265,21 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="CompleteComboBox" name="authors_box">
|
<widget class="MultiCompleteComboBox" name="authors_box">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Enter an author's name. Only one author can be used.</string>
|
<string>Enter an author's name. Only one author can be used.</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="EnComboBox" name="series_box">
|
<widget class="MultiCompleteComboBox" name="series_box">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Enter a series name, without an index. Only one series name can be used.</string>
|
<string>Enter a series name, without an index. Only one series name can be used.</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="CompleteLineEdit" name="tags_box">
|
<widget class="MultiCompleteLineEdit" name="tags_box">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Enter tags separated by spaces</string>
|
<string>Enter tags separated by spaces</string>
|
||||||
</property>
|
</property>
|
||||||
@ -355,19 +355,14 @@
|
|||||||
<header>widgets.h</header>
|
<header>widgets.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>EnComboBox</class>
|
<class>MultiCompleteLineEdit</class>
|
||||||
<extends>QComboBox</extends>
|
|
||||||
<header>widgets.h</header>
|
|
||||||
</customwidget>
|
|
||||||
<customwidget>
|
|
||||||
<class>CompleteLineEdit</class>
|
|
||||||
<extends>QLineEdit</extends>
|
<extends>QLineEdit</extends>
|
||||||
<header>widgets.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>CompleteComboBox</class>
|
<class>MultiCompleteComboBox</class>
|
||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
<header>widgets.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
|
@ -264,8 +264,9 @@ class EmailMixin(object): # {{{
|
|||||||
if _auto_ids != []:
|
if _auto_ids != []:
|
||||||
for id in _auto_ids:
|
for id in _auto_ids:
|
||||||
if specific_format == None:
|
if specific_format == None:
|
||||||
formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')]
|
dbfmts = self.library_view.model().db.formats(id, index_is_id=True)
|
||||||
formats = formats if formats != None else []
|
formats = [f.lower() for f in (dbfmts.split(',') if fmts else
|
||||||
|
[])]
|
||||||
if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []:
|
if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []:
|
||||||
auto.append(id)
|
auto.append(id)
|
||||||
else:
|
else:
|
||||||
|
@ -12,11 +12,11 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \
|
|||||||
QPainterPath, QLinearGradient, QBrush, \
|
QPainterPath, QLinearGradient, QBrush, \
|
||||||
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
|
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
|
||||||
QIcon, QDoubleSpinBox, QVariant, QSpinBox, \
|
QIcon, QDoubleSpinBox, QVariant, QSpinBox, \
|
||||||
QStyledItemDelegate, QCompleter, \
|
QStyledItemDelegate, QComboBox, QTextDocument
|
||||||
QComboBox, QTextDocument
|
|
||||||
|
|
||||||
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
|
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
|
||||||
from calibre.gui2.widgets import EnLineEdit, CompleteLineEdit
|
from calibre.gui2.widgets import EnLineEdit
|
||||||
|
from calibre.gui2.complete import MultiCompleteLineEdit
|
||||||
from calibre.utils.date import now, format_date
|
from calibre.utils.date import now, format_date
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.formatter import validation_formatter
|
from calibre.utils.formatter import validation_formatter
|
||||||
@ -151,38 +151,15 @@ class TextDelegate(QStyledItemDelegate): # {{{
|
|||||||
self.auto_complete_function = f
|
self.auto_complete_function = f
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
def createEditor(self, parent, option, index):
|
||||||
editor = EnLineEdit(parent)
|
|
||||||
if self.auto_complete_function:
|
if self.auto_complete_function:
|
||||||
|
editor = MultiCompleteLineEdit(parent)
|
||||||
|
editor.set_separator(None)
|
||||||
complete_items = [i[1] for i in self.auto_complete_function()]
|
complete_items = [i[1] for i in self.auto_complete_function()]
|
||||||
completer = QCompleter(complete_items, self)
|
editor.update_items_cache(complete_items)
|
||||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
|
||||||
completer.setCompletionMode(QCompleter.PopupCompletion)
|
|
||||||
editor.setCompleter(completer)
|
|
||||||
return editor
|
|
||||||
#}}}
|
|
||||||
|
|
||||||
class TagsDelegate(QStyledItemDelegate): # {{{
|
|
||||||
def __init__(self, parent):
|
|
||||||
QStyledItemDelegate.__init__(self, parent)
|
|
||||||
self.db = None
|
|
||||||
|
|
||||||
def set_database(self, db):
|
|
||||||
self.db = db
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
if self.db:
|
|
||||||
col = index.model().column_map[index.column()]
|
|
||||||
if not index.model().is_custom_column(col):
|
|
||||||
editor = CompleteLineEdit(parent, self.db.all_tags())
|
|
||||||
else:
|
|
||||||
editor = CompleteLineEdit(parent,
|
|
||||||
sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))),
|
|
||||||
key=sort_key))
|
|
||||||
return editor
|
|
||||||
else:
|
else:
|
||||||
editor = EnLineEdit(parent)
|
editor = EnLineEdit(parent)
|
||||||
return editor
|
return editor
|
||||||
# }}}
|
#}}}
|
||||||
|
|
||||||
class CompleteDelegate(QStyledItemDelegate): # {{{
|
class CompleteDelegate(QStyledItemDelegate): # {{{
|
||||||
def __init__(self, parent, sep, items_func_name, space_before_sep=False):
|
def __init__(self, parent, sep, items_func_name, space_before_sep=False):
|
||||||
@ -197,13 +174,15 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
|
|||||||
def createEditor(self, parent, option, index):
|
def createEditor(self, parent, option, index):
|
||||||
if self.db and hasattr(self.db, self.items_func_name):
|
if self.db and hasattr(self.db, self.items_func_name):
|
||||||
col = index.model().column_map[index.column()]
|
col = index.model().column_map[index.column()]
|
||||||
|
editor = MultiCompleteLineEdit(parent)
|
||||||
|
editor.set_separator(self.sep)
|
||||||
|
editor.set_space_before_sep(self.space_before_sep)
|
||||||
if not index.model().is_custom_column(col):
|
if not index.model().is_custom_column(col):
|
||||||
editor = CompleteLineEdit(parent, getattr(self.db, self.items_func_name)(),
|
all_items = getattr(self.db, self.items_func_name)()
|
||||||
self.sep, self.space_before_sep)
|
|
||||||
else:
|
else:
|
||||||
editor = CompleteLineEdit(parent,
|
all_items = list(self.db.all_custom(
|
||||||
sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))),
|
label=self.db.field_metadata.key_to_label(col)))
|
||||||
key=sort_key), self.sep, self.space_before_sep)
|
editor.update_items_cache(all_items)
|
||||||
else:
|
else:
|
||||||
editor = EnLineEdit(parent)
|
editor = EnLineEdit(parent)
|
||||||
return editor
|
return editor
|
||||||
@ -273,13 +252,11 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
|
|||||||
editor.setRange(-100., float(sys.maxint))
|
editor.setRange(-100., float(sys.maxint))
|
||||||
editor.setDecimals(2)
|
editor.setDecimals(2)
|
||||||
else:
|
else:
|
||||||
editor = EnLineEdit(parent)
|
editor = MultiCompleteLineEdit(parent)
|
||||||
|
editor.set_separator(None)
|
||||||
complete_items = sorted(list(m.db.all_custom(label=m.db.field_metadata.key_to_label(col))),
|
complete_items = sorted(list(m.db.all_custom(label=m.db.field_metadata.key_to_label(col))),
|
||||||
key=sort_key)
|
key=sort_key)
|
||||||
completer = QCompleter(complete_items, self)
|
editor.update_items_cache(complete_items)
|
||||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
|
||||||
completer.setCompletionMode(QCompleter.PopupCompletion)
|
|
||||||
editor.setCompleter(completer)
|
|
||||||
return editor
|
return editor
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -12,8 +12,8 @@ from PyQt4.Qt import Qt, QDateEdit, QDate, \
|
|||||||
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
|
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
|
||||||
QPushButton, QSpinBox, QLineEdit
|
QPushButton, QSpinBox, QLineEdit
|
||||||
|
|
||||||
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
|
from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
|
||||||
EnComboBox, FormatList, ImageView, CompleteLineEdit
|
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.config import tweaks, prefs
|
from calibre.utils.config import tweaks, prefs
|
||||||
from calibre.ebooks.metadata import title_sort, authors_to_string, \
|
from calibre.ebooks.metadata import title_sort, authors_to_string, \
|
||||||
@ -149,14 +149,14 @@ class TitleSortEdit(TitleEdit):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Authors {{{
|
# Authors {{{
|
||||||
class AuthorsEdit(CompleteComboBox):
|
class AuthorsEdit(MultiCompleteComboBox):
|
||||||
|
|
||||||
TOOLTIP = ''
|
TOOLTIP = ''
|
||||||
LABEL = _('&Author(s):')
|
LABEL = _('&Author(s):')
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
self.dialog = parent
|
self.dialog = parent
|
||||||
CompleteComboBox.__init__(self, parent)
|
MultiCompleteComboBox.__init__(self, parent)
|
||||||
self.setToolTip(self.TOOLTIP)
|
self.setToolTip(self.TOOLTIP)
|
||||||
self.setWhatsThis(self.TOOLTIP)
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
self.setEditable(True)
|
self.setEditable(True)
|
||||||
@ -283,13 +283,14 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Series {{{
|
# Series {{{
|
||||||
class SeriesEdit(EnComboBox):
|
class SeriesEdit(MultiCompleteComboBox):
|
||||||
|
|
||||||
TOOLTIP = _('List of known series. You can add new series.')
|
TOOLTIP = _('List of known series. You can add new series.')
|
||||||
LABEL = _('&Series:')
|
LABEL = _('&Series:')
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
EnComboBox.__init__(self, parent)
|
MultiCompleteComboBox.__init__(self, parent)
|
||||||
|
self.set_separator(None)
|
||||||
self.dialog = parent
|
self.dialog = parent
|
||||||
self.setSizeAdjustPolicy(
|
self.setSizeAdjustPolicy(
|
||||||
self.AdjustToMinimumContentsLengthWithIcon)
|
self.AdjustToMinimumContentsLengthWithIcon)
|
||||||
@ -314,6 +315,7 @@ class SeriesEdit(EnComboBox):
|
|||||||
def initialize(self, db, id_):
|
def initialize(self, db, id_):
|
||||||
all_series = db.all_series()
|
all_series = db.all_series()
|
||||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
all_series.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.update_items_cache([x[1] for x in all_series])
|
||||||
series_id = db.series_id(id_, index_is_id=True)
|
series_id = db.series_id(id_, index_is_id=True)
|
||||||
idx, c = None, 0
|
idx, c = None, 0
|
||||||
for i in all_series:
|
for i in all_series:
|
||||||
@ -472,6 +474,7 @@ class FormatsManager(QWidget): # {{{
|
|||||||
def initialize(self, db, id_):
|
def initialize(self, db, id_):
|
||||||
self.changed = False
|
self.changed = False
|
||||||
exts = db.formats(id_, index_is_id=True)
|
exts = db.formats(id_, index_is_id=True)
|
||||||
|
self.original_val = set([])
|
||||||
if exts:
|
if exts:
|
||||||
exts = exts.split(',')
|
exts = exts.split(',')
|
||||||
for ext in exts:
|
for ext in exts:
|
||||||
@ -482,6 +485,7 @@ class FormatsManager(QWidget): # {{{
|
|||||||
if size is None:
|
if size is None:
|
||||||
continue
|
continue
|
||||||
Format(self.formats, ext, size, timestamp=timestamp)
|
Format(self.formats, ext, size, timestamp=timestamp)
|
||||||
|
self.original_val.add(ext.lower())
|
||||||
|
|
||||||
def commit(self, db, id_):
|
def commit(self, db, id_):
|
||||||
if not self.changed:
|
if not self.changed:
|
||||||
@ -500,11 +504,12 @@ class FormatsManager(QWidget): # {{{
|
|||||||
for ext in new_extensions:
|
for ext in new_extensions:
|
||||||
db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
|
db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
|
||||||
index_is_id=True)
|
index_is_id=True)
|
||||||
db_extensions = set([f.lower() for f in db.formats(id_,
|
dbfmts = db.formats(id_, index_is_id=True)
|
||||||
index_is_id=True).split(',')])
|
db_extensions = set([f.lower() for f in (dbfmts.split(',') if dbfmts
|
||||||
|
else [])])
|
||||||
extensions = new_extensions.union(old_extensions)
|
extensions = new_extensions.union(old_extensions)
|
||||||
for ext in db_extensions:
|
for ext in db_extensions:
|
||||||
if ext not in extensions:
|
if ext not in extensions and ext in self.original_val:
|
||||||
db.remove_format(id_, ext, notify=False, index_is_id=True)
|
db.remove_format(id_, ext, notify=False, index_is_id=True)
|
||||||
|
|
||||||
self.changed = False
|
self.changed = False
|
||||||
@ -811,14 +816,14 @@ class RatingEdit(QSpinBox): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class TagsEdit(CompleteLineEdit): # {{{
|
class TagsEdit(MultiCompleteLineEdit): # {{{
|
||||||
LABEL = _('Ta&gs:')
|
LABEL = _('Ta&gs:')
|
||||||
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
|
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
|
||||||
'useful while searching. <br><br>They can be any words'
|
'useful while searching. <br><br>They can be any words'
|
||||||
'or phrases, separated by commas.')
|
'or phrases, separated by commas.')
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
CompleteLineEdit.__init__(self, parent)
|
MultiCompleteLineEdit.__init__(self, parent)
|
||||||
self.setToolTip(self.TOOLTIP)
|
self.setToolTip(self.TOOLTIP)
|
||||||
self.setWhatsThis(self.TOOLTIP)
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
|
|
||||||
@ -836,7 +841,7 @@ class TagsEdit(CompleteLineEdit): # {{{
|
|||||||
tags = db.tags(id_, index_is_id=True)
|
tags = db.tags(id_, index_is_id=True)
|
||||||
tags = tags.split(',') if tags else []
|
tags = tags.split(',') if tags else []
|
||||||
self.current_val = tags
|
self.current_val = tags
|
||||||
self.update_items_cache(db.all_tags())
|
self.all_items = db.all_tags()
|
||||||
self.original_val = self.current_val
|
self.original_val = self.current_val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -857,7 +862,7 @@ class TagsEdit(CompleteLineEdit): # {{{
|
|||||||
d = TagEditor(self, db, id_)
|
d = TagEditor(self, db, id_)
|
||||||
if d.exec_() == TagEditor.Accepted:
|
if d.exec_() == TagEditor.Accepted:
|
||||||
self.current_val = d.tags
|
self.current_val = d.tags
|
||||||
self.update_items_cache(db.all_tags())
|
self.all_items = db.all_tags()
|
||||||
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
def commit(self, db, id_):
|
||||||
@ -907,11 +912,12 @@ class ISBNEdit(QLineEdit): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class PublisherEdit(EnComboBox): # {{{
|
class PublisherEdit(MultiCompleteComboBox): # {{{
|
||||||
LABEL = _('&Publisher:')
|
LABEL = _('&Publisher:')
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
EnComboBox.__init__(self, parent)
|
MultiCompleteComboBox.__init__(self, parent)
|
||||||
|
self.set_separator(None)
|
||||||
self.setSizeAdjustPolicy(
|
self.setSizeAdjustPolicy(
|
||||||
self.AdjustToMinimumContentsLengthWithIcon)
|
self.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
|
||||||
@ -932,6 +938,7 @@ class PublisherEdit(EnComboBox): # {{{
|
|||||||
def initialize(self, db, id_):
|
def initialize(self, db, id_):
|
||||||
all_publishers = db.all_publishers()
|
all_publishers = db.all_publishers()
|
||||||
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
||||||
|
self.update_items_cache([x[1] for x in all_publishers])
|
||||||
publisher_id = db.publisher_id(id_, index_is_id=True)
|
publisher_id = db.publisher_id(id_, index_is_id=True)
|
||||||
idx, c = None, 0
|
idx, c = None, 0
|
||||||
for i in all_publishers:
|
for i in all_publishers:
|
||||||
|
@ -430,8 +430,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
authors = self.authors(id, index_is_id=True)
|
authors = self.authors(id, index_is_id=True)
|
||||||
if not authors:
|
if not authors:
|
||||||
authors = _('Unknown')
|
authors = _('Unknown')
|
||||||
author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
|
author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||||
title = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
|
title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||||
path = author + '/' + title + ' (%d)'%id
|
path = author + '/' + title + ' (%d)'%id
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@ -442,8 +442,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
authors = self.authors(id, index_is_id=True)
|
authors = self.authors(id, index_is_id=True)
|
||||||
if not authors:
|
if not authors:
|
||||||
authors = _('Unknown')
|
authors = _('Unknown')
|
||||||
author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
|
author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||||
title = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
|
title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||||
name = title + ' - ' + author
|
name = title + ' - ' + author
|
||||||
while name.endswith('.'):
|
while name.endswith('.'):
|
||||||
name = name[:-1]
|
name = name[:-1]
|
||||||
|
@ -311,10 +311,25 @@ remove all non-breaking-space entities, or may include false positive matches re
|
|||||||
|
|
||||||
:guilabel:`Ensure scene breaks are consistently formatted`
|
:guilabel:`Ensure scene breaks are consistently formatted`
|
||||||
With this option |app| will attempt to detect common scene-break markers and ensure that they are center aligned.
|
With this option |app| will attempt to detect common scene-break markers and ensure that they are center aligned.
|
||||||
It also attempts to detect scene breaks defined by white space and replace them with a horizontal rule 15% of the
|
'Soft' scene break markers, i.e. scene breaks only defined by extra white space, are styled to ensure that they
|
||||||
page width. Some readers may find this desirable as these 'soft' scene breaks often become page breaks on readers, and
|
will not be displayed in conjunction with page breaks.
|
||||||
thus become difficult to distinguish.
|
|
||||||
|
|
||||||
|
:guilabel:`Replace scene breaks`
|
||||||
|
If this option is configured then |app| will replace scene break markers it finds with the replacement text specified by the
|
||||||
|
user. Please note that some ornamental characters may not be supported across all reading devices.
|
||||||
|
|
||||||
|
In general you should avoid using html tags, |app| will discard any tags and use pre-defined markup. <hr />
|
||||||
|
tags, i.e. horizontal rules, and <img> tags are exceptions. Horizontal rules can optionally be specified with styles, if you
|
||||||
|
choose to add your own style be sure to include the 'width' setting, otherwise the style information will be discarded. Image
|
||||||
|
tags can used, but |app| does not provide the ability to add the image during conversion, this must be done after the fact using
|
||||||
|
the 'Tweak Epub' feature, or Sigil.
|
||||||
|
|
||||||
|
Example image tag (place the image within an 'Images' folder inside the epub after conversion):
|
||||||
|
<img style="width:10%" src="../Images/scenebreak.png" />
|
||||||
|
|
||||||
|
Example horizontal rule with styles:
|
||||||
|
<hr style="width:20%;padding-top: 1px;border-top: 2px ridge black;border-bottom: 2px groove black;"/>
|
||||||
|
|
||||||
:guilabel:`Remove unnecessary hyphens`
|
:guilabel:`Remove unnecessary hyphens`
|
||||||
|app| will analyze all hyphenated content in the document when this option is enabled. The document itself is used
|
|app| will analyze all hyphenated content in the document when this option is enabled. The document itself is used
|
||||||
as a dictionary for analysis. This allows |app| to accurately remove hyphens for any words in the document in any language,
|
as a dictionary for analysis. This allows |app| to accurately remove hyphens for any words in the document in any language,
|
||||||
@ -628,7 +643,7 @@ between 0 and 1. The default is 0.45, just under the median line length. Lower t
|
|||||||
text in the unwrapping. Increase to include less. You can adjust this value in the conversion settings under :guilabel:`PDF Input`.
|
text in the unwrapping. Increase to include less. You can adjust this value in the conversion settings under :guilabel:`PDF Input`.
|
||||||
|
|
||||||
Also, they often have headers and footers as part of the document that will become included with the text.
|
Also, they often have headers and footers as part of the document that will become included with the text.
|
||||||
Use the options to remove headers and footers to mitigate this issue. If the headers and footers are not
|
Use the Search and Replace panel to remove headers and footers to mitigate this issue. If the headers and footers are not
|
||||||
removed from the text it can throw off the paragraph unwrapping. To learn how to use the header and footer removal options, read
|
removed from the text it can throw off the paragraph unwrapping. To learn how to use the header and footer removal options, read
|
||||||
:ref:`regexptutorial`.
|
:ref:`regexptutorial`.
|
||||||
|
|
||||||
|
@ -391,6 +391,8 @@ Take your pick:
|
|||||||
* A tribute to the SONY Librie which was the first e-ink based e-book reader
|
* A tribute to the SONY Librie which was the first e-ink based e-book reader
|
||||||
* My wife chose it ;-)
|
* My wife chose it ;-)
|
||||||
|
|
||||||
|
|app| is pronounced as cal-i-ber *not* ca-li-bre. If you're wondering, |app| is the British/commonwealth spelling for caliber. Being Indian, that's the natural spelling for me.
|
||||||
|
|
||||||
Why does |app| show only some of my fonts on OS X?
|
Why does |app| show only some of my fonts on OS X?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|app| embeds fonts in ebook files it creates. E-book files support embedding only TrueType (.ttf) fonts. Most fonts on OS X systems are in .dfont format, thus they cannot be embedded. |app| shows only TrueType fonts found on your system. You can obtain many TrueType fonts on the web. Simply download the .ttf files and add them to the Library/Fonts directory in your home directory.
|
|app| embeds fonts in ebook files it creates. E-book files support embedding only TrueType (.ttf) fonts. Most fonts on OS X systems are in .dfont format, thus they cannot be embedded. |app| shows only TrueType fonts found on your system. You can obtain many TrueType fonts on the web. Simply download the .ttf files and add them to the Library/Fonts directory in your home directory.
|
||||||
|
@ -186,7 +186,7 @@ class BuiltinTemplate(BuiltinFormatterFunction):
|
|||||||
|
|
||||||
def evaluate(self, formatter, kwargs, mi, locals, template):
|
def evaluate(self, formatter, kwargs, mi, locals, template):
|
||||||
template = template.replace('[[', '{').replace(']]', '}')
|
template = template.replace('[[', '{').replace(']]', '}')
|
||||||
return formatter.safe_format(template, kwargs, 'TEMPLATE', mi)
|
return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)
|
||||||
|
|
||||||
class BuiltinEval(BuiltinFormatterFunction):
|
class BuiltinEval(BuiltinFormatterFunction):
|
||||||
name = 'eval'
|
name = 'eval'
|
||||||
|
@ -72,11 +72,17 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
|
|||||||
f.write(data)
|
f.write(data)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'):
|
def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg',
|
||||||
|
preserve_aspect_ratio=True):
|
||||||
img = Image()
|
img = Image()
|
||||||
img.load(data)
|
img.load(data)
|
||||||
owidth, oheight = img.size
|
owidth, oheight = img.size
|
||||||
scaled, nwidth, nheight = fit_image(owidth, oheight, width, height)
|
if not preserve_aspect_ratio:
|
||||||
|
scaled = owidth > width or oheight > height
|
||||||
|
nwidth = width
|
||||||
|
nheight = height
|
||||||
|
else:
|
||||||
|
scaled, nwidth, nheight = fit_image(owidth, oheight, width, height)
|
||||||
if scaled:
|
if scaled:
|
||||||
img.size = (nwidth, nheight)
|
img.size = (nwidth, nheight)
|
||||||
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user