mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
Merge frome trunk
This commit is contained in:
commit
12e1c5219b
116
Changelog.yaml
116
Changelog.yaml
@ -4,6 +4,122 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.42
|
||||||
|
date: 2011-01-21
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "0.7.42 is a re-release of 0.7.41, because conversion to MOBI was broken in 0.7.41"
|
||||||
|
|
||||||
|
- title: "Conversions: Replace the remove header/footer options with a more geenric search replace option, that allows you to not only remove but also replace text"
|
||||||
|
|
||||||
|
- title: "Conversion: The preprocess html option has now become a new 'Heuristic Processing' option which allows you to control exactly which heuristics are used"
|
||||||
|
|
||||||
|
- title: "Conversion: Various improvements to Heuristic Processing (used to be preprocess HTML)"
|
||||||
|
|
||||||
|
- title: "When adding empty books to calibre, optionally set the author to the author of the currently selected book"
|
||||||
|
tickets: [7702]
|
||||||
|
|
||||||
|
- title: "Device drivers for the Archos 101, SmatQ T7 and Acer Lumiread"
|
||||||
|
|
||||||
|
- title: "Catalog generation: Make By Authors optional"
|
||||||
|
|
||||||
|
- title: "Allow bulk editing of Date and Published columns."
|
||||||
|
|
||||||
|
- title: "Add a little button to clear date and published values to the edit metadata dialogs"
|
||||||
|
|
||||||
|
- title: "When adding books by ISBN, allow the specification of special tags that will be added to the new book entries"
|
||||||
|
tickets: [8436]
|
||||||
|
|
||||||
|
- title: "Completion on multiple authors"
|
||||||
|
tickets: [8405]
|
||||||
|
|
||||||
|
- title: "Add AZW to default list of internally viewed formats, a I am tired of getting tickets about it"
|
||||||
|
|
||||||
|
- title: "Nicer error message when catalog generation fails"
|
||||||
|
|
||||||
|
- title: "Add capitalize option to context menus in the edit metadata dialog"
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "RTF Input: Fix regression in 0.7.40 that broke conversion of some old style RTF files"
|
||||||
|
|
||||||
|
- title: "Fix Tag editor forgets position"
|
||||||
|
tickets: [8271]
|
||||||
|
|
||||||
|
- title: "When converting books in the calibre GUI, override metadata from the input document, even when empty."
|
||||||
|
description: >
|
||||||
|
"So if you have removed all the tags and comments in the calibre GUI for the book in the calibre GUI, but the actual file that is being converted still has tags and comments, they are ignored. This affects only conversions in the calibre GUI, not from the command line via ebook-convert."
|
||||||
|
tickets: [8390]
|
||||||
|
|
||||||
|
- title: "Fix memory leak when switching libraries"
|
||||||
|
|
||||||
|
- title: "RTF Output: Fix incorrent spacing between letters."
|
||||||
|
tickets: [8422]
|
||||||
|
|
||||||
|
- title: "Catalog generation: Add composite columns to Merge Comments eligible types"
|
||||||
|
|
||||||
|
- title: "Add a confirmation when closing the add a custom news source dialog."
|
||||||
|
tickets: [8460]
|
||||||
|
|
||||||
|
- title: "Another workaround for LibraryThing UA sniffing that was preventing series metadata download, sigh."
|
||||||
|
tickets: [8477]
|
||||||
|
|
||||||
|
- title: "PD Novel driver: Put books on the SD card into the eBooks folder"
|
||||||
|
|
||||||
|
- title: "When shortening filepaths to conform to windows path length limitations, remove text from the middle of each component instead of the ends."
|
||||||
|
tickets: [8451]
|
||||||
|
|
||||||
|
- title: "Make completion in most places case insensitive"
|
||||||
|
tickets: [8441]
|
||||||
|
|
||||||
|
- title: "Fix regression that caused the N key to stop working when editing a Yes/no column"
|
||||||
|
tickets: [8417]
|
||||||
|
|
||||||
|
- title: "Email: Fix bug when connecting to SMTP relays that use MD5 auth"
|
||||||
|
|
||||||
|
- title: "MOBI Output: Fix bug that could cause a link pointing to the start of a section to go to a point later in the section is the section contained an empty id attribute"
|
||||||
|
|
||||||
|
- title: "When auto converting books and the device is unplugged, do not raise an error."
|
||||||
|
tickets: [8426]
|
||||||
|
|
||||||
|
- title: "Ebook-viewer: Display cover when viewing FB2 files"
|
||||||
|
|
||||||
|
- title: "MOBI Input: Special case handling of emptu div tags with a defined height used as paragraph separators."
|
||||||
|
tickets: [8391]
|
||||||
|
|
||||||
|
- title: "Fix sorting of author names into sub categories by first letter in the Tag Browser when the first letter has diacritics"
|
||||||
|
tickets: [8378]
|
||||||
|
|
||||||
|
- title: "Fix regression in 0.7.40 that caused commas in author names to become | when converting/saving to disk"
|
||||||
|
|
||||||
|
- title: "Fix view specific format on a book with no formats gives an error"
|
||||||
|
tickets: [8352]
|
||||||
|
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Blic
|
||||||
|
- Las Vegas Review Journal
|
||||||
|
- La Vanguardia
|
||||||
|
- New York Times
|
||||||
|
- El Pais
|
||||||
|
- Seattle Times
|
||||||
|
- Ars Technica
|
||||||
|
- Dilbert
|
||||||
|
- Nature News
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: "kath.net"
|
||||||
|
author: "Bobus"
|
||||||
|
|
||||||
|
- title: "iHNed"
|
||||||
|
author: "Karel Bilek"
|
||||||
|
|
||||||
|
- title: "Gulf News"
|
||||||
|
author: "Darko Miletic"
|
||||||
|
|
||||||
|
- title: "South Africa Mail and Guardian"
|
||||||
|
author: "77ja65"
|
||||||
|
|
||||||
|
|
||||||
- version: 0.7.40
|
- version: 0.7.40
|
||||||
date: 2011-01-14
|
date: 2011-01-14
|
||||||
|
|
||||||
|
BIN
resources/images/news/dailytportal.png
Normal file
BIN
resources/images/news/dailytportal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 635 B |
17
resources/recipes/20_minutos.recipe
Normal file
17
resources/recipes/20_minutos.recipe
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1295310874(BasicNewsRecipe):
|
||||||
|
title = u'20 Minutos (Boletin)'
|
||||||
|
__author__ = 'Luis Hernandez'
|
||||||
|
description = 'Periódico gratuito en español'
|
||||||
|
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif'
|
||||||
|
language = 'es'
|
||||||
|
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 50
|
||||||
|
|
||||||
|
feeds = [(u'VESPERTINO', u'http://20minutos.feedsportal.com/c/32489/f/478284/index.rss')
|
||||||
|
, (u'DEPORTES', u'http://20minutos.feedsportal.com/c/32489/f/478286/index.rss')
|
||||||
|
, (u'CULTURA', u'http://www.20minutos.es/rss/ocio/')
|
||||||
|
, (u'TV', u'http://20minutos.feedsportal.com/c/32489/f/490877/index.rss')
|
||||||
|
]
|
43
resources/recipes/abc.recipe
Normal file
43
resources/recipes/abc.recipe
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import re
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class ABCRecipe(BasicNewsRecipe):
|
||||||
|
title = u'ABC Linuxu'
|
||||||
|
oldest_article = 5
|
||||||
|
max_articles_per_feed = 3#5
|
||||||
|
__author__ = 'Funthomas'
|
||||||
|
language = 'cs'
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
#(u'Blogy', u'http://www.abclinuxu.cz/auto/blogDigest.rss'),
|
||||||
|
(u'Články', u'http://www.abclinuxu.cz/auto/abc.rss'),
|
||||||
|
(u'Zprávičky','http://www.abclinuxu.cz/auto/zpravicky.rss')
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_attributes = ['width','height']
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='h1')
|
||||||
|
remove_tags = [
|
||||||
|
dict(attrs={'class':['meta-vypis','page_tools','cl_perex']}),
|
||||||
|
dict(attrs={'class':['cl_nadpis-link','komix-nav']})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags_after = [
|
||||||
|
dict(name='div',attrs={'class':['cl_perex','komix-nav']}),
|
||||||
|
dict(attrs={'class':['meta-vypis','page_tools']}),
|
||||||
|
dict(name='',attrs={'':''}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
preprocess_regexps = [
|
||||||
|
(re.compile(r'</div>.*<p class="perex">', re.DOTALL),lambda match: '</div><p class="perex">')
|
||||||
|
]
|
||||||
|
def print_version(self, url):
|
||||||
|
return url + '?varianta=print&noDiz'
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1 {font-size:130%; font-weight:bold}
|
||||||
|
h3 {font-size:111%; font-weight:bold}
|
||||||
|
'''
|
66
resources/recipes/dailytportal.recipe
Normal file
66
resources/recipes/dailytportal.recipe
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
daily.tportal.hr
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Pagina12(BasicNewsRecipe):
|
||||||
|
title = 'Daily tportal.h'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'News from Croatia'
|
||||||
|
publisher = 'tportal.hr'
|
||||||
|
category = 'news, politics, Croatia'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 200
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'utf-8'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'en_HR'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
publication_type = 'newsportal'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: Verdana,sans-serif }
|
||||||
|
img{margin-bottom: 0.4em; display:block}
|
||||||
|
h1,h2{color: #2D648A; font-family: Georgia,serif}
|
||||||
|
.artAbstract{font-size: 1.2em; font-family: Georgia,serif}
|
||||||
|
"""
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name=['meta','link','embed','object','iframe','base'])
|
||||||
|
,dict(name='div', attrs={'class':'artInfo'})
|
||||||
|
]
|
||||||
|
remove_attributes=['lang']
|
||||||
|
|
||||||
|
keep_only_tags=dict(attrs={'class':'articleDetails'})
|
||||||
|
|
||||||
|
feeds = [(u'News', u'http://daily.tportal.hr/rss/dailynaslovnicarss.xml')]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('a'):
|
||||||
|
limg = item.find('img')
|
||||||
|
if item.string is not None:
|
||||||
|
str = item.string
|
||||||
|
item.replaceWith(str)
|
||||||
|
else:
|
||||||
|
if limg:
|
||||||
|
item.name = 'div'
|
||||||
|
item.attrs = []
|
||||||
|
else:
|
||||||
|
str = self.tag_to_string(item)
|
||||||
|
item.replaceWith(str)
|
||||||
|
for item in soup.findAll('img'):
|
||||||
|
if not item.has_key('alt'):
|
||||||
|
item['alt'] = 'image'
|
||||||
|
return soup
|
||||||
|
|
36
resources/recipes/everett_herald.recipe
Normal file
36
resources/recipes/everett_herald.recipe
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1295088390(BasicNewsRecipe):
|
||||||
|
title = u'Everett Herald'
|
||||||
|
language = 'en'
|
||||||
|
__author__ = '77ja65'
|
||||||
|
oldest_article = 4
|
||||||
|
max_articles_per_feed = 50
|
||||||
|
no_stylesheets = True
|
||||||
|
masthead_url = 'http://heraldnet.com/images/hnet/jQueryComponents/jQueryNavigation/heraldnet_logo.png'
|
||||||
|
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
|
||||||
|
|
||||||
|
feeds = [(u'Local News',
|
||||||
|
u'http://heraldnet.com/section/RSS02&mime=xml'),
|
||||||
|
(u'Sports', u'http://heraldnet.com/section/RSS04&mime=xml'),
|
||||||
|
(u'Entertainment',
|
||||||
|
u'http://heraldnet.com/section/RSS07&mime=xml'),
|
||||||
|
(u'Life', u'http://heraldnet.com/section/RSS03&mime=xml'),
|
||||||
|
(u'Breaking News',
|
||||||
|
u'http://heraldnet.com/section/RSS34&mime=xml'),
|
||||||
|
(u'Seahawks', u'http://heraldnet.com/section/RSS22&mime=xml'),
|
||||||
|
(u'HeraldNet', u'http://heraldnet.com/section/RSS01&mime=xml'),
|
||||||
|
(u'Inside Everett',
|
||||||
|
u'http://heraldnet.com/section/RSS26&mime=xml')
|
||||||
|
]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return url + "&template=PrinterFriendly"
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1{font-family:Arial,Helvetica,sans-serif; font-
|
||||||
|
weight:bold;font-size:large;}
|
||||||
|
h2{font-family:Arial,Helvetica,sans-serif; font-
|
||||||
|
weight:normal;font-size:small;}
|
||||||
|
'''
|
||||||
|
|
@ -52,6 +52,7 @@ class heiseDe(BasicNewsRecipe):
|
|||||||
dict(id='navi_login'),
|
dict(id='navi_login'),
|
||||||
dict(id='navigation'),
|
dict(id='navigation'),
|
||||||
dict(id='breadcrumb'),
|
dict(id='breadcrumb'),
|
||||||
|
dict(id='adblockerwarnung'),
|
||||||
dict(id=''),
|
dict(id=''),
|
||||||
dict(id='sitemap'),
|
dict(id='sitemap'),
|
||||||
dict(id='bannerzone'),
|
dict(id='bannerzone'),
|
||||||
@ -67,3 +68,4 @@ class heiseDe(BasicNewsRecipe):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class hnaDe(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 40
|
max_articles_per_feed = 40
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
encoding = 'iso-8859-1'
|
encoding = 'utf-8'
|
||||||
|
|
||||||
remove_tags = [dict(id='topnav'),
|
remove_tags = [dict(id='topnav'),
|
||||||
dict(id='nav_main'),
|
dict(id='nav_main'),
|
||||||
@ -60,3 +60,4 @@ class hnaDe(BasicNewsRecipe):
|
|||||||
feeds = [ ('hna_soehre', 'http://feeds2.feedburner.com/hna/soehre'),
|
feeds = [ ('hna_soehre', 'http://feeds2.feedburner.com/hna/soehre'),
|
||||||
('hna_kassel', 'http://feeds2.feedburner.com/hna/kassel') ]
|
('hna_kassel', 'http://feeds2.feedburner.com/hna/kassel') ]
|
||||||
|
|
||||||
|
|
||||||
|
54
resources/recipes/idnes.recipe
Normal file
54
resources/recipes/idnes.recipe
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
class iHeuteRecipe(BasicNewsRecipe):
|
||||||
|
__author__ = 'FunThomas'
|
||||||
|
title = u'iDnes.cz'
|
||||||
|
publisher = u'MAFRA a.s.'
|
||||||
|
description = 'iDNES.cz Zprávy, Technet, Komiksy a další'
|
||||||
|
oldest_article = 3
|
||||||
|
max_articles_per_feed = 2
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Zprávy', u'http://servis.idnes.cz/rss.asp?c=zpravodaj'),
|
||||||
|
(u'Sport', u'http://servis.idnes.cz/rss.asp?c=sport'),
|
||||||
|
(u'Technet', u'http://servis.idnes.cz/rss.asp?c=technet'),
|
||||||
|
(u'Mobil', u'http://servis.idnes.cz/rss.asp?c=mobil'),
|
||||||
|
(u'Ekonomika', u'http://servis.idnes.cz/rss.asp?c=ekonomikah'),
|
||||||
|
#(u'Kultura', u'http://servis.idnes.cz/rss.asp?c=kultura'),
|
||||||
|
(u'Cestování', u'http://servis.idnes.cz/rss.asp?c=iglobe'),
|
||||||
|
#(u'Kavárna', u'http://servis.idnes.cz/rss.asp?r=kavarna'),
|
||||||
|
(u'Komixy', u'http://servis.idnes.cz/rss.asp?c=komiksy')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
encoding = 'cp1250'
|
||||||
|
language = 'cs'
|
||||||
|
cover_url = 'http://g.idnes.cz/u/loga-n4/idnes.gif'
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
|
||||||
|
remove_attributes = ['width','height']
|
||||||
|
remove_tags = [dict(name='div', attrs={'id':['zooming']}),
|
||||||
|
dict(name='div', attrs={'class':['related','mapa-wrapper']}),
|
||||||
|
dict(name='table', attrs={'id':['opener-img','portal']}),
|
||||||
|
dict(name='table', attrs={'class':['video-16ku9']})]
|
||||||
|
remove_tags_after = [dict(name='div',attrs={'id':['related','related2']})]
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'class':['art-full adwords-text','dil-day']})
|
||||||
|
,dict(name='table',attrs={'class':['kemel-box']})]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
print_url = url
|
||||||
|
split_url = url.split("?")
|
||||||
|
if (split_url[0].rfind('dilbert.asp') != -1): #dilbert komix
|
||||||
|
print_url = print_url.replace('.htm','.gif&tisk=1')
|
||||||
|
print_url = print_url.replace('.asp','.aspx')
|
||||||
|
elif (split_url[0].rfind('kemel.asp') == -1): #not Kemel komix
|
||||||
|
print_url = 'http://zpravy.idnes.cz/tiskni.asp?' + split_url[1]
|
||||||
|
#kemel kemel print page doesn't work
|
||||||
|
return print_url
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1 {font-size:125%; font-weight:bold}
|
||||||
|
h3 {font-size:110%; font-weight:bold}
|
||||||
|
'''
|
29
resources/recipes/la_tribuna.recipe
Normal file
29
resources/recipes/la_tribuna.recipe
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||||
|
title = u'La Tribuna de Talavera'
|
||||||
|
__author__ = 'Luis Hernández'
|
||||||
|
description = 'Diario de Talavera de la Reina'
|
||||||
|
cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif'
|
||||||
|
|
||||||
|
oldest_article = 5
|
||||||
|
max_articles_per_feed = 50
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
|
||||||
|
encoding = 'utf-8'
|
||||||
|
language = 'es'
|
||||||
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':['articulo']})
|
||||||
|
,dict(name='div', attrs={'class':['foto']})
|
||||||
|
,dict(name='p', attrs={'id':['texto']})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='div' , attrs={'class':['comparte']})
|
||||||
|
remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']})
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [(u'Portada', u'http://www.latribunadetalavera.es/rss.html')]
|
@ -1,5 +1,5 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
nrc.nl
|
nrc.nl
|
||||||
'''
|
'''
|
||||||
@ -15,13 +15,18 @@ class Pagina12(BasicNewsRecipe):
|
|||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
max_articles_per_feed = 200
|
max_articles_per_feed = 200
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'cp1252'
|
encoding = 'utf8'
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'nl'
|
language = 'nl'
|
||||||
country = 'NL'
|
country = 'NL'
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
masthead_url = 'http://www.nrc.nl/nrc.nl/images/logo_nrc.png'
|
masthead_url = 'http://www.nrc.nl/nrc.nl/images/logo_nrc.png'
|
||||||
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} h1,h2,h3{text-align:left} '
|
extra_css = """
|
||||||
|
body{font-family: Georgia,serif }
|
||||||
|
img{margin-bottom: 0.4em; display: block}
|
||||||
|
.bijschrift,.sectie{font-size: x-small}
|
||||||
|
.sectie{color: gray}
|
||||||
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
@ -30,21 +35,42 @@ class Pagina12(BasicNewsRecipe):
|
|||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div',attrs={'class':'article clearfix'})]
|
keep_only_tags = [dict(attrs={'class':'uitstekendekeus'})]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name=['meta','base','link','object','embed'])
|
||||||
|
,dict(attrs={'class':['reclamespace','tags-and-sharing']})
|
||||||
|
]
|
||||||
|
remove_attributes=['lang']
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Voorpagina' , u'http://feeds.feedburner.com/NRCHandelsbladVoorpagina' )
|
(u'Voor nieuws', u'http://www.nrc.nl/nieuws/categorie/nieuws/rss.php' )
|
||||||
,(u'Binnenland' , u'http://feeds.feedburner.com/NRCHandelsbladBinnenland' )
|
,(u'Binnenland' , u'http://www.nrc.nl/nieuws/categorie/binnenland/rss.php' )
|
||||||
,(u'Buitenland' , u'http://feeds.feedburner.com/NRCHandelsbladBuitenland' )
|
,(u'Buitenland' , u'http://www.nrc.nl/nieuws/categorie/buitenland/rss.php' )
|
||||||
,(u'Economie' , u'http://feeds.feedburner.com/NRCHandelsbladEconomie' )
|
,(u'Economie' , u'http://www.nrc.nl/nieuws/categorie/economie/rss.php' )
|
||||||
,(u'Kunst & Film' , u'http://feeds.feedburner.com/nrc/NRCHandelsbladKunstEnFilm')
|
,(u'Cultuur' , u'http://www.nrc.nl/nieuws/categorie/cultuur/rss.php' )
|
||||||
,(u'Sport' , u'http://feeds.feedburner.com/NRCHandelsbladSport' )
|
,(u'Sport' , u'http://www.nrc.nl/nieuws/categorie/sport/rss.php' )
|
||||||
,(u'Wetenschap ' , u'http://www.nrc.nl/rss/wetenschap' )
|
,(u'Wetenschap ', u'http://www.nrc.nl/nieuws/categorie/wetenschap-nieuws/rss.php')
|
||||||
]
|
]
|
||||||
|
|
||||||
def print_version(self, url):
|
|
||||||
return url + '?service=Print'
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
return self.adeify_images(soup)
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('a'):
|
||||||
|
limg = item.find('img')
|
||||||
|
if item.string is not None:
|
||||||
|
str = item.string
|
||||||
|
item.replaceWith(str)
|
||||||
|
else:
|
||||||
|
if limg:
|
||||||
|
item.name = 'div'
|
||||||
|
atritems =['href','target','rel']
|
||||||
|
for atit in atritems:
|
||||||
|
if item.has_key(atit):
|
||||||
|
del item[atit]
|
||||||
|
else:
|
||||||
|
str = self.tag_to_string(item)
|
||||||
|
item.replaceWith(str)
|
||||||
|
for item in soup.findAll('img'):
|
||||||
|
if not item.has_key('alt'):
|
||||||
|
item['alt'] = 'image'
|
||||||
|
return soup
|
||||||
|
120
resources/recipes/roger_ebert.recipe
Normal file
120
resources/recipes/roger_ebert.recipe
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import re
|
||||||
|
import urllib2
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, SoupStrainer
|
||||||
|
|
||||||
|
class Ebert(BasicNewsRecipe):
|
||||||
|
title = 'Roger Ebert'
|
||||||
|
__author__ = 'Shane Erstad'
|
||||||
|
description = 'Roger Ebert Movie Reviews'
|
||||||
|
publisher = 'Chicago Sun Times'
|
||||||
|
category = 'movies'
|
||||||
|
oldest_article = 8
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
encoding = 'utf-8'
|
||||||
|
masthead_url = 'http://rogerebert.suntimes.com/graphics/global/roger.jpg'
|
||||||
|
language = 'en'
|
||||||
|
remove_empty_feeds = False
|
||||||
|
PREFIX = 'http://rogerebert.suntimes.com'
|
||||||
|
patternReviews = r'<span class="*?movietitle"*?>(.*?)</span>.*?<div class="*?headline"*?>(.*?)</div>(.*?)</div>'
|
||||||
|
patternCommentary = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?COMMENTARY.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
|
||||||
|
patternPeople = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?PEOPLE.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
|
||||||
|
patternGlossary = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?GLOSSARY.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
, 'linearize_tables' : True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Reviews' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=reviews' )
|
||||||
|
,(u'Commentary' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=COMMENTARY')
|
||||||
|
,(u'Great Movies' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=REVIEWS08')
|
||||||
|
,(u'People' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=PEOPLE')
|
||||||
|
,(u'Glossary' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=GLOSSARY')
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
preprocess_regexps = [
|
||||||
|
(re.compile(r'<font.*?>.*?This is a printer friendly.*?</font>.*?<hr>', re.DOTALL|re.IGNORECASE),
|
||||||
|
lambda m: '')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return url + '&template=printart'
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
totalfeeds = []
|
||||||
|
lfeeds = self.get_feeds()
|
||||||
|
for feedobj in lfeeds:
|
||||||
|
feedtitle, feedurl = feedobj
|
||||||
|
self.log('\tFeedurl: ', feedurl)
|
||||||
|
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
|
||||||
|
articles = []
|
||||||
|
page = urllib2.urlopen(feedurl).read()
|
||||||
|
|
||||||
|
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
|
||||||
|
pattern = self.patternReviews
|
||||||
|
elif feedtitle == 'Commentary':
|
||||||
|
pattern = self.patternCommentary
|
||||||
|
elif feedtitle == 'People':
|
||||||
|
pattern = self.patternPeople
|
||||||
|
elif feedtitle == 'Glossary':
|
||||||
|
pattern = self.patternGlossary
|
||||||
|
|
||||||
|
|
||||||
|
regex = re.compile(pattern, re.IGNORECASE|re.DOTALL)
|
||||||
|
|
||||||
|
for match in regex.finditer(page):
|
||||||
|
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
|
||||||
|
movietitle = match.group(1)
|
||||||
|
thislink = match.group(2)
|
||||||
|
description = match.group(3)
|
||||||
|
elif feedtitle == 'Commentary' or feedtitle == 'People' or feedtitle == 'Glossary':
|
||||||
|
thislink = match.group(1)
|
||||||
|
description = match.group(2)
|
||||||
|
|
||||||
|
self.log(thislink)
|
||||||
|
|
||||||
|
for link in BeautifulSoup(thislink, parseOnlyThese=SoupStrainer('a')):
|
||||||
|
thisurl = self.PREFIX + link['href']
|
||||||
|
thislinktext = self.tag_to_string(link)
|
||||||
|
|
||||||
|
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
|
||||||
|
thistitle = movietitle
|
||||||
|
elif feedtitle == 'Commentary' or feedtitle == 'People' or feedtitle == 'Glossary':
|
||||||
|
thistitle = thislinktext
|
||||||
|
|
||||||
|
if thistitle == '':
|
||||||
|
thistitle = 'Ebert Journal Post'
|
||||||
|
|
||||||
|
"""
|
||||||
|
pattern2 = r'AID=\/(.*?)\/'
|
||||||
|
reg2 = re.compile(pattern2, re.IGNORECASE|re.DOTALL)
|
||||||
|
match2 = reg2.search(thisurl)
|
||||||
|
date = match2.group(1)
|
||||||
|
c = time.strptime(match2.group(1),"%Y%m%d")
|
||||||
|
date=time.strftime("%a, %b %d, %Y", c)
|
||||||
|
self.log(date)
|
||||||
|
"""
|
||||||
|
|
||||||
|
articles.append({
|
||||||
|
'title' :thistitle
|
||||||
|
,'date' :''
|
||||||
|
,'url' :thisurl
|
||||||
|
,'description':description
|
||||||
|
})
|
||||||
|
totalfeeds.append((feedtitle, articles))
|
||||||
|
|
||||||
|
return totalfeeds
|
||||||
|
|
39
resources/recipes/root.recipe
Normal file
39
resources/recipes/root.recipe
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import re
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1289939440(BasicNewsRecipe):
|
||||||
|
__author__ = 'FunThomas'
|
||||||
|
title = u'Root.cz'
|
||||||
|
description = u'Zprávičky a články z Root.cz'
|
||||||
|
publisher = u'Internet Info, s.r.o'
|
||||||
|
oldest_article = 2 #max stari clanku ve dnech
|
||||||
|
max_articles_per_feed = 50 #max pocet clanku na feed
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Články', u'http://www.root.cz/rss/clanky/'),
|
||||||
|
(u'Zprávičky', u'http://www.root.cz/rss/zpravicky/')
|
||||||
|
]
|
||||||
|
|
||||||
|
publication_type = u'magazine'
|
||||||
|
language = u'cs'
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
cover_url = u'http://i.iinfo.cz/urs/logo-root-bila-oranzova-cerna-111089527143118.gif'
|
||||||
|
|
||||||
|
remove_attributes = ['width','height','href'] #,'href'
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h1'),
|
||||||
|
dict(name='a',attrs={'class':'author'}),
|
||||||
|
dict(name='p', attrs={'class':'intro'}),
|
||||||
|
dict(name='div',attrs={'class':'urs'})
|
||||||
|
]
|
||||||
|
|
||||||
|
preprocess_regexps = [
|
||||||
|
(re.compile(u'<p class="perex[^"]*">[^<]*<img[^>]*>', re.DOTALL),lambda match: '<p class="intro">'),
|
||||||
|
(re.compile(u'<h3><a name="tucnak">Tričko tučňák.*</body>', re.DOTALL),lambda match: '<!--deleted-->')
|
||||||
|
]
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1 {font-size:130%; font-weight:bold}
|
||||||
|
h3 {font-size:111%; font-weight:bold}
|
||||||
|
'''
|
@ -27,12 +27,34 @@ class cdnet(BasicNewsRecipe):
|
|||||||
dict(id='header'),
|
dict(id='header'),
|
||||||
dict(id='search'),
|
dict(id='search'),
|
||||||
dict(id='nav'),
|
dict(id='nav'),
|
||||||
|
dict(id='blog-author-info'),
|
||||||
|
dict(id='post-tags'),
|
||||||
|
dict(id='bio-naraine'),
|
||||||
|
dict(id='bio-kennedy'),
|
||||||
|
dict(id='author-short-disclosure-kennedy'),
|
||||||
dict(id=''),
|
dict(id=''),
|
||||||
dict(name='div', attrs={'class':'banner'}),
|
dict(name='div', attrs={'class':'banner'}),
|
||||||
|
dict(name='div', attrs={'class':'int'}),
|
||||||
|
dict(name='div', attrs={'class':'talkback clear space-2'}),
|
||||||
|
dict(name='div', attrs={'class':'content-1 clear'}),
|
||||||
|
dict(name='div', attrs={'class':'space-2'}),
|
||||||
|
dict(name='div', attrs={'class':'space-3'}),
|
||||||
|
dict(name='div', attrs={'class':'thumb-2 left'}),
|
||||||
|
dict(name='div', attrs={'class':'hotspot'}),
|
||||||
|
dict(name='div', attrs={'class':'hed hed-1 space-1'}),
|
||||||
|
dict(name='div', attrs={'class':'view-1 clear content-3 space-2'}),
|
||||||
|
dict(name='div', attrs={'class':'hed hed-1 space-1'}),
|
||||||
|
dict(name='div', attrs={'class':'hed hed-1'}),
|
||||||
|
dict(name='div', attrs={'class':'post-header'}),
|
||||||
|
dict(name='div', attrs={'class':'lvl-nav clear'}),
|
||||||
|
dict(name='div', attrs={'class':'t-share-overlay overlay-pop contain-overlay-4'}),
|
||||||
dict(name='p', attrs={'class':'tags'}),
|
dict(name='p', attrs={'class':'tags'}),
|
||||||
|
dict(name='span', attrs={'class':'follow'}),
|
||||||
|
dict(name='span', attrs={'class':'int'}),
|
||||||
|
dict(name='h4', attrs={'class':'h s-4'}),
|
||||||
dict(name='a', attrs={'href':'http://www.twitter.com/ryanaraine'}),
|
dict(name='a', attrs={'href':'http://www.twitter.com/ryanaraine'}),
|
||||||
dict(name='div', attrs={'class':'special1'})]
|
dict(name='div', attrs={'class':'special1'})]
|
||||||
remove_tags_after = [dict(name='div', attrs={'class':'bloggerDesc clear'})]
|
remove_tags_after = [dict(name='div', attrs={'class':'clear'})]
|
||||||
|
|
||||||
feeds = [ ('zdnet', 'http://feeds.feedburner.com/zdnet/security') ]
|
feeds = [ ('zdnet', 'http://feeds.feedburner.com/zdnet/security') ]
|
||||||
|
|
||||||
@ -43,3 +65,4 @@ class cdnet(BasicNewsRecipe):
|
|||||||
return soup
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,8 +43,9 @@ class Stage3(Command):
|
|||||||
|
|
||||||
description = 'Stage 3 of the publish process'
|
description = 'Stage 3 of the publish process'
|
||||||
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
|
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
|
||||||
'upload_to_google_code', 'tag_release', 'upload_to_server',
|
'upload_to_google_code', 'upload_to_sourceforge',
|
||||||
'upload_to_sourceforge', 'upload_to_mobileread',
|
'tag_release', 'upload_to_server',
|
||||||
|
'upload_to_mobileread',
|
||||||
]
|
]
|
||||||
|
|
||||||
class Stage4(Command):
|
class Stage4(Command):
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.40'
|
__version__ = '0.7.42'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -54,7 +54,7 @@ class ANDROID(USBMS):
|
|||||||
0x1004 : { 0x61cc : [0x100] },
|
0x1004 : { 0x61cc : [0x100] },
|
||||||
|
|
||||||
# Archos
|
# Archos
|
||||||
0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216]},
|
0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216], 0x1422 : [0x0216]},
|
||||||
|
|
||||||
}
|
}
|
||||||
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
|
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
|
||||||
@ -70,7 +70,7 @@ class ANDROID(USBMS):
|
|||||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||||
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
|
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
|
||||||
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT']
|
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H']
|
||||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||||
'A70S', 'A101IT']
|
'A70S', 'A101IT']
|
||||||
|
@ -25,13 +25,15 @@ class HeuristicProcessor(object):
|
|||||||
self.chapters_with_title = 0
|
self.chapters_with_title = 0
|
||||||
self.blanks_deleted = False
|
self.blanks_deleted = 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(?!\sid=\"softbreak\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
|
self.blankreg = re.compile(r'\s*(?P<openline><p(?!\sclass=\"softbreak\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
|
||||||
|
self.softbreak = re.compile(r'\s*(?P<openline><p(?=\sclass=\"softbreak\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
|
||||||
self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}', re.IGNORECASE)
|
self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}', re.IGNORECASE)
|
||||||
|
|
||||||
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]
|
||||||
|
|
||||||
def chapter_head(self, match):
|
def chapter_head(self, match):
|
||||||
|
from calibre.utils.html2text import html2text
|
||||||
chap = match.group('chap')
|
chap = match.group('chap')
|
||||||
title = match.group('title')
|
title = match.group('title')
|
||||||
if not title:
|
if not title:
|
||||||
@ -40,10 +42,12 @@ class HeuristicProcessor(object):
|
|||||||
" chapters. - " + unicode(chap))
|
" chapters. - " + unicode(chap))
|
||||||
return '<h2>'+chap+'</h2>\n'
|
return '<h2>'+chap+'</h2>\n'
|
||||||
else:
|
else:
|
||||||
|
txt_chap = html2text(chap)
|
||||||
|
txt_title = html2text(title)
|
||||||
self.html_preprocess_sections = self.html_preprocess_sections + 1
|
self.html_preprocess_sections = self.html_preprocess_sections + 1
|
||||||
self.log.debug("marked " + unicode(self.html_preprocess_sections) +
|
self.log.debug("marked " + unicode(self.html_preprocess_sections) +
|
||||||
" chapters & titles. - " + unicode(chap) + ", " + unicode(title))
|
" chapters & titles. - " + unicode(chap) + ", " + unicode(title))
|
||||||
return '<h2>'+chap+'</h2>\n<h3>'+title+'</h3>\n'
|
return '<h2 title="'+txt_chap+', '+txt_title+'">'+chap+'</h2>\n<h3 class="sigilNotInTOC">'+title+'</h3>\n'
|
||||||
|
|
||||||
def chapter_break(self, match):
|
def chapter_break(self, match):
|
||||||
chap = match.group('section')
|
chap = match.group('section')
|
||||||
@ -137,17 +141,17 @@ class HeuristicProcessor(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
ITALICIZE_STYLE_PATS = [
|
ITALICIZE_STYLE_PATS = [
|
||||||
r'(?msu)(?<=\s)_(?P<words>\S[^_]{0,40}?\S)?_(?=\s)',
|
r'(?msu)(?<=\s)_(?P<words>\S[^_]{0,40}?\S)?_(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)/(?P<words>\S[^/]{0,40}?\S)?/(?=\s)',
|
r'(?msu)(?<=\s)/(?P<words>\S[^/]{0,40}?\S)?/(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)~~(?P<words>\S[^~]{0,40}?\S)?~~(?=\s)',
|
r'(?msu)(?<=\s)~~(?P<words>\S[^~]{0,40}?\S)?~~(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)\*(?P<words>\S[^\*]{0,40}?\S)?\*(?=\s)',
|
r'(?msu)(?<=\s)\*(?P<words>\S[^\*]{0,40}?\S)?\*(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)~(?P<words>\S[^~]{0,40}?\S)?~(?=\s)',
|
r'(?msu)(?<=\s)~(?P<words>\S[^~]{0,40}?\S)?~(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)_/(?P<words>\S[^/_]{0,40}?\S)?/_(?=\s)',
|
r'(?msu)(?<=\s)_/(?P<words>\S[^/_]{0,40}?\S)?/_(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)_\*(?P<words>\S[^\*_]{0,40}?\S)?\*_(?=\s)',
|
r'(?msu)(?<=\s)_\*(?P<words>\S[^\*_]{0,40}?\S)?\*_(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)\*/(?P<words>\S[^/\*]{0,40}?\S)?/\*(?=\s)',
|
r'(?msu)(?<=\s)\*/(?P<words>\S[^/\*]{0,40}?\S)?/\*(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)_\*/(?P<words>\S[^\*_]{0,40}?\S)?/\*_(?=\s)',
|
r'(?msu)(?<=\s)_\*/(?P<words>\S[^\*_]{0,40}?\S)?/\*_(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)/:(?P<words>\S[^:/]{0,40}?\S)?:/(?=\s)',
|
r'(?msu)(?<=\s)/:(?P<words>\S[^:/]{0,40}?\S)?:/(?=[\s\.,\!\?])',
|
||||||
r'(?msu)(?<=\s)\|:(?P<words>\S[^:\|]{0,40}?\S)?:\|(?=\s)',
|
r'(?msu)(?<=\s)\|:(?P<words>\S[^:\|]{0,40}?\S)?:\|(?=[\s\.,\!\?])',
|
||||||
]
|
]
|
||||||
|
|
||||||
for word in ITALICIZE_WORDS:
|
for word in ITALICIZE_WORDS:
|
||||||
@ -203,8 +207,8 @@ class HeuristicProcessor(object):
|
|||||||
blank_lines = ""
|
blank_lines = ""
|
||||||
opt_title_open = "("
|
opt_title_open = "("
|
||||||
opt_title_close = ")?"
|
opt_title_close = ")?"
|
||||||
n_lookahead_open = "\s+(?!"
|
n_lookahead_open = "(?!\s*"
|
||||||
n_lookahead_close = ")"
|
n_lookahead_close = ")\s*"
|
||||||
|
|
||||||
default_title = r"(<[ibu][^>]*>)?\s{0,3}(?!Chapter)([\w\:\'’\"-]+\s{0,3}){1,5}?(</[ibu][^>]*>)?(?=<)"
|
default_title = r"(<[ibu][^>]*>)?\s{0,3}(?!Chapter)([\w\:\'’\"-]+\s{0,3}){1,5}?(</[ibu][^>]*>)?(?=<)"
|
||||||
simple_title = r"(<[ibu][^>]*>)?\s{0,3}(?!(Chapter|\s+<)).{0,65}?(</[ibu][^>]*>)?(?=<)"
|
simple_title = r"(<[ibu][^>]*>)?\s{0,3}(?!(Chapter|\s+<)).{0,65}?(</[ibu][^>]*>)?(?=<)"
|
||||||
@ -215,7 +219,7 @@ class HeuristicProcessor(object):
|
|||||||
[r"[^'\"]?(Introduction|Synopsis|Acknowledgements|Epilogue|CHAPTER|Kapitel|Volume\b|Prologue|Book\b|Part\b|Dedication|Preface)\s*([\d\w-]+\:?\'?\s*){0,5}", True, True, True, False, "Searching for common section headings", 'common'],
|
[r"[^'\"]?(Introduction|Synopsis|Acknowledgements|Epilogue|CHAPTER|Kapitel|Volume\b|Prologue|Book\b|Part\b|Dedication|Preface)\s*([\d\w-]+\:?\'?\s*){0,5}", True, True, True, False, "Searching for common section headings", 'common'],
|
||||||
[r"[^'\"]?(CHAPTER|Kapitel)\s*([\dA-Z\-\'\"\?!#,]+\s*){0,7}\s*", True, True, True, False, "Searching for most common chapter headings", 'chapter'], # Highest frequency headings which include titles
|
[r"[^'\"]?(CHAPTER|Kapitel)\s*([\dA-Z\-\'\"\?!#,]+\s*){0,7}\s*", True, True, True, False, "Searching for most common chapter headings", 'chapter'], # Highest frequency headings which include titles
|
||||||
[r"<b[^>]*>\s*(<span[^>]*>)?\s*(?!([*#•=]+\s*)+)(\s*(?=[\d.\w#\-*\s]+<)([\d.\w#-*]+\s*){1,5}\s*)(?!\.)(</span>)?\s*</b>", True, True, True, False, "Searching for emphasized lines", 'emphasized'], # Emphasized lines
|
[r"<b[^>]*>\s*(<span[^>]*>)?\s*(?!([*#•=]+\s*)+)(\s*(?=[\d.\w#\-*\s]+<)([\d.\w#-*]+\s*){1,5}\s*)(?!\.)(</span>)?\s*</b>", True, True, True, False, "Searching for emphasized lines", 'emphasized'], # Emphasized lines
|
||||||
[r"[^'\"]?(\d+(\.|:))\s*([\dA-Z\-\'\"#,]+\s*){0,7}\s*", True, True, True, False, "Searching for numeric chapter headings", 'numeric'], # Numeric Chapters
|
[r"[^'\"]?(\d+(\.|:))\s*([\w\-\'\"#,]+\s*){0,7}\s*", True, True, True, False, "Searching for numeric chapter headings", 'numeric'], # Numeric Chapters
|
||||||
[r"([A-Z]\s+){3,}\s*([\d\w-]+\s*){0,3}\s*", True, True, True, False, "Searching for letter spaced headings", 'letter_spaced'], # Spaced Lettering
|
[r"([A-Z]\s+){3,}\s*([\d\w-]+\s*){0,3}\s*", True, True, True, False, "Searching for letter spaced headings", 'letter_spaced'], # Spaced Lettering
|
||||||
[r"[^'\"]?(\d+\.?\s+([\d\w-]+\:?\'?-?\s?){0,5})\s*", True, True, True, False, "Searching for numeric chapters with titles", 'numeric_title'], # Numeric Titles
|
[r"[^'\"]?(\d+\.?\s+([\d\w-]+\:?\'?-?\s?){0,5})\s*", True, True, True, False, "Searching for numeric chapters with titles", 'numeric_title'], # Numeric Titles
|
||||||
[r"[^'\"]?(\d+)\s*([\dA-Z\-\'\"\?!#,]+\s*){0,7}\s*", True, True, True, False, "Searching for simple numeric headings", 'plain_number'], # Numeric Chapters, no dot or colon
|
[r"[^'\"]?(\d+)\s*([\dA-Z\-\'\"\?!#,]+\s*){0,7}\s*", True, True, True, False, "Searching for simple numeric headings", 'plain_number'], # Numeric Chapters, no dot or colon
|
||||||
@ -275,7 +279,7 @@ class HeuristicProcessor(object):
|
|||||||
self.log.debug(unicode(type_name)+" had "+unicode(hits)+" hits - "+unicode(self.chapters_no_title)+" chapters with no title, "+unicode(self.chapters_with_title)+" chapters with titles, "+unicode(float(self.chapters_with_title) / float(hits))+" percent. ")
|
self.log.debug(unicode(type_name)+" had "+unicode(hits)+" hits - "+unicode(self.chapters_no_title)+" chapters with no title, "+unicode(self.chapters_with_title)+" chapters with titles, "+unicode(float(self.chapters_with_title) / float(hits))+" percent. ")
|
||||||
if type_name == 'common':
|
if type_name == 'common':
|
||||||
analysis_result.append([chapter_type, n_lookahead_req, strict_title, ignorecase, title_req, log_message, type_name])
|
analysis_result.append([chapter_type, n_lookahead_req, strict_title, ignorecase, title_req, log_message, type_name])
|
||||||
elif self.min_chapters <= hits < max_chapters:
|
elif self.min_chapters <= hits < max_chapters or self.min_chapters < 3 > hits:
|
||||||
analysis_result.append([chapter_type, n_lookahead_req, strict_title, ignorecase, title_req, log_message, type_name])
|
analysis_result.append([chapter_type, n_lookahead_req, strict_title, ignorecase, title_req, log_message, type_name])
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -367,6 +371,8 @@ 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
|
||||||
|
html = re.sub('<p\s?/>', '', 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)[^>]*>\s*(<(font|[ibu]|em)[^>]*>\s*</(font|[ibu]|em)>\s*){0,2}\s*</(font|[ibu]|em)>", " ", html)
|
html = re.sub(r"\s*<(font|[ibu]|em)[^>]*>\s*(<(font|[ibu]|em)[^>]*>\s*</(font|[ibu]|em)>\s*){0,2}\s*</(font|[ibu]|em)>", " ", html)
|
||||||
@ -467,7 +473,7 @@ class HeuristicProcessor(object):
|
|||||||
if blanks_between_paragraphs and getattr(self.extra_opts, 'delete_blank_paragraphs', False):
|
if 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 id="softbreak" style="margin-top:1.5em; margin-bottom:1.5em"> </p>', html)
|
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1.5em; margin-bottom:1.5em"> </p>', html)
|
||||||
html = self.blankreg.sub('', html)
|
html = self.blankreg.sub('', html)
|
||||||
|
|
||||||
# Determine line ending type
|
# Determine line ending type
|
||||||
@ -522,11 +528,11 @@ class HeuristicProcessor(object):
|
|||||||
# Center separator lines
|
# Center separator lines
|
||||||
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">' + '\g<break>' + '</p>', 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">' + '\g<break>' + '</p>', html)
|
||||||
if not self.blanks_deleted:
|
if not self.blanks_deleted:
|
||||||
html = self.multi_blank.sub('\n<p id="softbreak" style="margin-top:1.5em; margin-bottom:1.5em"> </p>', html)
|
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1.5em; margin-bottom:1.5em"> </p>', html)
|
||||||
html = re.sub('<p\s+id="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 = 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)
|
||||||
|
|
||||||
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 to preserve original formatting
|
||||||
html = self.blankreg.sub('\n'+r'\g<openline>'+u'\u00a0'+r'\g<closeline>', html)
|
html = self.blankreg.sub('\n'+r'\g<openline>'+u'\u00a0'+r'\g<closeline>', html)
|
||||||
|
html = self.softbreak.sub('\n'+r'\g<openline>'+u'\u00a0'+r'\g<closeline>', html)
|
||||||
return html
|
return html
|
||||||
|
@ -373,7 +373,7 @@ def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
|
|||||||
r.pubdate = pubdate
|
r.pubdate = pubdate
|
||||||
|
|
||||||
def fix_case(x):
|
def fix_case(x):
|
||||||
if x and x.isupper():
|
if x:
|
||||||
x = titlecase(x)
|
x = titlecase(x)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
@ -1541,7 +1541,10 @@ class MobiWriter(object):
|
|||||||
exth.write(data)
|
exth.write(data)
|
||||||
nrecs += 1
|
nrecs += 1
|
||||||
if term == 'rights' :
|
if term == 'rights' :
|
||||||
|
try:
|
||||||
rights = unicode(oeb.metadata.rights[0]).encode('utf-8')
|
rights = unicode(oeb.metadata.rights[0]).encode('utf-8')
|
||||||
|
except:
|
||||||
|
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)
|
||||||
|
|
||||||
|
@ -221,7 +221,10 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
|
|||||||
el.text):
|
el.text):
|
||||||
stylesheet = parseString(el.text)
|
stylesheet = parseString(el.text)
|
||||||
replaceUrls(stylesheet, link_repl_func)
|
replaceUrls(stylesheet, link_repl_func)
|
||||||
el.text = '\n'+stylesheet.cssText + '\n'
|
repl = stylesheet.cssText
|
||||||
|
if isbytestring(repl):
|
||||||
|
repl = repl.decode('utf-8')
|
||||||
|
el.text = '\n'+ repl + '\n'
|
||||||
|
|
||||||
if 'style' in el.attrib:
|
if 'style' in el.attrib:
|
||||||
text = el.attrib['style']
|
text = el.attrib['style']
|
||||||
@ -234,8 +237,11 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
|
|||||||
set_property(item)
|
set_property(item)
|
||||||
elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
|
elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
|
||||||
set_property(v)
|
set_property(v)
|
||||||
el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r',
|
repl = stext.cssText.replace('\n', ' ').replace('\r',
|
||||||
' ')
|
' ')
|
||||||
|
if isbytestring(repl):
|
||||||
|
repl = repl.decode('utf-8')
|
||||||
|
el.attrib['style'] = repl
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,13 +84,9 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
|
|||||||
if not mi.is_null('rights'):
|
if not mi.is_null('rights'):
|
||||||
m.clear('rights')
|
m.clear('rights')
|
||||||
m.add('rights', mi.rights)
|
m.add('rights', mi.rights)
|
||||||
elif override_input_metadata:
|
|
||||||
m.clear('rights')
|
|
||||||
if not mi.is_null('publication_type'):
|
if not mi.is_null('publication_type'):
|
||||||
m.clear('publication_type')
|
m.clear('publication_type')
|
||||||
m.add('publication_type', mi.publication_type)
|
m.add('publication_type', mi.publication_type)
|
||||||
elif override_input_metadata:
|
|
||||||
m.clear('publication_type')
|
|
||||||
|
|
||||||
if not m.timestamp:
|
if not m.timestamp:
|
||||||
m.add('timestamp', isoformat(now()))
|
m.add('timestamp', isoformat(now()))
|
||||||
|
@ -385,13 +385,27 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
|
|
||||||
prefs['library_path'] = loc
|
prefs['library_path'] = loc
|
||||||
#from calibre.utils.mem import memory
|
#from calibre.utils.mem import memory
|
||||||
#import weakref, gc
|
#import weakref
|
||||||
#ref = weakref.ref(self.gui.library_view.model().db)
|
#from PyQt4.Qt import QTimer
|
||||||
#before = memory()/1024**2
|
#self.dbref = weakref.ref(self.gui.library_view.model().db)
|
||||||
|
#self.before_mem = memory()/1024**2
|
||||||
self.gui.library_moved(loc)
|
self.gui.library_moved(loc)
|
||||||
#print gc.get_referrers(ref)[0]
|
#QTimer.singleShot(1000, self.debug_leak)
|
||||||
#for i in xrange(3): gc.collect()
|
|
||||||
#print 'leaked:', memory()/1024**2 - before
|
def debug_leak(self):
|
||||||
|
import gc
|
||||||
|
from calibre.utils.mem import memory
|
||||||
|
ref = self.dbref
|
||||||
|
for i in xrange(3): gc.collect()
|
||||||
|
if ref() is not None:
|
||||||
|
print 11111, ref()
|
||||||
|
for r in gc.get_referrers(ref())[:10]:
|
||||||
|
print r
|
||||||
|
print
|
||||||
|
print 'before:', self.before_mem
|
||||||
|
print 'after:', memory()/1024**2
|
||||||
|
self.dbref = self.before_mem = None
|
||||||
|
|
||||||
|
|
||||||
def qs_requested(self, idx, *args):
|
def qs_requested(self, idx, *args):
|
||||||
self.switch_requested(self.qs_locations[idx])
|
self.switch_requested(self.qs_locations[idx])
|
||||||
|
@ -12,7 +12,8 @@ from lxml.html import soupparser
|
|||||||
|
|
||||||
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
|
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
|
||||||
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
|
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
|
||||||
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
|
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog, \
|
||||||
|
QHBoxLayout
|
||||||
from PyQt4.QtWebKit import QWebView, QWebPage
|
from PyQt4.QtWebKit import QWebView, QWebPage
|
||||||
|
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
@ -488,7 +489,7 @@ class Highlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
class Editor(QWidget): # {{{
|
class Editor(QWidget): # {{{
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None, one_line_toolbar=False):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.toolbar1 = QToolBar(self)
|
self.toolbar1 = QToolBar(self)
|
||||||
self.toolbar2 = QToolBar(self)
|
self.toolbar2 = QToolBar(self)
|
||||||
@ -508,9 +509,14 @@ class Editor(QWidget): # {{{
|
|||||||
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
|
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
|
||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
l.setContentsMargins(0, 0, 0, 0)
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
l.addWidget(self.toolbar1)
|
if one_line_toolbar:
|
||||||
l.addWidget(self.toolbar2)
|
tb = QHBoxLayout()
|
||||||
l.addWidget(self.toolbar3)
|
l.addLayout(tb)
|
||||||
|
else:
|
||||||
|
tb = l
|
||||||
|
tb.addWidget(self.toolbar1)
|
||||||
|
tb.addWidget(self.toolbar2)
|
||||||
|
tb.addWidget(self.toolbar3)
|
||||||
l.addWidget(self.editor)
|
l.addWidget(self.editor)
|
||||||
self._layout.addWidget(self.tabs)
|
self._layout.addWidget(self.tabs)
|
||||||
self.tabs.addTab(self.wyswyg, _('Normal view'))
|
self.tabs.addTab(self.wyswyg, _('Normal view'))
|
||||||
|
@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from PyQt4.QtCore import SIGNAL, Qt
|
from PyQt4.QtCore import SIGNAL, Qt, pyqtSignal
|
||||||
from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \
|
from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \
|
||||||
QBrush, QTextCursor, QTextEdit
|
QBrush, QTextCursor, QTextEdit
|
||||||
|
|
||||||
@ -19,8 +19,8 @@ from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
|||||||
|
|
||||||
class RegexBuilder(QDialog, Ui_RegexBuilder):
|
class RegexBuilder(QDialog, Ui_RegexBuilder):
|
||||||
|
|
||||||
def __init__(self, db, book_id, regex, *args):
|
def __init__(self, db, book_id, regex, doc=None, parent=None):
|
||||||
QDialog.__init__(self, *args)
|
QDialog.__init__(self, parent)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.regex.setText(regex)
|
self.regex.setText(regex)
|
||||||
@ -28,13 +28,21 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
|
|
||||||
if not db or not book_id:
|
if not db or not book_id:
|
||||||
self.button_box.addButton(QDialogButtonBox.Open)
|
self.button_box.addButton(QDialogButtonBox.Open)
|
||||||
elif not self.select_format(db, book_id):
|
elif not doc and not self.select_format(db, book_id):
|
||||||
self.cancelled = True
|
self.cancelled = True
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if doc:
|
||||||
|
self.preview.setPlainText(doc)
|
||||||
|
|
||||||
self.cancelled = False
|
self.cancelled = False
|
||||||
self.connect(self.button_box, SIGNAL('clicked(QAbstractButton*)'), self.button_clicked)
|
self.connect(self.button_box, SIGNAL('clicked(QAbstractButton*)'), self.button_clicked)
|
||||||
self.connect(self.regex, SIGNAL('textChanged(QString)'), self.regex_valid)
|
self.connect(self.regex, SIGNAL('textChanged(QString)'), self.regex_valid)
|
||||||
self.connect(self.test, SIGNAL('clicked()'), self.do_test)
|
self.connect(self.test, SIGNAL('clicked()'), self.do_test)
|
||||||
|
self.connect(self.previous, SIGNAL('clicked()'), self.goto_previous)
|
||||||
|
self.connect(self.next, SIGNAL('clicked()'), self.goto_next)
|
||||||
|
|
||||||
|
self.match_locs = []
|
||||||
|
|
||||||
def regex_valid(self):
|
def regex_valid(self):
|
||||||
regex = unicode(self.regex.text())
|
regex = unicode(self.regex.text())
|
||||||
@ -42,17 +50,23 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
try:
|
try:
|
||||||
re.compile(regex)
|
re.compile(regex)
|
||||||
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgba(0,255,0,20%); }')
|
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgba(0,255,0,20%); }')
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgb(255,0,0,20%); }')
|
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgb(255,0,0,20%); }')
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
|
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
|
||||||
self.preview.setExtraSelections([])
|
self.preview.setExtraSelections([])
|
||||||
|
|
||||||
|
self.match_locs = []
|
||||||
|
self.next.setEnabled(False)
|
||||||
|
self.previous.setEnabled(False)
|
||||||
|
self.occurrences.setText('0')
|
||||||
|
|
||||||
return False
|
return False
|
||||||
return True
|
|
||||||
|
|
||||||
def do_test(self):
|
def do_test(self):
|
||||||
selections = []
|
selections = []
|
||||||
|
self.match_locs = []
|
||||||
if self.regex_valid():
|
if self.regex_valid():
|
||||||
text = unicode(self.preview.toPlainText())
|
text = unicode(self.preview.toPlainText())
|
||||||
regex = unicode(self.regex.text())
|
regex = unicode(self.regex.text())
|
||||||
@ -66,9 +80,43 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
es.cursor.setPosition(match.start(), QTextCursor.MoveAnchor)
|
es.cursor.setPosition(match.start(), QTextCursor.MoveAnchor)
|
||||||
es.cursor.setPosition(match.end(), QTextCursor.KeepAnchor)
|
es.cursor.setPosition(match.end(), QTextCursor.KeepAnchor)
|
||||||
selections.append(es)
|
selections.append(es)
|
||||||
|
self.match_locs.append((match.start(), match.end()))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
self.preview.setExtraSelections(selections)
|
self.preview.setExtraSelections(selections)
|
||||||
|
if self.match_locs:
|
||||||
|
self.next.setEnabled(True)
|
||||||
|
self.previous.setEnabled(True)
|
||||||
|
self.occurrences.setText(str(len(self.match_locs)))
|
||||||
|
|
||||||
|
def goto_previous(self):
|
||||||
|
pos = self.preview.textCursor().position()
|
||||||
|
if self.match_locs:
|
||||||
|
match_loc = len(self.match_locs) - 1
|
||||||
|
for i in xrange(len(self.match_locs) - 1, -1, -1):
|
||||||
|
loc = self.match_locs[i][1]
|
||||||
|
if pos > loc:
|
||||||
|
match_loc = i
|
||||||
|
break
|
||||||
|
self.goto_loc(self.match_locs[match_loc][1], operation=QTextCursor.Left, n=self.match_locs[match_loc][1] - self.match_locs[match_loc][0])
|
||||||
|
|
||||||
|
def goto_next(self):
|
||||||
|
pos = self.preview.textCursor().position()
|
||||||
|
if self.match_locs:
|
||||||
|
match_loc = 0
|
||||||
|
for i in xrange(len(self.match_locs)):
|
||||||
|
loc = self.match_locs[i][0]
|
||||||
|
if pos < loc:
|
||||||
|
match_loc = i
|
||||||
|
break
|
||||||
|
self.goto_loc(self.match_locs[match_loc][0], n=self.match_locs[match_loc][1] - self.match_locs[match_loc][0])
|
||||||
|
|
||||||
|
def goto_loc(self, loc, operation=QTextCursor.Right, mode=QTextCursor.KeepAnchor, n=0):
|
||||||
|
cursor = QTextCursor(self.preview.document())
|
||||||
|
cursor.setPosition(loc)
|
||||||
|
if n:
|
||||||
|
cursor.movePosition(operation, mode, n)
|
||||||
|
self.preview.setTextCursor(cursor)
|
||||||
|
|
||||||
def select_format(self, db, book_id):
|
def select_format(self, db, book_id):
|
||||||
format = None
|
format = None
|
||||||
@ -109,24 +157,41 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
if button == self.button_box.button(QDialogButtonBox.Ok):
|
if button == self.button_box.button(QDialogButtonBox.Ok):
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
def doc(self):
|
||||||
|
return unicode(self.preview.toPlainText())
|
||||||
|
|
||||||
class RegexEdit(QWidget, Ui_Edit):
|
class RegexEdit(QWidget, Ui_Edit):
|
||||||
|
|
||||||
|
doc_update = pyqtSignal(unicode)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.book_id = None
|
self.book_id = None
|
||||||
self.db = None
|
self.db = None
|
||||||
|
self.doc_cache = None
|
||||||
|
|
||||||
self.connect(self.button, SIGNAL('clicked()'), self.builder)
|
self.connect(self.button, SIGNAL('clicked()'), self.builder)
|
||||||
|
|
||||||
def builder(self):
|
def builder(self):
|
||||||
bld = RegexBuilder(self.db, self.book_id, self.edit.text(), self)
|
bld = RegexBuilder(self.db, self.book_id, self.edit.text(), self.doc_cache, self)
|
||||||
if bld.cancelled:
|
if bld.cancelled:
|
||||||
return
|
return
|
||||||
|
if not self.doc_cache:
|
||||||
|
self.doc_cache = bld.doc()
|
||||||
|
self.doc_update.emit(self.doc_cache)
|
||||||
if bld.exec_() == bld.Accepted:
|
if bld.exec_() == bld.Accepted:
|
||||||
self.edit.setText(bld.regex.text())
|
self.edit.setText(bld.regex.text())
|
||||||
|
|
||||||
|
def doc(self):
|
||||||
|
return self.doc_cache
|
||||||
|
|
||||||
|
def setObjectName(self, *args):
|
||||||
|
QWidget.setObjectName(self, *args)
|
||||||
|
if hasattr(self, 'edit'):
|
||||||
|
self.edit.initialize('regex_edit_'+unicode(self.objectName()))
|
||||||
|
|
||||||
def set_msg(self, msg):
|
def set_msg(self, msg):
|
||||||
self.msg.setText(msg)
|
self.msg.setText(msg)
|
||||||
|
|
||||||
@ -136,8 +201,11 @@ class RegexEdit(QWidget, Ui_Edit):
|
|||||||
def set_db(self, db):
|
def set_db(self, db):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
|
def set_doc(self, doc):
|
||||||
|
self.doc_cache = doc
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
self.db = None
|
self.db = self.doc_cache = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self):
|
def text(self):
|
||||||
|
@ -6,15 +6,102 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>662</width>
|
<width>580</width>
|
||||||
<height>505</height>
|
<height>503</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Regex Builder</string>
|
<string>Regex Builder</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<item row="1" column="0" colspan="5">
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Regex:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="regex"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="test">
|
||||||
|
<property name="text">
|
||||||
|
<string>Test</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>Occurrences:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="occurrences">
|
||||||
|
<property name="text">
|
||||||
|
<string>0</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>298</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Goto:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="previous">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Previous</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="next">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Next</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Preview</string>
|
<string>Preview</string>
|
||||||
@ -36,7 +123,22 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="4">
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>328</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
<widget class="QDialogButtonBox" name="button_box">
|
<widget class="QDialogButtonBox" name="button_box">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
@ -46,25 +148,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Regex:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1" colspan="4">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="regex"/>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="test">
|
|
||||||
<property name="text">
|
|
||||||
<string>Test</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -35,13 +35,26 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
|||||||
self.opt_sr3_search.set_book_id(book_id)
|
self.opt_sr3_search.set_book_id(book_id)
|
||||||
self.opt_sr3_search.set_db(db)
|
self.opt_sr3_search.set_db(db)
|
||||||
|
|
||||||
|
self.opt_sr1_search.doc_update.connect(self.update_doc)
|
||||||
|
self.opt_sr2_search.doc_update.connect(self.update_doc)
|
||||||
|
self.opt_sr3_search.doc_update.connect(self.update_doc)
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
Widget.break_cycles(self)
|
Widget.break_cycles(self)
|
||||||
|
|
||||||
|
self.opt_sr1_search.doc_update.disconnect()
|
||||||
|
self.opt_sr2_search.doc_update.disconnect()
|
||||||
|
self.opt_sr3_search.doc_update.disconnect()
|
||||||
|
|
||||||
self.opt_sr1_search.break_cycles()
|
self.opt_sr1_search.break_cycles()
|
||||||
self.opt_sr2_search.break_cycles()
|
self.opt_sr2_search.break_cycles()
|
||||||
self.opt_sr3_search.break_cycles()
|
self.opt_sr3_search.break_cycles()
|
||||||
|
|
||||||
|
def update_doc(self, doc):
|
||||||
|
self.opt_sr1_search.set_doc(doc)
|
||||||
|
self.opt_sr2_search.set_doc(doc)
|
||||||
|
self.opt_sr3_search.set_doc(doc)
|
||||||
|
|
||||||
def pre_commit_check(self):
|
def pre_commit_check(self):
|
||||||
for x in ('sr1_search', 'sr2_search', 'sr3_search'):
|
for x in ('sr1_search', 'sr2_search', 'sr3_search'):
|
||||||
x = getattr(self, 'opt_'+x)
|
x = getattr(self, 'opt_'+x)
|
||||||
|
@ -13,7 +13,7 @@ from calibre.customize.ui import available_input_formats, available_output_forma
|
|||||||
device_plugins
|
device_plugins
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
from calibre.devices.errors import UserFeedback, OpenFeedback
|
from calibre.devices.errors import UserFeedback, OpenFeedback
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog
|
||||||
from calibre.utils.ipc.job import BaseJob
|
from calibre.utils.ipc.job import BaseJob
|
||||||
from calibre.devices.scanner import DeviceScanner
|
from calibre.devices.scanner import DeviceScanner
|
||||||
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
||||||
@ -826,8 +826,24 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
fmt = None
|
fmt = None
|
||||||
if specific:
|
if specific:
|
||||||
d = ChooseFormatDialog(self, _('Choose format to send to device'),
|
formats = []
|
||||||
self.device_manager.device.settings().format_map)
|
aval_out_formats = available_output_formats()
|
||||||
|
format_count = {}
|
||||||
|
for row in rows:
|
||||||
|
fmts = self.library_view.model().db.formats(row.row())
|
||||||
|
if fmts:
|
||||||
|
for f in fmts.split(','):
|
||||||
|
f = f.lower()
|
||||||
|
if format_count.has_key(f):
|
||||||
|
format_count[f] += 1
|
||||||
|
else:
|
||||||
|
format_count[f] = 1
|
||||||
|
for f in self.device_manager.device.settings().format_map:
|
||||||
|
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))
|
||||||
|
elif f in aval_out_formats:
|
||||||
|
formats.append((f, _('0 of %i Books' % len(rows)), True))
|
||||||
|
d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats)
|
||||||
if d.exec_() != QDialog.Accepted:
|
if d.exec_() != QDialog.Accepted:
|
||||||
return
|
return
|
||||||
if d.format():
|
if d.format():
|
||||||
|
53
src/calibre/gui2/dialogs/choose_format_device.py
Normal file
53
src/calibre/gui2/dialogs/choose_format_device.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
|
||||||
|
from PyQt4.Qt import QDialog, QTreeWidgetItem, QIcon, SIGNAL
|
||||||
|
|
||||||
|
from calibre.gui2 import file_icon_provider
|
||||||
|
from calibre.gui2.dialogs.choose_format_device_ui import Ui_ChooseFormatDeviceDialog
|
||||||
|
|
||||||
|
class ChooseFormatDeviceDialog(QDialog, Ui_ChooseFormatDeviceDialog):
|
||||||
|
|
||||||
|
def __init__(self, window, msg, formats):
|
||||||
|
'''
|
||||||
|
formats is a list of tuples: [(format, exists, convertible)].
|
||||||
|
format: Lower case format identifier. E.G. mobi
|
||||||
|
exists: String representing the number of books that
|
||||||
|
exist in the format.
|
||||||
|
convertible: True if the format is a convertible format.
|
||||||
|
formats should be ordered in the device's preferred format ordering.
|
||||||
|
'''
|
||||||
|
QDialog.__init__(self, window)
|
||||||
|
Ui_ChooseFormatDeviceDialog.__init__(self)
|
||||||
|
self.setupUi(self)
|
||||||
|
self.connect(self.formats, SIGNAL('activated(QModelIndex)'),
|
||||||
|
self.activated_slot)
|
||||||
|
|
||||||
|
self.msg.setText(msg)
|
||||||
|
for i, (format, exists, convertible) in enumerate(formats):
|
||||||
|
t_item = QTreeWidgetItem()
|
||||||
|
t_item.setIcon(0, file_icon_provider().icon_from_ext(format.lower()))
|
||||||
|
t_item.setText(0, format.upper())
|
||||||
|
t_item.setText(1, exists)
|
||||||
|
if convertible:
|
||||||
|
t_item.setIcon(2, QIcon(I('ok.png')))
|
||||||
|
self.formats.addTopLevelItem(t_item)
|
||||||
|
if i == 0:
|
||||||
|
self.formats.setCurrentItem(t_item)
|
||||||
|
t_item.setSelected(True)
|
||||||
|
self.formats.resizeColumnToContents(2)
|
||||||
|
self.formats.resizeColumnToContents(1)
|
||||||
|
self.formats.resizeColumnToContents(0)
|
||||||
|
self.formats.header().resizeSection(0, self.formats.header().sectionSize(0) * 2)
|
||||||
|
self._format = None
|
||||||
|
|
||||||
|
def activated_slot(self, *args):
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def format(self):
|
||||||
|
return self._format
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
self._format = unicode(self.formats.currentItem().text(0))
|
||||||
|
return QDialog.accept(self)
|
||||||
|
|
111
src/calibre/gui2/dialogs/choose_format_device.ui
Normal file
111
src/calibre/gui2/dialogs/choose_format_device.ui
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ChooseFormatDeviceDialog</class>
|
||||||
|
<widget class="QDialog" name="ChooseFormatDeviceDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>507</width>
|
||||||
|
<height>377</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Choose Format</string>
|
||||||
|
</property>
|
||||||
|
<property name="windowIcon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="msg">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTreeWidget" name="formats">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>64</width>
|
||||||
|
<height>64</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="allColumnsShowFocus">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Format</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Existing</string>
|
||||||
|
</property>
|
||||||
|
<property name="textAlignment">
|
||||||
|
<set>AlignLeft|AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Convertible</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources>
|
||||||
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
</resources>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>ChooseFormatDeviceDialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>ChooseFormatDeviceDialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -6,8 +6,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import re, os
|
import re, os
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
|
from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
|
||||||
pyqtSignal, QDialogButtonBox
|
pyqtSignal, QDialogButtonBox, QInputDialog, QLineEdit, \
|
||||||
from PyQt4 import QtGui
|
QMessageBox, QDate
|
||||||
|
|
||||||
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||||
@ -15,9 +15,9 @@ from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
|||||||
from calibre.ebooks.metadata.book.base import composite_formatter
|
from calibre.ebooks.metadata.book.base import composite_formatter
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||||
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE
|
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE, gprefs
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.utils.config import dynamic
|
from calibre.utils.config import dynamic, JSONConfig
|
||||||
from calibre.utils.titlecase import titlecase
|
from calibre.utils.titlecase import titlecase
|
||||||
from calibre.utils.icu import sort_key, capitalize
|
from calibre.utils.icu import sort_key, capitalize
|
||||||
from calibre.utils.config import prefs, tweaks
|
from calibre.utils.config import prefs, tweaks
|
||||||
@ -302,6 +302,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
self.pubdate.setSpecialValueText(_('Undefined'))
|
self.pubdate.setSpecialValueText(_('Undefined'))
|
||||||
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
|
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
|
||||||
self.pubdate.dateChanged.connect(self.do_apply_pubdate)
|
self.pubdate.dateChanged.connect(self.do_apply_pubdate)
|
||||||
|
self.adddate.setDate(QDate.currentDate())
|
||||||
self.adddate.setMinimumDate(UNDEFINED_QDATE)
|
self.adddate.setMinimumDate(UNDEFINED_QDATE)
|
||||||
self.adddate.setSpecialValueText(_('Undefined'))
|
self.adddate.setSpecialValueText(_('Undefined'))
|
||||||
self.clear_adddate_button.clicked.connect(self.clear_adddate)
|
self.clear_adddate_button.clicked.connect(self.clear_adddate)
|
||||||
@ -320,8 +321,15 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
'This operation cannot be canceled or undone'))
|
'This operation cannot be canceled or undone'))
|
||||||
self.do_again = False
|
self.do_again = False
|
||||||
self.central_widget.setCurrentIndex(tab)
|
self.central_widget.setCurrentIndex(tab)
|
||||||
|
geom = gprefs.get('bulk_metadata_window_geometry', None)
|
||||||
|
if geom is not None:
|
||||||
|
self.restoreGeometry(bytes(geom))
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
|
def save_state(self, *args):
|
||||||
|
gprefs['bulk_metadata_window_geometry'] = \
|
||||||
|
bytearray(self.saveGeometry())
|
||||||
|
|
||||||
def do_apply_pubdate(self, *args):
|
def do_apply_pubdate(self, *args):
|
||||||
self.apply_pubdate.setChecked(True)
|
self.apply_pubdate.setChecked(True)
|
||||||
|
|
||||||
@ -365,16 +373,16 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
offset = 10
|
offset = 10
|
||||||
self.s_r_number_of_books = min(10, len(self.ids))
|
self.s_r_number_of_books = min(10, len(self.ids))
|
||||||
for i in range(1,self.s_r_number_of_books+1):
|
for i in range(1,self.s_r_number_of_books+1):
|
||||||
w = QtGui.QLabel(self.tabWidgetPage3)
|
w = QLabel(self.tabWidgetPage3)
|
||||||
w.setText(_('Book %d:')%i)
|
w.setText(_('Book %d:')%i)
|
||||||
self.testgrid.addWidget(w, i+offset, 0, 1, 1)
|
self.testgrid.addWidget(w, i+offset, 0, 1, 1)
|
||||||
w = QtGui.QLineEdit(self.tabWidgetPage3)
|
w = QLineEdit(self.tabWidgetPage3)
|
||||||
w.setReadOnly(True)
|
w.setReadOnly(True)
|
||||||
name = 'book_%d_text'%i
|
name = 'book_%d_text'%i
|
||||||
setattr(self, name, w)
|
setattr(self, name, w)
|
||||||
self.book_1_text.setObjectName(name)
|
self.book_1_text.setObjectName(name)
|
||||||
self.testgrid.addWidget(w, i+offset, 1, 1, 1)
|
self.testgrid.addWidget(w, i+offset, 1, 1, 1)
|
||||||
w = QtGui.QLineEdit(self.tabWidgetPage3)
|
w = QLineEdit(self.tabWidgetPage3)
|
||||||
w.setReadOnly(True)
|
w.setReadOnly(True)
|
||||||
name = 'book_%d_result'%i
|
name = 'book_%d_result'%i
|
||||||
setattr(self, name, w)
|
setattr(self, name, w)
|
||||||
@ -451,6 +459,15 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed)
|
self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed)
|
||||||
self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed)
|
self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed)
|
||||||
|
|
||||||
|
self.save_button.clicked.connect(self.s_r_save_query)
|
||||||
|
self.remove_button.clicked.connect(self.s_r_remove_query)
|
||||||
|
|
||||||
|
self.queries = JSONConfig("search_replace_queries")
|
||||||
|
self.query_field.addItem("")
|
||||||
|
self.query_field.addItems(sorted([q for q in self.queries], key=sort_key))
|
||||||
|
self.query_field.currentIndexChanged[str].connect(self.s_r_query_change)
|
||||||
|
self.query_field.setCurrentIndex(0)
|
||||||
|
|
||||||
def s_r_get_field(self, mi, field):
|
def s_r_get_field(self, mi, field):
|
||||||
if field:
|
if field:
|
||||||
if field == '{template}':
|
if field == '{template}':
|
||||||
@ -780,7 +797,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
self.series_start_number.setEnabled(False)
|
self.series_start_number.setEnabled(False)
|
||||||
self.series_start_number.setValue(1)
|
self.series_start_number.setValue(1)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self.save_state()
|
||||||
|
ResizableDialog.reject(self)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
|
self.save_state()
|
||||||
if len(self.ids) < 1:
|
if len(self.ids) < 1:
|
||||||
return QDialog.accept(self)
|
return QDialog.accept(self)
|
||||||
|
|
||||||
@ -862,3 +884,117 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
def series_changed(self, *args):
|
def series_changed(self, *args):
|
||||||
self.write_series = True
|
self.write_series = True
|
||||||
|
|
||||||
|
def s_r_remove_query(self, *args):
|
||||||
|
if self.query_field.currentIndex() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
ret = QMessageBox.question(self, _("Delete saved search/replace"),
|
||||||
|
_("The selected saved search/replace will be deleted. "
|
||||||
|
"Are you sure?"),
|
||||||
|
QMessageBox.Ok, QMessageBox.Cancel)
|
||||||
|
|
||||||
|
if ret == QMessageBox.Cancel:
|
||||||
|
return
|
||||||
|
|
||||||
|
item_id = self.query_field.currentIndex()
|
||||||
|
item_name = unicode(self.query_field.currentText())
|
||||||
|
|
||||||
|
self.query_field.blockSignals(True)
|
||||||
|
self.query_field.removeItem(item_id)
|
||||||
|
self.query_field.blockSignals(False)
|
||||||
|
self.query_field.setCurrentIndex(0)
|
||||||
|
|
||||||
|
if item_name in self.queries.keys():
|
||||||
|
del(self.queries[item_name])
|
||||||
|
self.queries.commit()
|
||||||
|
|
||||||
|
def s_r_save_query(self, *args):
|
||||||
|
name, ok = QInputDialog.getText(self, _('Save search/replace'),
|
||||||
|
_('Search/replace name:'))
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
new = True
|
||||||
|
name = unicode(name)
|
||||||
|
if name in self.queries.keys():
|
||||||
|
ret = QMessageBox.question(self, _("Save search/replace"),
|
||||||
|
_("That saved search/replace already exists and will be overwritten. "
|
||||||
|
"Are you sure?"),
|
||||||
|
QMessageBox.Ok, QMessageBox.Cancel)
|
||||||
|
if ret == QMessageBox.Cancel:
|
||||||
|
return
|
||||||
|
new = False
|
||||||
|
|
||||||
|
query = {}
|
||||||
|
query['name'] = name
|
||||||
|
query['search_field'] = unicode(self.search_field.currentText())
|
||||||
|
query['search_mode'] = unicode(self.search_mode.currentText())
|
||||||
|
query['s_r_template'] = unicode(self.s_r_template.text())
|
||||||
|
query['search_for'] = unicode(self.search_for.text())
|
||||||
|
query['case_sensitive'] = self.case_sensitive.isChecked()
|
||||||
|
query['replace_with'] = unicode(self.replace_with.text())
|
||||||
|
query['replace_func'] = unicode(self.replace_func.currentText())
|
||||||
|
query['destination_field'] = unicode(self.destination_field.currentText())
|
||||||
|
query['replace_mode'] = unicode(self.replace_mode.currentText())
|
||||||
|
query['comma_separated'] = self.comma_separated.isChecked()
|
||||||
|
query['results_count'] = self.results_count.value()
|
||||||
|
query['starting_from'] = self.starting_from.value()
|
||||||
|
query['multiple_separator'] = unicode(self.multiple_separator.text())
|
||||||
|
|
||||||
|
self.queries[name] = query
|
||||||
|
self.queries.commit()
|
||||||
|
|
||||||
|
if new:
|
||||||
|
self.query_field.blockSignals(True)
|
||||||
|
self.query_field.clear()
|
||||||
|
self.query_field.addItem('')
|
||||||
|
self.query_field.addItems(sorted([q for q in self.queries], key=sort_key))
|
||||||
|
self.query_field.blockSignals(False)
|
||||||
|
self.query_field.setCurrentIndex(self.query_field.findText(name))
|
||||||
|
|
||||||
|
def s_r_query_change(self, item_name):
|
||||||
|
if not item_name:
|
||||||
|
self.s_r_reset_query_fields()
|
||||||
|
return
|
||||||
|
item = self.queries.get(unicode(item_name), None)
|
||||||
|
if item is None:
|
||||||
|
self.s_r_reset_query_fields()
|
||||||
|
return
|
||||||
|
|
||||||
|
def set_index(attr, txt):
|
||||||
|
try:
|
||||||
|
attr.setCurrentIndex(attr.findText(txt))
|
||||||
|
except:
|
||||||
|
attr.setCurrentIndex(0)
|
||||||
|
|
||||||
|
set_index(self.search_mode, item['search_mode'])
|
||||||
|
set_index(self.search_field, item['search_field'])
|
||||||
|
self.s_r_template.setText(item['s_r_template'])
|
||||||
|
self.s_r_template_changed() #simulate gain/loss of focus
|
||||||
|
self.search_for.setText(item['search_for'])
|
||||||
|
self.case_sensitive.setChecked(item['case_sensitive'])
|
||||||
|
self.replace_with.setText(item['replace_with'])
|
||||||
|
set_index(self.replace_func, item['replace_func'])
|
||||||
|
set_index(self.destination_field, item['destination_field'])
|
||||||
|
set_index(self.replace_mode, item['replace_mode'])
|
||||||
|
self.comma_separated.setChecked(item['comma_separated'])
|
||||||
|
self.results_count.setValue(int(item['results_count']))
|
||||||
|
self.starting_from.setValue(int(item['starting_from']))
|
||||||
|
self.multiple_separator.setText(item['multiple_separator'])
|
||||||
|
|
||||||
|
def s_r_reset_query_fields(self):
|
||||||
|
# Don't reset the search mode. The user will probably want to use it
|
||||||
|
# as it was
|
||||||
|
self.search_field.setCurrentIndex(0)
|
||||||
|
self.s_r_template.setText("")
|
||||||
|
self.search_for.setText("")
|
||||||
|
self.case_sensitive.setChecked(False)
|
||||||
|
self.replace_with.setText("")
|
||||||
|
self.replace_func.setCurrentIndex(0)
|
||||||
|
self.destination_field.setCurrentIndex(0)
|
||||||
|
self.replace_mode.setCurrentIndex(0)
|
||||||
|
self.comma_separated.setChecked(True)
|
||||||
|
self.results_count.setValue(999)
|
||||||
|
self.starting_from.setValue(1)
|
||||||
|
self.multiple_separator.setText(" ::: ")
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>850</width>
|
<width>962</width>
|
||||||
<height>650</height>
|
<height>727</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -44,8 +44,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>842</width>
|
<width>954</width>
|
||||||
<height>589</height>
|
<height>666</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
@ -574,7 +574,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
<property name="sizeConstraint">
|
<property name="sizeConstraint">
|
||||||
<enum>QLayout::SetMinimumSize</enum>
|
<enum>QLayout::SetMinimumSize</enum>
|
||||||
</property>
|
</property>
|
||||||
<item row="1" column="0" colspan="3">
|
<item row="0" column="0" colspan="4">
|
||||||
<widget class="QLabel" name="s_r_heading">
|
<widget class="QLabel" name="s_r_heading">
|
||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@ -584,14 +584,91 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="filler">
|
<widget class="QLabel" name="filler">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="0" colspan="3">
|
||||||
|
<widget class="Line" name="line">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="xlabel_22">
|
||||||
|
<property name="text">
|
||||||
|
<string>Load searc&h/replace:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>search_field</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QComboBox" name="query_field">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Select saved search/replace to load.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeType">
|
||||||
|
<enum>QSizePolicy::Fixed</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="save_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Save current search/replace</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Sa&ve</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="remove_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Delete saved search/replace</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Delete</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="xlabel_21">
|
<widget class="QLabel" name="xlabel_21">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Search &field:</string>
|
<string>Search &field:</string>
|
||||||
@ -601,14 +678,14 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="QComboBox" name="search_field">
|
<widget class="QComboBox" name="search_field">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>The name of the field that you want to search</string>
|
<string>The name of the field that you want to search</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="2">
|
<item row="4" column="2">
|
||||||
<layout class="QHBoxLayout" name="HLayout_3">
|
<layout class="QHBoxLayout" name="HLayout_3">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="xlabel_24">
|
<widget class="QLabel" name="xlabel_24">
|
||||||
@ -642,7 +719,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLabel" name="template_label">
|
<widget class="QLabel" name="template_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Te&mplate:</string>
|
<string>Te&mplate:</string>
|
||||||
@ -652,7 +729,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="HistoryLineEdit" name="s_r_template">
|
<widget class="HistoryLineEdit" name="s_r_template">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
@ -665,7 +742,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QLabel" name="xlabel_2">
|
<widget class="QLabel" name="xlabel_2">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Search for:</string>
|
<string>&Search for:</string>
|
||||||
@ -675,7 +752,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="HistoryLineEdit" name="search_for">
|
<widget class="HistoryLineEdit" name="search_for">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
@ -688,7 +765,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="2">
|
<item row="6" column="2">
|
||||||
<widget class="QCheckBox" name="case_sensitive">
|
<widget class="QCheckBox" name="case_sensitive">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string>
|
<string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string>
|
||||||
@ -701,7 +778,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" column="0">
|
||||||
<widget class="QLabel" name="xlabel_4">
|
<widget class="QLabel" name="xlabel_4">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Replace with:</string>
|
<string>&Replace with:</string>
|
||||||
@ -711,14 +788,14 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="7" column="1">
|
||||||
<widget class="HistoryLineEdit" name="replace_with">
|
<widget class="HistoryLineEdit" name="replace_with">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>The replacement text. The matched search text will be replaced with this string</string>
|
<string>The replacement text. The matched search text will be replaced with this string</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="2">
|
<item row="7" column="2">
|
||||||
<layout class="QHBoxLayout" name="verticalLayout">
|
<layout class="QHBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_41">
|
<widget class="QLabel" name="label_41">
|
||||||
@ -753,7 +830,7 @@ field is processed. In regular expression mode, only the matched text is process
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0">
|
<item row="8" column="0">
|
||||||
<widget class="QLabel" name="destination_field_label">
|
<widget class="QLabel" name="destination_field_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Destination field:</string>
|
<string>&Destination field:</string>
|
||||||
@ -763,7 +840,7 @@ field is processed. In regular expression mode, only the matched text is process
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="8" column="1">
|
||||||
<widget class="QComboBox" name="destination_field">
|
<widget class="QComboBox" name="destination_field">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>The field that the text will be put into after all replacements.
|
<string>The field that the text will be put into after all replacements.
|
||||||
@ -771,7 +848,7 @@ If blank, the source field is used if the field is modifiable</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="2">
|
<item row="8" column="2">
|
||||||
<layout class="QHBoxLayout" name="verticalLayout">
|
<layout class="QHBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="replace_mode_label">
|
<widget class="QLabel" name="replace_mode_label">
|
||||||
@ -820,7 +897,7 @@ not multiple and the destination field is multiple</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1" colspan="2">
|
<item row="9" column="1" colspan="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_21">
|
<layout class="QHBoxLayout" name="horizontalLayout_21">
|
||||||
<item>
|
<item>
|
||||||
<spacer name="HSpacer_347">
|
<spacer name="HSpacer_347">
|
||||||
@ -906,7 +983,7 @@ not multiple and the destination field is multiple</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0" colspan="4">
|
<item row="10" column="0" colspan="4">
|
||||||
<widget class="QScrollArea" name="scrollArea11">
|
<widget class="QScrollArea" name="scrollArea11">
|
||||||
<property name="frameShape">
|
<property name="frameShape">
|
||||||
<enum>QFrame::NoFrame</enum>
|
<enum>QFrame::NoFrame</enum>
|
||||||
@ -1030,6 +1107,9 @@ not multiple and the destination field is multiple</string>
|
|||||||
<tabstop>series_numbering_restarts</tabstop>
|
<tabstop>series_numbering_restarts</tabstop>
|
||||||
<tabstop>series_start_number</tabstop>
|
<tabstop>series_start_number</tabstop>
|
||||||
<tabstop>button_box</tabstop>
|
<tabstop>button_box</tabstop>
|
||||||
|
<tabstop>query_field</tabstop>
|
||||||
|
<tabstop>save_button</tabstop>
|
||||||
|
<tabstop>remove_button</tabstop>
|
||||||
<tabstop>search_field</tabstop>
|
<tabstop>search_field</tabstop>
|
||||||
<tabstop>search_mode</tabstop>
|
<tabstop>search_mode</tabstop>
|
||||||
<tabstop>s_r_template</tabstop>
|
<tabstop>s_r_template</tabstop>
|
||||||
@ -1045,6 +1125,23 @@ not multiple and the destination field is multiple</string>
|
|||||||
<tabstop>multiple_separator</tabstop>
|
<tabstop>multiple_separator</tabstop>
|
||||||
<tabstop>test_text</tabstop>
|
<tabstop>test_text</tabstop>
|
||||||
<tabstop>test_result</tabstop>
|
<tabstop>test_result</tabstop>
|
||||||
|
<tabstop>scrollArea</tabstop>
|
||||||
|
<tabstop>central_widget</tabstop>
|
||||||
|
<tabstop>swap_title_and_author</tabstop>
|
||||||
|
<tabstop>clear_series</tabstop>
|
||||||
|
<tabstop>adddate</tabstop>
|
||||||
|
<tabstop>clear_adddate_button</tabstop>
|
||||||
|
<tabstop>apply_adddate</tabstop>
|
||||||
|
<tabstop>pubdate</tabstop>
|
||||||
|
<tabstop>clear_pubdate_button</tabstop>
|
||||||
|
<tabstop>apply_pubdate</tabstop>
|
||||||
|
<tabstop>remove_format</tabstop>
|
||||||
|
<tabstop>change_title_to_title_case</tabstop>
|
||||||
|
<tabstop>remove_conversion_settings</tabstop>
|
||||||
|
<tabstop>cover_generate</tabstop>
|
||||||
|
<tabstop>cover_remove</tabstop>
|
||||||
|
<tabstop>cover_from_fmt</tabstop>
|
||||||
|
<tabstop>scrollArea11</tabstop>
|
||||||
</tabstops>
|
</tabstops>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../../resources/images.qrc"/>
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
@ -16,7 +16,7 @@ class TagEditor(QDialog, Ui_TagEditor):
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.db = db
|
self.db = db
|
||||||
self.index = db.row(id_)
|
self.index = db.row(id_) if id_ is not None else None
|
||||||
if self.index is not None:
|
if self.index is not None:
|
||||||
tags = self.db.tags(self.index)
|
tags = self.db.tags(self.index)
|
||||||
else:
|
else:
|
||||||
|
@ -43,7 +43,17 @@ p, li { white-space: pre-wrap; }
|
|||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout">
|
<layout class="QVBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="re"/>
|
<widget class="QComboBox" name="re">
|
||||||
|
<property name="editable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="maxCount">
|
||||||
|
<number>10</number>
|
||||||
|
</property>
|
||||||
|
<property name="insertPolicy">
|
||||||
|
<enum>QComboBox::InsertAtTop</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
@ -94,8 +104,8 @@ p, li { white-space: pre-wrap; }
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>301</width>
|
<width>277</width>
|
||||||
<height>234</height>
|
<height>276</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_2">
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
@ -150,13 +150,13 @@ class GuiRunner(QObject):
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Starting up...')
|
prints('Starting up...')
|
||||||
|
|
||||||
def start_gui(self):
|
def start_gui(self, db):
|
||||||
from calibre.gui2.ui import Main
|
from calibre.gui2.ui import Main
|
||||||
main = Main(self.opts, gui_debug=self.gui_debug)
|
main = Main(self.opts, gui_debug=self.gui_debug)
|
||||||
if self.splash_screen is not None:
|
if self.splash_screen is not None:
|
||||||
self.splash_screen.showMessage(_('Initializing user interface...'))
|
self.splash_screen.showMessage(_('Initializing user interface...'))
|
||||||
self.splash_screen.finish(main)
|
self.splash_screen.finish(main)
|
||||||
main.initialize(self.library_path, self.db, self.listener, self.actions)
|
main.initialize(self.library_path, db, self.listener, self.actions)
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
prints('Started up in', time.time() - self.startup_time)
|
prints('Started up in', time.time() - self.startup_time)
|
||||||
add_filesystem_book = partial(main.iactions['Add Books'].add_filesystem_book, allow_device=False)
|
add_filesystem_book = partial(main.iactions['Add Books'].add_filesystem_book, allow_device=False)
|
||||||
@ -200,8 +200,7 @@ class GuiRunner(QObject):
|
|||||||
det_msg=traceback.format_exc(), show=True)
|
det_msg=traceback.format_exc(), show=True)
|
||||||
self.initialization_failed()
|
self.initialization_failed()
|
||||||
|
|
||||||
self.db = db
|
self.start_gui(db)
|
||||||
self.start_gui()
|
|
||||||
|
|
||||||
def initialize_db(self):
|
def initialize_db(self):
|
||||||
db = None
|
db = None
|
||||||
|
@ -77,9 +77,9 @@ class TitleEdit(EnLineEdit):
|
|||||||
def commit(self, db, id_):
|
def commit(self, db, id_):
|
||||||
title = self.current_val
|
title = self.current_val
|
||||||
if self.COMMIT:
|
if self.COMMIT:
|
||||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
|
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False)
|
||||||
else:
|
else:
|
||||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
|
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False,
|
||||||
commit=False)
|
commit=False)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from functools import partial
|
|||||||
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
|
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
|
||||||
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
|
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
|
||||||
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
|
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
|
||||||
QSizePolicy
|
QSizePolicy, QPalette, QFrame, QSize
|
||||||
|
|
||||||
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 import ResizableDialog, error_dialog, gprefs
|
from calibre.gui2 import ResizableDialog, error_dialog, gprefs
|
||||||
@ -22,9 +22,11 @@ from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
|
|||||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
|
|
||||||
class MetadataSingleDialog(ResizableDialog):
|
class MetadataSingleDialogBase(ResizableDialog):
|
||||||
|
|
||||||
view_format = pyqtSignal(object)
|
view_format = pyqtSignal(object)
|
||||||
|
cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields']
|
||||||
|
one_line_comments_toolbar = False
|
||||||
|
|
||||||
def __init__(self, db, parent=None):
|
def __init__(self, db, parent=None):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -61,13 +63,11 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
self.l.addWidget(self.button_box)
|
self.l.addWidget(self.button_box)
|
||||||
|
|
||||||
self.setWindowIcon(QIcon(I('edit_input.png')))
|
self.setWindowIcon(QIcon(I('edit_input.png')))
|
||||||
self.setWindowTitle(_('Edit Meta Information'))
|
self.setWindowTitle(_('Edit Metadata'))
|
||||||
|
|
||||||
self.create_basic_metadata_widgets()
|
self.create_basic_metadata_widgets()
|
||||||
|
|
||||||
if len(self.db.custom_column_label_map) == 0:
|
if len(self.db.custom_column_label_map):
|
||||||
self.central_widget.tabBar().setVisible(False)
|
|
||||||
else:
|
|
||||||
self.create_custom_metadata_widgets()
|
self.create_custom_metadata_widgets()
|
||||||
|
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
'Using this button to create author sort will change author sort from'
|
'Using this button to create author sort will change author sort from'
|
||||||
' red to green.'))
|
' red to green.'))
|
||||||
self.author_sort = AuthorSortEdit(self, self.authors,
|
self.author_sort = AuthorSortEdit(self, self.authors,
|
||||||
self.deduce_author_sort_button, db)
|
self.deduce_author_sort_button, self.db)
|
||||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||||
|
|
||||||
self.swap_title_author_button = QToolButton(self)
|
self.swap_title_author_button = QToolButton(self)
|
||||||
@ -127,7 +127,7 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
self.cover = Cover(self)
|
self.cover = Cover(self)
|
||||||
self.basic_metadata_widgets.append(self.cover)
|
self.basic_metadata_widgets.append(self.cover)
|
||||||
|
|
||||||
self.comments = CommentsEdit(self)
|
self.comments = CommentsEdit(self, self.one_line_comments_toolbar)
|
||||||
self.basic_metadata_widgets.append(self.comments)
|
self.basic_metadata_widgets.append(self.comments)
|
||||||
|
|
||||||
self.rating = RatingEdit(self)
|
self.rating = RatingEdit(self)
|
||||||
@ -166,19 +166,213 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
w.setLayout(layout)
|
w.setLayout(layout)
|
||||||
self.custom_metadata_widgets, self.__cc_spacers = \
|
self.custom_metadata_widgets, self.__cc_spacers = \
|
||||||
populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
|
populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
|
||||||
two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
|
two_column=self.cc_two_column)
|
||||||
self.__custom_col_layouts = [layout]
|
self.__custom_col_layouts = [layout]
|
||||||
ans = self.custom_metadata_widgets
|
|
||||||
for i in range(len(ans)-1):
|
|
||||||
if len(ans[i+1].widgets) == 2:
|
|
||||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
|
|
||||||
else:
|
|
||||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
|
|
||||||
for c in range(2, len(ans[i].widgets), 2):
|
|
||||||
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def do_layout(self): # {{{
|
def set_custom_metadata_tab_order(self, before=None, after=None): # {{{
|
||||||
|
sto = QWidget.setTabOrder
|
||||||
|
if getattr(self, 'custom_metadata_widgets', []):
|
||||||
|
ans = self.custom_metadata_widgets
|
||||||
|
for i in range(len(ans)-1):
|
||||||
|
if before is not None and i == 0:
|
||||||
|
pass# Do something
|
||||||
|
if len(ans[i+1].widgets) == 2:
|
||||||
|
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
|
||||||
|
else:
|
||||||
|
sto(ans[i].widgets[-1], ans[i+1].widgets[0])
|
||||||
|
for c in range(2, len(ans[i].widgets), 2):
|
||||||
|
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
||||||
|
if after is not None:
|
||||||
|
pass # Do something
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def do_layout(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __call__(self, id_):
|
||||||
|
self.book_id = id_
|
||||||
|
for widget in self.basic_metadata_widgets:
|
||||||
|
widget.initialize(self.db, id_)
|
||||||
|
for widget in self.custom_metadata_widgets:
|
||||||
|
widget.initialize(id_)
|
||||||
|
# Commented out as it doesn't play nice with Next, Prev buttons
|
||||||
|
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
|
||||||
|
|
||||||
|
|
||||||
|
# Miscellaneous interaction methods {{{
|
||||||
|
def update_window_title(self, *args):
|
||||||
|
title = self.title.current_val
|
||||||
|
if len(title) > 50:
|
||||||
|
title = title[:50] + u'\u2026'
|
||||||
|
self.setWindowTitle(_('Edit Metadata') + ' - ' +
|
||||||
|
title)
|
||||||
|
|
||||||
|
def swap_title_author(self, *args):
|
||||||
|
title = self.title.current_val
|
||||||
|
self.title.current_val = authors_to_string(self.authors.current_val)
|
||||||
|
self.authors.current_val = string_to_authors(title)
|
||||||
|
self.title_sort.auto_generate()
|
||||||
|
self.author_sort.auto_generate()
|
||||||
|
|
||||||
|
def remove_unused_series(self, *args):
|
||||||
|
self.db.remove_unused_series()
|
||||||
|
idx = self.series.current_val
|
||||||
|
self.series.clear()
|
||||||
|
self.series.initialize(self.db, self.book_id)
|
||||||
|
if idx:
|
||||||
|
for i in range(self.series.count()):
|
||||||
|
if unicode(self.series.itemText(i)) == idx:
|
||||||
|
self.series.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
def tags_editor(self, *args):
|
||||||
|
self.tags.edit(self.db, self.book_id)
|
||||||
|
|
||||||
|
def metadata_from_format(self, *args):
|
||||||
|
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
||||||
|
self.book_id)
|
||||||
|
if mi is not None:
|
||||||
|
self.update_from_mi(mi)
|
||||||
|
|
||||||
|
def cover_from_format(self, *args):
|
||||||
|
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
||||||
|
self.book_id)
|
||||||
|
if mi is None:
|
||||||
|
return
|
||||||
|
cdata = None
|
||||||
|
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||||
|
cdata = open(mi.cover).read()
|
||||||
|
elif mi.cover_data[1] is not None:
|
||||||
|
cdata = mi.cover_data[1]
|
||||||
|
if cdata is None:
|
||||||
|
error_dialog(self, _('Could not read cover'),
|
||||||
|
_('Could not read cover from %s format')%ext).exec_()
|
||||||
|
return
|
||||||
|
orig = self.cover.current_val
|
||||||
|
self.cover.current_val = cdata
|
||||||
|
if self.cover.current_val is None:
|
||||||
|
self.cover.current_val = orig
|
||||||
|
return error_dialog(self, _('Could not read cover'),
|
||||||
|
_('The cover in the %s format is invalid')%ext,
|
||||||
|
show=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def update_from_mi(self, mi):
|
||||||
|
if not mi.is_null('title'):
|
||||||
|
self.title.current_val = mi.title
|
||||||
|
if not mi.is_null('authors'):
|
||||||
|
self.authors.current_val = mi.authors
|
||||||
|
if not mi.is_null('author_sort'):
|
||||||
|
self.author_sort.current_val = mi.author_sort
|
||||||
|
if not mi.is_null('rating'):
|
||||||
|
try:
|
||||||
|
self.rating.current_val = mi.rating
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not mi.is_null('publisher'):
|
||||||
|
self.publisher.current_val = mi.publisher
|
||||||
|
if not mi.is_null('tags'):
|
||||||
|
self.tags.current_val = mi.tags
|
||||||
|
if not mi.is_null('isbn'):
|
||||||
|
self.isbn.current_val = mi.isbn
|
||||||
|
if not mi.is_null('pubdate'):
|
||||||
|
self.pubdate.current_val = mi.pubdate
|
||||||
|
if not mi.is_null('series') and mi.series.strip():
|
||||||
|
self.series.current_val = mi.series
|
||||||
|
if mi.series_index is not None:
|
||||||
|
self.series_index.current_val = float(mi.series_index)
|
||||||
|
if mi.comments and mi.comments.strip():
|
||||||
|
self.comments.current_val = mi.comments
|
||||||
|
|
||||||
|
def fetch_metadata(self, *args):
|
||||||
|
pass # TODO: fetch metadata
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def apply_changes(self):
|
||||||
|
self.changed.add(self.book_id)
|
||||||
|
for widget in self.basic_metadata_widgets:
|
||||||
|
try:
|
||||||
|
if not widget.commit(self.db, self.book_id):
|
||||||
|
return False
|
||||||
|
except IOError, err:
|
||||||
|
if err.errno == 13: # Permission denied
|
||||||
|
import traceback
|
||||||
|
fname = err.filename if err.filename else 'file'
|
||||||
|
error_dialog(self, _('Permission denied'),
|
||||||
|
_('Could not open %s. Is it being used by another'
|
||||||
|
' program?')%fname, det_msg=traceback.format_exc(),
|
||||||
|
show=True)
|
||||||
|
return False
|
||||||
|
raise
|
||||||
|
for widget in getattr(self, 'custom_metadata_widgets', []):
|
||||||
|
widget.commit(self.book_id)
|
||||||
|
|
||||||
|
self.db.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
self.save_state()
|
||||||
|
if not self.apply_changes():
|
||||||
|
return
|
||||||
|
ResizableDialog.accept(self)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self.save_state()
|
||||||
|
ResizableDialog.reject(self)
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
|
||||||
|
|
||||||
|
# Dialog use methods {{{
|
||||||
|
def start(self, row_list, current_row, view_slot=None):
|
||||||
|
self.row_list = row_list
|
||||||
|
self.current_row = current_row
|
||||||
|
if view_slot is not None:
|
||||||
|
self.view_format.connect(view_slot)
|
||||||
|
self.do_one()
|
||||||
|
ret = self.exec_()
|
||||||
|
self.break_cycles()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def do_one(self, delta=0):
|
||||||
|
self.current_row += delta
|
||||||
|
prev = next_ = None
|
||||||
|
if self.current_row > 0:
|
||||||
|
prev = self.db.title(self.row_list[self.current_row-1])
|
||||||
|
if self.current_row < len(self.row_list) - 1:
|
||||||
|
next_ = self.db.title(self.row_list[self.current_row+1])
|
||||||
|
|
||||||
|
if next_ is not None:
|
||||||
|
tip = _('Save changes and edit the metadata of %s')%next_
|
||||||
|
self.next_button.setToolTip(tip)
|
||||||
|
self.next_button.setVisible(next_ is not None)
|
||||||
|
if prev is not None:
|
||||||
|
tip = _('Save changes and edit the metadata of %s')%prev
|
||||||
|
self.prev_button.setToolTip(tip)
|
||||||
|
self.prev_button.setVisible(prev is not None)
|
||||||
|
self(self.db.id(self.row_list[self.current_row]))
|
||||||
|
|
||||||
|
def break_cycles(self):
|
||||||
|
# Break any reference cycles that could prevent python
|
||||||
|
# from garbage collecting this dialog
|
||||||
|
def disconnect(signal):
|
||||||
|
try:
|
||||||
|
signal.disconnect()
|
||||||
|
except:
|
||||||
|
pass # Fails if view format was never connected
|
||||||
|
disconnect(self.view_format)
|
||||||
|
for b in ('next_button', 'prev_button'):
|
||||||
|
x = getattr(self, b, None)
|
||||||
|
if x is not None:
|
||||||
|
disconnect(x.clicked)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
|
||||||
|
|
||||||
|
def do_layout(self):
|
||||||
|
if len(self.db.custom_column_label_map) == 0:
|
||||||
|
self.central_widget.tabBar().setVisible(False)
|
||||||
self.central_widget.clear()
|
self.central_widget.clear()
|
||||||
self.tabs = []
|
self.tabs = []
|
||||||
self.labels = []
|
self.labels = []
|
||||||
@ -283,181 +477,143 @@ class MetadataSingleDialog(ResizableDialog):
|
|||||||
l.addWidget(self.comments)
|
l.addWidget(self.comments)
|
||||||
self.splitter.addWidget(gb)
|
self.splitter.addWidget(gb)
|
||||||
|
|
||||||
|
self.set_custom_metadata_tab_order()
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def __call__(self, id_):
|
class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
|
||||||
self.book_id = id_
|
|
||||||
for widget in self.basic_metadata_widgets:
|
|
||||||
widget.initialize(self.db, id_)
|
|
||||||
for widget in self.custom_metadata_widgets:
|
|
||||||
widget.initialize(id_)
|
|
||||||
# Commented out as it doesn't play nice with Next, Prev buttons
|
|
||||||
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
|
|
||||||
|
|
||||||
|
cc_two_column = False
|
||||||
|
one_line_comments_toolbar = True
|
||||||
|
|
||||||
def update_window_title(self, *args):
|
def do_layout(self):
|
||||||
title = self.title.current_val
|
self.central_widget.clear()
|
||||||
if len(title) > 50:
|
self.tabs = []
|
||||||
title = title[:50] + u'\u2026'
|
self.labels = []
|
||||||
self.setWindowTitle(_('Edit Meta Information') + ' - ' +
|
sto = QWidget.setTabOrder
|
||||||
title)
|
|
||||||
|
|
||||||
def swap_title_author(self, *args):
|
self.tabs.append(QWidget(self))
|
||||||
title = self.title.current_val
|
self.central_widget.addTab(self.tabs[0], _("&Metadata"))
|
||||||
self.title.current_val = authors_to_string(self.authors.current_val)
|
self.tabs[0].l = QGridLayout()
|
||||||
self.authors.current_val = string_to_authors(title)
|
self.tabs[0].setLayout(self.tabs[0].l)
|
||||||
self.title_sort.auto_generate()
|
|
||||||
self.author_sort.auto_generate()
|
|
||||||
|
|
||||||
def remove_unused_series(self, *args):
|
self.tabs.append(QWidget(self))
|
||||||
self.db.remove_unused_series()
|
self.central_widget.addTab(self.tabs[1], _("&Cover and formats"))
|
||||||
idx = self.series.current_val
|
self.tabs[1].l = QGridLayout()
|
||||||
self.series.clear()
|
self.tabs[1].setLayout(self.tabs[1].l)
|
||||||
self.series.initialize(self.db, self.book_id)
|
|
||||||
if idx:
|
|
||||||
for i in range(self.series.count()):
|
|
||||||
if unicode(self.series.itemText(i)) == idx:
|
|
||||||
self.series.setCurrentIndex(i)
|
|
||||||
break
|
|
||||||
|
|
||||||
def tags_editor(self, *args):
|
# Tab 0
|
||||||
self.tags.edit(self.db, self.book_id)
|
tab0 = self.tabs[0]
|
||||||
|
|
||||||
def metadata_from_format(self, *args):
|
tl = QGridLayout()
|
||||||
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
gb = QGroupBox(_('&Basic metadata'), self.tabs[0])
|
||||||
self.book_id)
|
self.tabs[0].l.addWidget(gb, 0, 0, 1, 1)
|
||||||
if mi is not None:
|
gb.setLayout(tl)
|
||||||
self.update_from_mi(mi)
|
|
||||||
|
|
||||||
def cover_from_format(self, *args):
|
sto(self.button_box, self.title)
|
||||||
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
|
||||||
self.book_id)
|
|
||||||
if mi is None:
|
|
||||||
return
|
|
||||||
cdata = None
|
|
||||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
|
||||||
cdata = open(mi.cover).read()
|
|
||||||
elif mi.cover_data[1] is not None:
|
|
||||||
cdata = mi.cover_data[1]
|
|
||||||
if cdata is None:
|
|
||||||
error_dialog(self, _('Could not read cover'),
|
|
||||||
_('Could not read cover from %s format')%ext).exec_()
|
|
||||||
return
|
|
||||||
orig = self.cover.current_val
|
|
||||||
self.cover.current_val = cdata
|
|
||||||
if self.cover.current_val is None:
|
|
||||||
self.cover.current_val = orig
|
|
||||||
return error_dialog(self, _('Could not read cover'),
|
|
||||||
_('The cover in the %s format is invalid')%ext,
|
|
||||||
show=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
def update_from_mi(self, mi):
|
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
|
||||||
if not mi.is_null('title'):
|
ql = BuddyLabel(widget)
|
||||||
self.title.current_val = mi.title
|
tl.addWidget(ql, row, 1, 1, 1)
|
||||||
if not mi.is_null('authors'):
|
tl.addWidget(widget, row, 2, 1, 1)
|
||||||
self.authors.current_val = mi.authors
|
if button is not None:
|
||||||
if not mi.is_null('author_sort'):
|
tl.addWidget(button, row, 3, span, 1)
|
||||||
self.author_sort.current_val = mi.author_sort
|
if icon is not None:
|
||||||
if not mi.is_null('rating'):
|
button.setIcon(QIcon(I(icon)))
|
||||||
try:
|
if tab_to is not None:
|
||||||
self.rating.current_val = mi.rating
|
if button is not None:
|
||||||
except:
|
sto(widget, button)
|
||||||
pass
|
sto(button, tab_to)
|
||||||
if not mi.is_null('publisher'):
|
else:
|
||||||
self.publisher.current_val = mi.publisher
|
sto(widget, tab_to)
|
||||||
if not mi.is_null('tags'):
|
|
||||||
self.tags.current_val = mi.tags
|
|
||||||
if not mi.is_null('isbn'):
|
|
||||||
self.isbn.current_val = mi.isbn
|
|
||||||
if not mi.is_null('pubdate'):
|
|
||||||
self.pubdate.current_val = mi.pubdate
|
|
||||||
if not mi.is_null('series') and mi.series.strip():
|
|
||||||
self.series.current_val = mi.series
|
|
||||||
if mi.series_index is not None:
|
|
||||||
self.series_index.current_val = float(mi.series_index)
|
|
||||||
if mi.comments and mi.comments.strip():
|
|
||||||
self.comments.current_val = mi.comments
|
|
||||||
|
|
||||||
def fetch_metadata(self, *args):
|
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
|
||||||
pass # TODO: fetch metadata
|
|
||||||
|
|
||||||
def apply_changes(self):
|
create_row(0, self.title, self.title_sort,
|
||||||
self.changed.add(self.book_id)
|
button=self.deduce_title_sort_button, span=2,
|
||||||
for widget in self.basic_metadata_widgets:
|
icon='auto_author_sort.png')
|
||||||
try:
|
create_row(1, self.title_sort, self.authors)
|
||||||
if not widget.commit(self.db, self.book_id):
|
create_row(2, self.authors, self.author_sort,
|
||||||
return False
|
button=self.deduce_author_sort_button,
|
||||||
except IOError, err:
|
span=2, icon='auto_author_sort.png')
|
||||||
if err.errno == 13: # Permission denied
|
create_row(3, self.author_sort, self.series)
|
||||||
import traceback
|
create_row(4, self.series, self.series_index,
|
||||||
fname = err.filename if err.filename else 'file'
|
button=self.remove_unused_series_button, icon='trash.png')
|
||||||
error_dialog(self, _('Permission denied'),
|
create_row(5, self.series_index, self.tags)
|
||||||
_('Could not open %s. Is it being used by another'
|
create_row(6, self.tags, self.rating, button=self.tags_editor_button)
|
||||||
' program?')%fname, det_msg=traceback.format_exc(),
|
create_row(7, self.rating, self.pubdate)
|
||||||
show=True)
|
create_row(8, self.pubdate, self.publisher,
|
||||||
return False
|
button=self.pubdate.clear_button, icon='trash.png')
|
||||||
raise
|
create_row(9, self.publisher, self.timestamp)
|
||||||
for widget in getattr(self, 'custom_metadata_widgets', []):
|
create_row(10, self.timestamp, self.isbn,
|
||||||
widget.commit(self.book_id)
|
button=self.timestamp.clear_button, icon='trash.png')
|
||||||
|
create_row(11, self.isbn, self.comments)
|
||||||
|
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
|
||||||
|
12, 1, 1 ,1)
|
||||||
|
|
||||||
self.db.commit()
|
w = getattr(self, 'custom_metadata_widgets_parent', None)
|
||||||
return True
|
if w is not None:
|
||||||
|
gb = QGroupBox(_('C&ustom metadata'), tab0)
|
||||||
|
gbl = QVBoxLayout()
|
||||||
|
gb.setLayout(gbl)
|
||||||
|
sr = QScrollArea(tab0)
|
||||||
|
sr.setWidgetResizable(True)
|
||||||
|
sr.setBackgroundRole(QPalette.Base)
|
||||||
|
sr.setFrameStyle(QFrame.NoFrame)
|
||||||
|
sr.setWidget(w)
|
||||||
|
gbl.addWidget(sr)
|
||||||
|
self.tabs[0].l.addWidget(gb, 0, 1, 1, 1)
|
||||||
|
sto(self.isbn, gb)
|
||||||
|
|
||||||
def accept(self):
|
w = QGroupBox(_('&Comments'), tab0)
|
||||||
self.save_state()
|
sp = QSizePolicy()
|
||||||
if not self.apply_changes():
|
sp.setVerticalStretch(10)
|
||||||
return
|
sp.setHorizontalPolicy(QSizePolicy.Expanding)
|
||||||
ResizableDialog.accept(self)
|
sp.setVerticalPolicy(QSizePolicy.Expanding)
|
||||||
|
w.setSizePolicy(sp)
|
||||||
|
l = QHBoxLayout()
|
||||||
|
w.setLayout(l)
|
||||||
|
l.addWidget(self.comments)
|
||||||
|
tab0.l.addWidget(w, 1, 0, 1, 2)
|
||||||
|
|
||||||
def reject(self):
|
# Tab 1
|
||||||
self.save_state()
|
tab1 = self.tabs[1]
|
||||||
ResizableDialog.reject(self)
|
|
||||||
|
|
||||||
def save_state(self):
|
wsp = QWidget(tab1)
|
||||||
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
|
wgl = QVBoxLayout()
|
||||||
|
wsp.setLayout(wgl)
|
||||||
|
|
||||||
def start(self, row_list, current_row, view_slot=None):
|
# right-hand side of splitter
|
||||||
self.row_list = row_list
|
gb = QGroupBox(_('Change cover'), tab1)
|
||||||
self.current_row = current_row
|
l = QGridLayout()
|
||||||
if view_slot is not None:
|
gb.setLayout(l)
|
||||||
self.view_format.connect(view_slot)
|
sto(self.swap_title_author_button, self.cover.buttons[0])
|
||||||
self.do_one()
|
for i, b in enumerate(self.cover.buttons[:3]):
|
||||||
ret = self.exec_()
|
l.addWidget(b, 0, i, 1, 1)
|
||||||
self.break_cycles()
|
sto(b, self.cover.buttons[i+1])
|
||||||
return ret
|
hl = QHBoxLayout()
|
||||||
|
for b in self.cover.buttons[3:]:
|
||||||
|
hl.addWidget(b)
|
||||||
|
sto(self.cover.buttons[-2], self.cover.buttons[-1])
|
||||||
|
l.addLayout(hl, 1, 0, 1, 3)
|
||||||
|
wgl.addWidget(gb)
|
||||||
|
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||||
|
QSizePolicy.Expanding))
|
||||||
|
wgl.addWidget(self.fetch_metadata_button)
|
||||||
|
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||||
|
QSizePolicy.Expanding))
|
||||||
|
wgl.addWidget(self.formats_manager)
|
||||||
|
|
||||||
def do_one(self, delta=0):
|
self.splitter = QSplitter(Qt.Horizontal, tab1)
|
||||||
self.current_row += delta
|
tab1.l.addWidget(self.splitter)
|
||||||
prev = next_ = None
|
self.splitter.addWidget(self.cover)
|
||||||
if self.current_row > 0:
|
self.splitter.addWidget(wsp)
|
||||||
prev = self.db.title(self.row_list[self.current_row-1])
|
|
||||||
if self.current_row < len(self.row_list) - 1:
|
|
||||||
next_ = self.db.title(self.row_list[self.current_row+1])
|
|
||||||
|
|
||||||
if next_ is not None:
|
self.formats_manager.formats.setMaximumWidth(10000)
|
||||||
tip = _('Save changes and edit the metadata of %s')%next_
|
self.formats_manager.formats.setIconSize(QSize(64, 64))
|
||||||
self.next_button.setToolTip(tip)
|
|
||||||
self.next_button.setVisible(next_ is not None)
|
# }}}
|
||||||
if prev is not None:
|
|
||||||
tip = _('Save changes and edit the metadata of %s')%prev
|
|
||||||
self.prev_button.setToolTip(tip)
|
|
||||||
self.prev_button.setVisible(prev is not None)
|
|
||||||
self(self.db.id(self.row_list[self.current_row]))
|
|
||||||
|
|
||||||
def break_cycles(self):
|
|
||||||
# Break any reference cycles that could prevent python
|
|
||||||
# from garbage collecting this dialog
|
|
||||||
def disconnect(signal):
|
|
||||||
try:
|
|
||||||
signal.disconnect()
|
|
||||||
except:
|
|
||||||
pass # Fails if view format was never connected
|
|
||||||
disconnect(self.view_format)
|
|
||||||
for b in ('next_button', 'prev_button'):
|
|
||||||
x = getattr(self, b, None)
|
|
||||||
if x is not None:
|
|
||||||
disconnect(x.clicked)
|
|
||||||
|
|
||||||
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
|
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
|
||||||
d = MetadataSingleDialog(db, parent)
|
d = MetadataSingleDialog(db, parent)
|
||||||
@ -467,8 +623,8 @@ def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from PyQt4.Qt import QApplication
|
from PyQt4.Qt import QApplication
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
from calibre.library import db
|
from calibre.library import db as db_
|
||||||
db = db()
|
db = db_()
|
||||||
row_list = list(range(len(db.data)))
|
row_list = list(range(len(db.data)))
|
||||||
edit_metadata(db, row_list, 0)
|
edit_metadata(db, row_list, 0)
|
||||||
|
|
||||||
|
@ -114,6 +114,9 @@ class TagsView(QTreeView): # {{{
|
|||||||
|
|
||||||
def set_database(self, db, tag_match, sort_by):
|
def set_database(self, db, tag_match, sort_by):
|
||||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||||
|
old = getattr(self, '_model', None)
|
||||||
|
if old is not None:
|
||||||
|
old.break_cycles()
|
||||||
self._model = TagsModel(db, parent=self,
|
self._model = TagsModel(db, parent=self,
|
||||||
hidden_categories=self.hidden_categories,
|
hidden_categories=self.hidden_categories,
|
||||||
search_restriction=None,
|
search_restriction=None,
|
||||||
@ -183,7 +186,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
def context_menu_handler(self, action=None, category=None,
|
def context_menu_handler(self, action=None, category=None,
|
||||||
key=None, index=None):
|
key=None, index=None, negate=None):
|
||||||
if not action:
|
if not action:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@ -196,6 +199,9 @@ class TagsView(QTreeView): # {{{
|
|||||||
if action == 'manage_categories':
|
if action == 'manage_categories':
|
||||||
self.user_category_edit.emit(category)
|
self.user_category_edit.emit(category)
|
||||||
return
|
return
|
||||||
|
if action == 'search_category':
|
||||||
|
self.tags_marked.emit(category + ':' + str(not negate))
|
||||||
|
return
|
||||||
if action == 'manage_searches':
|
if action == 'manage_searches':
|
||||||
self.saved_search_edit.emit(category)
|
self.saved_search_edit.emit(category)
|
||||||
return
|
return
|
||||||
@ -265,6 +271,15 @@ class TagsView(QTreeView): # {{{
|
|||||||
m.addAction(col,
|
m.addAction(col,
|
||||||
partial(self.context_menu_handler, action='show', category=col))
|
partial(self.context_menu_handler, action='show', category=col))
|
||||||
|
|
||||||
|
# search by category
|
||||||
|
self.context_menu.addAction(
|
||||||
|
_('Search for books in category %s')%category,
|
||||||
|
partial(self.context_menu_handler, action='search_category',
|
||||||
|
category=key, negate=False))
|
||||||
|
self.context_menu.addAction(
|
||||||
|
_('Search for books not in category %s')%category,
|
||||||
|
partial(self.context_menu_handler, action='search_category',
|
||||||
|
category=key, negate=True))
|
||||||
# Offer specific editors for tags/series/publishers/saved searches
|
# Offer specific editors for tags/series/publishers/saved searches
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
if key in ['tags', 'publisher', 'series'] or \
|
if key in ['tags', 'publisher', 'series'] or \
|
||||||
@ -371,6 +386,9 @@ class TagsView(QTreeView): # {{{
|
|||||||
# model. Reason: it is much easier than reconstructing the browser tree.
|
# model. Reason: it is much easier than reconstructing the browser tree.
|
||||||
def set_new_model(self, filter_categories_by=None):
|
def set_new_model(self, filter_categories_by=None):
|
||||||
try:
|
try:
|
||||||
|
old = getattr(self, '_model', None)
|
||||||
|
if old is not None:
|
||||||
|
old.break_cycles()
|
||||||
self._model = TagsModel(self.db, parent=self,
|
self._model = TagsModel(self.db, parent=self,
|
||||||
hidden_categories=self.hidden_categories,
|
hidden_categories=self.hidden_categories,
|
||||||
search_restriction=self.search_restriction,
|
search_restriction=self.search_restriction,
|
||||||
@ -509,8 +527,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
QAbstractItemModel.__init__(self, parent)
|
QAbstractItemModel.__init__(self, parent)
|
||||||
|
|
||||||
# must do this here because 'QPixmap: Must construct a QApplication
|
# must do this here because 'QPixmap: Must construct a QApplication
|
||||||
# before a QPaintDevice'. The ':' in front avoids polluting either the
|
# before a QPaintDevice'. The ':' at the end avoids polluting either of
|
||||||
# user-defined categories (':' at end) or columns namespaces (no ':').
|
# the other namespaces (alpha, '#', or '@')
|
||||||
iconmap = {}
|
iconmap = {}
|
||||||
for key in category_icon_map:
|
for key in category_icon_map:
|
||||||
iconmap[key] = QIcon(I(category_icon_map[key]))
|
iconmap[key] = QIcon(I(category_icon_map[key]))
|
||||||
@ -544,6 +562,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
tooltip=tt, category_key=r)
|
tooltip=tt, category_key=r)
|
||||||
self.refresh(data=data)
|
self.refresh(data=data)
|
||||||
|
|
||||||
|
def break_cycles(self):
|
||||||
|
self.db = self.root_item = None
|
||||||
|
|
||||||
def mimeTypes(self):
|
def mimeTypes(self):
|
||||||
return ["application/calibre+from_library"]
|
return ["application/calibre+from_library"]
|
||||||
|
|
||||||
@ -681,7 +702,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
tb_cats = self.db.field_metadata
|
tb_cats = self.db.field_metadata
|
||||||
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(),
|
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(),
|
||||||
key=sort_key):
|
key=sort_key):
|
||||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
cat_name = '@' + user_cat # add the '@' to avoid name collision
|
||||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||||
if len(saved_searches().names()):
|
if len(saved_searches().names()):
|
||||||
tb_cats.add_search_category(label='search', name=_('Searches'))
|
tb_cats.add_search_category(label='search', name=_('Searches'))
|
||||||
@ -988,7 +1009,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
continue
|
continue
|
||||||
row_index += 1
|
row_index += 1
|
||||||
if key.endswith(':'):
|
if key.startswith('@'):
|
||||||
# User category, so skip it. The tag will be marked in its real category
|
# User category, so skip it. The tag will be marked in its real category
|
||||||
continue
|
continue
|
||||||
category_item = self.root_item.children[row_index]
|
category_item = self.root_item.children[row_index]
|
||||||
@ -1007,7 +1028,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
|
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def find_node(self, key, txt, start_path):
|
def find_item_node(self, key, txt, start_path):
|
||||||
'''
|
'''
|
||||||
Search for an item (a node) in the tags browser list that matches both
|
Search for an item (a node) in the tags browser list that matches both
|
||||||
the key (exact case-insensitive match) and txt (contains case-
|
the key (exact case-insensitive match) and txt (contains case-
|
||||||
@ -1061,6 +1082,22 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
break
|
break
|
||||||
return self.path_found
|
return self.path_found
|
||||||
|
|
||||||
|
def find_category_node(self, key):
|
||||||
|
'''
|
||||||
|
Search for an category node (a top-level node) in the tags browser list
|
||||||
|
that matches the key (exact case-insensitive match). Returns the path to
|
||||||
|
the node. Paths are as in find_item_node.
|
||||||
|
'''
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for i in xrange(self.rowCount(QModelIndex())):
|
||||||
|
idx = self.index(i, 0, QModelIndex())
|
||||||
|
ckey = idx.internalPointer().category_key
|
||||||
|
if strcmp(ckey, key) == 0:
|
||||||
|
return self.path_for_index(idx)
|
||||||
|
return None
|
||||||
|
|
||||||
def show_item_at_path(self, path, box=False):
|
def show_item_at_path(self, path, box=False):
|
||||||
'''
|
'''
|
||||||
Scroll the browser and open categories to show the item referenced by
|
Scroll the browser and open categories to show the item referenced by
|
||||||
@ -1109,8 +1146,7 @@ class TagBrowserMixin(object): # {{{
|
|||||||
|
|
||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
|
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
|
||||||
self.tags_view.set_database(self.library_view.model().db,
|
self.tags_view.set_database(db, self.tag_match, self.sort_by)
|
||||||
self.tag_match, self.sort_by)
|
|
||||||
self.tags_view.tags_marked.connect(self.search.set_search_string)
|
self.tags_view.tags_marked.connect(self.search.set_search_string)
|
||||||
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
||||||
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
||||||
@ -1347,15 +1383,15 @@ class TagBrowserWidget(QWidget): # {{{
|
|||||||
self.search_button.setFocus(True)
|
self.search_button.setFocus(True)
|
||||||
self.item_search.lineEdit().blockSignals(False)
|
self.item_search.lineEdit().blockSignals(False)
|
||||||
|
|
||||||
colon = txt.find(':')
|
|
||||||
key = None
|
key = None
|
||||||
|
colon = txt.rfind(':') if len(txt) > 2 else 0
|
||||||
if colon > 0:
|
if colon > 0:
|
||||||
key = self.parent.library_view.model().db.\
|
key = self.parent.library_view.model().db.\
|
||||||
field_metadata.search_term_to_field_key(txt[:colon])
|
field_metadata.search_term_to_field_key(txt[:colon])
|
||||||
txt = txt[colon+1:]
|
txt = txt[colon+1:]
|
||||||
|
|
||||||
self.current_find_position = model.find_node(key, txt,
|
self.current_find_position = \
|
||||||
self.current_find_position)
|
model.find_item_node(key, txt, self.current_find_position)
|
||||||
if self.current_find_position:
|
if self.current_find_position:
|
||||||
model.show_item_at_path(self.current_find_position, box=True)
|
model.show_item_at_path(self.current_find_position, box=True)
|
||||||
elif self.item_search.text():
|
elif self.item_search.text():
|
||||||
|
@ -16,7 +16,7 @@ from PyQt4.Qt import Qt, SIGNAL, QTimer, \
|
|||||||
QPixmap, QMenu, QIcon, pyqtSignal, \
|
QPixmap, QMenu, QIcon, pyqtSignal, \
|
||||||
QDialog, \
|
QDialog, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, \
|
QSystemTrayIcon, QApplication, QKeySequence, \
|
||||||
QMessageBox, QHelpEvent
|
QMessageBox, QHelpEvent, QAction
|
||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.constants import __appname__, isosx
|
from calibre.constants import __appname__, isosx
|
||||||
@ -198,6 +198,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.system_tray_icon.activated.connect(
|
self.system_tray_icon.activated.connect(
|
||||||
self.system_tray_icon_activated)
|
self.system_tray_icon_activated)
|
||||||
|
|
||||||
|
self.esc_action = QAction(self)
|
||||||
|
self.addAction(self.esc_action)
|
||||||
|
self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape))
|
||||||
|
self.esc_action.triggered.connect(self.esc)
|
||||||
|
|
||||||
####################### Start spare job server ########################
|
####################### Start spare job server ########################
|
||||||
QTimer.singleShot(1000, self.add_spare_server)
|
QTimer.singleShot(1000, self.add_spare_server)
|
||||||
@ -294,6 +298,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
'the file: %s<p>The '
|
'the file: %s<p>The '
|
||||||
'log will be displayed automatically.')%self.gui_debug, show=True)
|
'log will be displayed automatically.')%self.gui_debug, show=True)
|
||||||
|
|
||||||
|
def esc(self, *args):
|
||||||
|
self.search.clear()
|
||||||
|
|
||||||
def start_content_server(self):
|
def start_content_server(self):
|
||||||
from calibre.library.server.main import start_threaded_server
|
from calibre.library.server.main import start_threaded_server
|
||||||
@ -305,7 +311,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.content_server.state_callback(True)
|
self.content_server.state_callback(True)
|
||||||
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
||||||
|
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
MainWindow.resizeEvent(self, ev)
|
MainWindow.resizeEvent(self, ev)
|
||||||
self.search.setMaximumWidth(self.width()-150)
|
self.search.setMaximumWidth(self.width()-150)
|
||||||
|
@ -16,7 +16,6 @@ from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
|
|||||||
QTimer, QRect
|
QTimer, QRect
|
||||||
|
|
||||||
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
|
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
|
||||||
from calibre.constants import isosx
|
|
||||||
from calibre.gui2.filename_pattern_ui import Ui_Form
|
from calibre.gui2.filename_pattern_ui import Ui_Form
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
@ -67,17 +66,31 @@ class FilenamePattern(QWidget, Ui_Form):
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.connect(self.test_button, SIGNAL('clicked()'), self.do_test)
|
self.connect(self.test_button, SIGNAL('clicked()'), self.do_test)
|
||||||
self.connect(self.re, SIGNAL('returnPressed()'), self.do_test)
|
self.connect(self.re.lineEdit(), SIGNAL('returnPressed()'), self.do_test)
|
||||||
self.initialize()
|
self.re.lineEdit().textChanged.connect(lambda x: self.changed_signal.emit())
|
||||||
self.re.textChanged.connect(lambda x: self.changed_signal.emit())
|
|
||||||
|
|
||||||
def initialize(self, defaults=False):
|
def initialize(self, defaults=False):
|
||||||
|
# Get all itmes in the combobox. If we are resting
|
||||||
|
# to defaults we don't want to lose what the user
|
||||||
|
# has added.
|
||||||
|
val_hist = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
|
||||||
|
self.re.clear()
|
||||||
|
|
||||||
if defaults:
|
if defaults:
|
||||||
val = prefs.defaults['filename_pattern']
|
val = prefs.defaults['filename_pattern']
|
||||||
else:
|
else:
|
||||||
val = prefs['filename_pattern']
|
val = prefs['filename_pattern']
|
||||||
self.re.setText(val)
|
self.re.lineEdit().setText(val)
|
||||||
|
|
||||||
|
val_hist += gprefs.get('filename_pattern_history', ['(?P<title>.+)', '(?P<author>[^_-]+) -?\s*(?P<series>[^_0-9-]*)(?P<series_index>[0-9]*)\s*-\s*(?P<title>[^_].+) ?'])
|
||||||
|
if val in val_hist:
|
||||||
|
del val_hist[val_hist.index(val)]
|
||||||
|
val_hist.insert(0, val)
|
||||||
|
for v in val_hist:
|
||||||
|
# Ensure we don't have duplicate items.
|
||||||
|
if v and self.re.findText(v) == -1:
|
||||||
|
self.re.addItem(v)
|
||||||
|
self.re.setCurrentIndex(0)
|
||||||
|
|
||||||
def do_test(self):
|
def do_test(self):
|
||||||
try:
|
try:
|
||||||
@ -110,12 +123,21 @@ class FilenamePattern(QWidget, Ui_Form):
|
|||||||
|
|
||||||
|
|
||||||
def pattern(self):
|
def pattern(self):
|
||||||
pat = unicode(self.re.text())
|
pat = unicode(self.re.lineEdit().text())
|
||||||
return re.compile(pat)
|
return re.compile(pat)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
pat = self.pattern().pattern
|
pat = self.pattern().pattern
|
||||||
prefs['filename_pattern'] = pat
|
prefs['filename_pattern'] = pat
|
||||||
|
|
||||||
|
history = []
|
||||||
|
history_pats = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
|
||||||
|
for p in history_pats[:14]:
|
||||||
|
# Ensure we don't have duplicate items.
|
||||||
|
if p and p not in history:
|
||||||
|
history.append(p)
|
||||||
|
gprefs['filename_pattern_history'] = history
|
||||||
|
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
|
||||||
@ -304,8 +326,9 @@ class FontFamilyModel(QAbstractListModel):
|
|||||||
return NONE
|
return NONE
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
return QVariant(family)
|
return QVariant(family)
|
||||||
if not isosx and role == Qt.FontRole:
|
if False and role == Qt.FontRole:
|
||||||
# Causes a Qt crash with some fonts on OS X
|
# Causes a Qt crash with some fonts
|
||||||
|
# so disabled.
|
||||||
return QVariant(QFont(family))
|
return QVariant(QFont(family))
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
|
@ -42,6 +42,9 @@ class MetadataBackup(Thread): # {{{
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.keep_running = False
|
self.keep_running = False
|
||||||
|
# Break cycles so that this object doesn't hold references to db
|
||||||
|
self.do_write = self.get_metadata_for_dump = self.clear_dirtied = \
|
||||||
|
self.set_dirtied = self.db = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self.keep_running:
|
while self.keep_running:
|
||||||
@ -132,7 +135,7 @@ def _match(query, value, matchkind):
|
|||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class CacheRow(list):
|
class CacheRow(list): # {{{
|
||||||
|
|
||||||
def __init__(self, db, composites, val):
|
def __init__(self, db, composites, val):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -163,14 +166,16 @@ class CacheRow(list):
|
|||||||
def __getslice__(self, i, j):
|
def __getslice__(self, i, j):
|
||||||
return self.__getitem__(slice(i, j))
|
return self.__getitem__(slice(i, j))
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class ResultCache(SearchQueryParser): # {{{
|
class ResultCache(SearchQueryParser): # {{{
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Stores sorted and filtered metadata in memory.
|
Stores sorted and filtered metadata in memory.
|
||||||
'''
|
'''
|
||||||
def __init__(self, FIELD_MAP, field_metadata):
|
def __init__(self, FIELD_MAP, field_metadata, db_prefs=None):
|
||||||
self.FIELD_MAP = FIELD_MAP
|
self.FIELD_MAP = FIELD_MAP
|
||||||
|
self.db_prefs = db_prefs
|
||||||
self.composites = {}
|
self.composites = {}
|
||||||
for key in field_metadata:
|
for key in field_metadata:
|
||||||
if field_metadata[key]['datatype'] == 'composite':
|
if field_metadata[key]['datatype'] == 'composite':
|
||||||
@ -185,6 +190,11 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
self.build_date_relop_dict()
|
self.build_date_relop_dict()
|
||||||
self.build_numeric_relop_dict()
|
self.build_numeric_relop_dict()
|
||||||
|
|
||||||
|
def break_cycles(self):
|
||||||
|
self._data = self.field_metadata = self.FIELD_MAP = \
|
||||||
|
self.numeric_search_relops = self.date_search_relops = \
|
||||||
|
self.all_search_locations = self.db_prefs = None
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, row):
|
def __getitem__(self, row):
|
||||||
return self._data[self._map_filtered[row]]
|
return self._data[self._map_filtered[row]]
|
||||||
@ -397,6 +407,22 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
matches.add(item[0])
|
matches.add(item[0])
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
def get_user_category_matches(self, location, query, candidates):
|
||||||
|
res = set([])
|
||||||
|
if self.db_prefs is None:
|
||||||
|
return res
|
||||||
|
user_cats = self.db_prefs.get('user_categories', [])
|
||||||
|
if location not in user_cats:
|
||||||
|
return res
|
||||||
|
c = set(candidates)
|
||||||
|
for (item, category, ign) in user_cats[location]:
|
||||||
|
s = self.get_matches(category, '=' + item, candidates=c)
|
||||||
|
c -= s
|
||||||
|
res |= s
|
||||||
|
if query == 'false':
|
||||||
|
return candidates - res
|
||||||
|
return res
|
||||||
|
|
||||||
def get_matches(self, location, query, allow_recursion=True, candidates=None):
|
def get_matches(self, location, query, allow_recursion=True, candidates=None):
|
||||||
matches = set([])
|
matches = set([])
|
||||||
if candidates is None:
|
if candidates is None:
|
||||||
@ -435,6 +461,10 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
return self.get_numeric_matches(location, query[1:],
|
return self.get_numeric_matches(location, query[1:],
|
||||||
candidates, val_func=vf)
|
candidates, val_func=vf)
|
||||||
|
|
||||||
|
# check for user categories
|
||||||
|
if len(location) >= 2 and location.startswith('@'):
|
||||||
|
return self.get_user_category_matches(location[1:], query.lower(),
|
||||||
|
candidates)
|
||||||
# everything else, or 'all' matches
|
# everything else, or 'all' matches
|
||||||
matchkind = CONTAINS_MATCH
|
matchkind = CONTAINS_MATCH
|
||||||
if (len(query) > 1):
|
if (len(query) > 1):
|
||||||
@ -460,6 +490,8 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
for x in range(len(self.FIELD_MAP)):
|
for x in range(len(self.FIELD_MAP)):
|
||||||
col_datatype.append('')
|
col_datatype.append('')
|
||||||
for x in self.field_metadata:
|
for x in self.field_metadata:
|
||||||
|
if x.startswith('@'):
|
||||||
|
continue
|
||||||
if len(self.field_metadata[x]['search_terms']):
|
if len(self.field_metadata[x]['search_terms']):
|
||||||
db_col[x] = self.field_metadata[x]['rec_index']
|
db_col[x] = self.field_metadata[x]['rec_index']
|
||||||
if self.field_metadata[x]['datatype'] not in \
|
if self.field_metadata[x]['datatype'] not in \
|
||||||
|
@ -319,7 +319,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.field_metadata.remove_dynamic_categories()
|
self.field_metadata.remove_dynamic_categories()
|
||||||
tb_cats = self.field_metadata
|
tb_cats = self.field_metadata
|
||||||
for user_cat in sorted(self.prefs.get('user_categories', {}).keys(), key=sort_key):
|
for user_cat in sorted(self.prefs.get('user_categories', {}).keys(), key=sort_key):
|
||||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
cat_name = '@' + user_cat # add the '@' to avoid name collision
|
||||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||||
if len(saved_searches().names()):
|
if len(saved_searches().names()):
|
||||||
tb_cats.add_search_category(label='search', name=_('Searches'))
|
tb_cats.add_search_category(label='search', name=_('Searches'))
|
||||||
@ -332,7 +332,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
self.book_on_device_func = None
|
self.book_on_device_func = None
|
||||||
self.data = ResultCache(self.FIELD_MAP, self.field_metadata)
|
self.data = ResultCache(self.FIELD_MAP, self.field_metadata, db_prefs=self.prefs)
|
||||||
self.search = self.data.search
|
self.search = self.data.search
|
||||||
self.search_getting_ids = self.data.search_getting_ids
|
self.search_getting_ids = self.data.search_getting_ids
|
||||||
self.refresh = functools.partial(self.data.refresh, self)
|
self.refresh = functools.partial(self.data.refresh, self)
|
||||||
@ -362,7 +362,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.last_update_check = self.last_modified()
|
self.last_update_check = self.last_modified()
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
self.data = self.field_metadata = self.prefs = self.listeners = None
|
self.data.break_cycles()
|
||||||
|
self.data = self.field_metadata = self.prefs = self.listeners = \
|
||||||
|
self.refresh_ondevice = None
|
||||||
|
|
||||||
def initialize_database(self):
|
def initialize_database(self):
|
||||||
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
|
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
|
||||||
@ -1241,7 +1243,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if category in icon_map:
|
if category in icon_map:
|
||||||
icon = icon_map[label]
|
icon = icon_map[label]
|
||||||
else:
|
else:
|
||||||
icon = icon_map[':custom']
|
icon = icon_map['custom:']
|
||||||
icon_map[category] = icon
|
icon_map[category] = icon
|
||||||
|
|
||||||
datatype = cat['datatype']
|
datatype = cat['datatype']
|
||||||
@ -1337,11 +1339,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if label in taglist and name in taglist[label]:
|
if label in taglist and name in taglist[label]:
|
||||||
items.append(taglist[label][name])
|
items.append(taglist[label][name])
|
||||||
# else: do nothing, to not include nodes w zero counts
|
# else: do nothing, to not include nodes w zero counts
|
||||||
if len(items):
|
cat_name = '@' + user_cat # add the '@' to avoid name collision
|
||||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
|
||||||
# Not a problem if we accumulate entries in the icon map
|
# Not a problem if we accumulate entries in the icon map
|
||||||
if icon_map is not None:
|
if icon_map is not None:
|
||||||
icon_map[cat_name] = icon_map[':user']
|
icon_map[cat_name] = icon_map['user:']
|
||||||
if sort == 'popularity':
|
if sort == 'popularity':
|
||||||
categories[cat_name] = \
|
categories[cat_name] = \
|
||||||
sorted(items, key=lambda x: x.count, reverse=True)
|
sorted(items, key=lambda x: x.count, reverse=True)
|
||||||
|
@ -16,7 +16,7 @@ class TagsIcons(dict):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
category_icons = ['authors', 'series', 'formats', 'publisher', 'rating',
|
category_icons = ['authors', 'series', 'formats', 'publisher', 'rating',
|
||||||
'news', 'tags', ':custom', ':user', 'search',]
|
'news', 'tags', 'custom:', 'user:', 'search',]
|
||||||
def __init__(self, icon_dict):
|
def __init__(self, icon_dict):
|
||||||
for a in self.category_icons:
|
for a in self.category_icons:
|
||||||
if a not in icon_dict:
|
if a not in icon_dict:
|
||||||
@ -31,8 +31,8 @@ category_icon_map = {
|
|||||||
'rating' : 'rating.png',
|
'rating' : 'rating.png',
|
||||||
'news' : 'news.png',
|
'news' : 'news.png',
|
||||||
'tags' : 'tags.png',
|
'tags' : 'tags.png',
|
||||||
':custom' : 'column.png',
|
'custom:' : 'column.png',
|
||||||
':user' : 'drawer.png',
|
'user:' : 'drawer.png',
|
||||||
'search' : 'search.png'
|
'search' : 'search.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,6 +475,8 @@ class FieldMetadata(dict):
|
|||||||
val = self._tb_cats[key]
|
val = self._tb_cats[key]
|
||||||
if val['is_category'] and val['kind'] in ('user', 'search'):
|
if val['is_category'] and val['kind'] in ('user', 'search'):
|
||||||
del self._tb_cats[key]
|
del self._tb_cats[key]
|
||||||
|
if key in self._search_term_map:
|
||||||
|
del self._search_term_map[key]
|
||||||
|
|
||||||
def cc_series_index_column_for(self, key):
|
def cc_series_index_column_for(self, key):
|
||||||
return self._tb_cats[key]['rec_index'] + 1
|
return self._tb_cats[key]['rec_index'] + 1
|
||||||
@ -485,8 +487,9 @@ class FieldMetadata(dict):
|
|||||||
self._tb_cats[label] = {'table':None, 'column':None,
|
self._tb_cats[label] = {'table':None, 'column':None,
|
||||||
'datatype':None, 'is_multiple':None,
|
'datatype':None, 'is_multiple':None,
|
||||||
'kind':'user', 'name':name,
|
'kind':'user', 'name':name,
|
||||||
'search_terms':[], 'is_custom':False,
|
'search_terms':[label],'is_custom':False,
|
||||||
'is_category':True}
|
'is_category':True}
|
||||||
|
self._add_search_terms_to_map(label, [label])
|
||||||
|
|
||||||
def add_search_category(self, label, name):
|
def add_search_category(self, label, name):
|
||||||
if label in self._tb_cats:
|
if label in self._tb_cats:
|
||||||
|
@ -603,7 +603,7 @@ TXT input supports a number of options to differentiate how paragraphs are detec
|
|||||||
formatting will be applied.
|
formatting will be applied.
|
||||||
|
|
||||||
:guilabel:`Formatting Style: Heuristic`
|
:guilabel:`Formatting Style: Heuristic`
|
||||||
Analyses the document for common chapter headings, scene breaks, and italicized words and applies the
|
Analyzes the document for common chapter headings, scene breaks, and italicized words and applies the
|
||||||
appropriate html markup during conversion.
|
appropriate html markup during conversion.
|
||||||
|
|
||||||
:guilabel:`Formatting Style: Markdown`
|
:guilabel:`Formatting Style: Markdown`
|
||||||
|
@ -478,6 +478,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
|
|||||||
- Focus the search bar
|
- Focus the search bar
|
||||||
* - :kbd:`Shift+Ctrl+F`
|
* - :kbd:`Shift+Ctrl+F`
|
||||||
- Open the advanced search dialog
|
- Open the advanced search dialog
|
||||||
|
* - :kbd:`Esc`
|
||||||
|
- Clear the current search
|
||||||
* - :kbd:`N or F3`
|
* - :kbd:`N or F3`
|
||||||
- Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked)
|
- Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked)
|
||||||
* - :kbd:`Shift+N or Shift+F3`
|
* - :kbd:`Shift+N or Shift+F3`
|
||||||
@ -486,6 +488,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
|
|||||||
- Download metadata and shortcuts
|
- Download metadata and shortcuts
|
||||||
* - :kbd:`Ctrl+R`
|
* - :kbd:`Ctrl+R`
|
||||||
- Restart calibre
|
- Restart calibre
|
||||||
|
* - :kbd:`Shift+Ctrl+E`
|
||||||
|
- Add empty books to calibre
|
||||||
* - :kbd:`Ctrl+Q`
|
* - :kbd:`Ctrl+Q`
|
||||||
- Quit calibre
|
- Quit calibre
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user