mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Sync to trunk.
This commit is contained in:
commit
c1afa4d36e
BIN
resources/images/format-fill-color.png
Normal file
BIN
resources/images/format-fill-color.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
resources/images/format-text-color.png
Normal file
BIN
resources/images/format-text-color.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
resources/images/format-text-heading.png
Normal file
BIN
resources/images/format-text-heading.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 965 B |
BIN
resources/images/insert-link.png
Normal file
BIN
resources/images/insert-link.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
@ -1,64 +1,102 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2008 Kovid Goyal kovid@kovidgoyal.net, 2010 Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
http://www.businessweek.com/magazine/news/articles/business_news.htm
|
||||
www.businessweek.com
|
||||
'''
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class BWmagazine(BasicNewsRecipe):
|
||||
title = 'BusinessWeek Magazine'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Stay up to date with BusinessWeek magazine articles. Read news on international business, personal finances & the economy in the BusinessWeek online magazine.'
|
||||
class BusinessWeek(BasicNewsRecipe):
|
||||
title = 'Business Week'
|
||||
__author__ = 'Kovid Goyal and Darko Miletic'
|
||||
description = 'Read the latest international business news & stock market news. Get updated company profiles, financial advice, global economy and technology news.'
|
||||
publisher = 'Bloomberg L.P.'
|
||||
category = 'news, International Business News, current news in international business,international business articles, personal business, business week magazine, business week magazine articles, business week magazine online, business week online magazine'
|
||||
oldest_article = 10
|
||||
max_articles_per_feed = 100
|
||||
category = 'Business, business news, stock market, stock market news, financial advice, company profiles, financial advice, global economy, technology news'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'utf-8'
|
||||
encoding = 'utf8'
|
||||
use_embedded_content = False
|
||||
language = 'en'
|
||||
INDEX = 'http://www.businessweek.com/magazine/news/articles/business_news.htm'
|
||||
remove_empty_feeds = True
|
||||
publication_type = 'magazine'
|
||||
cover_url = 'http://images.businessweek.com/mz/covers/current_120x160.jpg'
|
||||
|
||||
masthead_url = 'http://assets.businessweek.com/images/bw-logo.png'
|
||||
extra_css = """
|
||||
body{font-family: Helvetica,Arial,sans-serif }
|
||||
img{margin-bottom: 0.4em; display:block}
|
||||
.tagline{color: gray; font-style: italic}
|
||||
.photoCredit{font-size: small; color: gray}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags = [
|
||||
dict(attrs={'class':'inStory'})
|
||||
,dict(name=['meta','link','iframe','base','embed','object','table','th','tr','td'])
|
||||
,dict(attrs={'id':['inset','videoDisplay']})
|
||||
]
|
||||
keep_only_tags = [dict(name='div', attrs={'id':['story-body','storyBody']})]
|
||||
remove_attributes = ['lang']
|
||||
match_regexps = [r'http://www.businessweek.com/.*_page_[1-9].*']
|
||||
|
||||
def parse_index(self):
|
||||
articles = []
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
ditem = soup.find('div',attrs={'id':'column2'})
|
||||
if ditem:
|
||||
for item in ditem.findAll('h3'):
|
||||
title_prefix = ''
|
||||
description = ''
|
||||
feed_link = item.find('a')
|
||||
if feed_link and feed_link.has_key('href'):
|
||||
url = 'http://www.businessweek.com/magazine/' + feed_link['href'].partition('../../')[2]
|
||||
title = title_prefix + self.tag_to_string(feed_link)
|
||||
date = strftime(self.timefmt)
|
||||
articles.append({
|
||||
'title' :title
|
||||
,'date' :date
|
||||
,'url' :url
|
||||
,'description':description
|
||||
})
|
||||
return [(soup.head.title.string, articles)]
|
||||
|
||||
keep_only_tags = dict(name='div', attrs={'id':'storyBody'})
|
||||
feeds = [
|
||||
(u'Top Stories', u'http://www.businessweek.com/topStories/rss/topStories.rss'),
|
||||
(u'Top News' , u'http://www.businessweek.com/rss/bwdaily.rss' ),
|
||||
(u'Asia', u'http://www.businessweek.com/rss/asia.rss'),
|
||||
(u'Autos', u'http://www.businessweek.com/rss/autos/index.rss'),
|
||||
(u'Classic Cars', u'http://rss.businessweek.com/bw_rss/classiccars'),
|
||||
(u'Hybrids', u'http://rss.businessweek.com/bw_rss/hybrids'),
|
||||
(u'Europe', u'http://www.businessweek.com/rss/europe.rss'),
|
||||
(u'Auto Reviews', u'http://rss.businessweek.com/bw_rss/autoreviews'),
|
||||
(u'Innovation & Design', u'http://www.businessweek.com/rss/innovate.rss'),
|
||||
(u'Architecture', u'http://www.businessweek.com/rss/architecture.rss'),
|
||||
(u'Brand Equity', u'http://www.businessweek.com/rss/brandequity.rss'),
|
||||
(u'Auto Design', u'http://www.businessweek.com/rss/carbuff.rss'),
|
||||
(u'Game Room', u'http://rss.businessweek.com/bw_rss/gameroom'),
|
||||
(u'Technology', u'http://www.businessweek.com/rss/technology.rss'),
|
||||
(u'Investing', u'http://rss.businessweek.com/bw_rss/investor'),
|
||||
(u'Small Business', u'http://www.businessweek.com/rss/smallbiz.rss'),
|
||||
(u'Careers', u'http://rss.businessweek.com/bw_rss/careers'),
|
||||
(u'B-Schools', u'http://www.businessweek.com/rss/bschools.rss'),
|
||||
(u'Magazine Selections', u'http://www.businessweek.com/rss/magazine.rss'),
|
||||
(u'CEO Guide to Tech', u'http://www.businessweek.com/rss/ceo_guide_tech.rss'),
|
||||
]
|
||||
|
||||
def get_article_url(self, article):
|
||||
url = article.get('guid', None)
|
||||
if 'podcasts' in url:
|
||||
return None
|
||||
if 'surveys' in url:
|
||||
return None
|
||||
if 'images' in url:
|
||||
return None
|
||||
if 'feedroom' in url:
|
||||
return None
|
||||
if '/magazine/toc/' in url:
|
||||
return None
|
||||
rurl, sep, rest = url.rpartition('?')
|
||||
if rurl:
|
||||
return rurl
|
||||
return rest
|
||||
|
||||
def print_version(self, url):
|
||||
rurl = url.rpartition('?')[0]
|
||||
if rurl == '':
|
||||
rurl = url
|
||||
return rurl.replace('.com/magazine/','.com/print/magazine/')
|
||||
|
||||
if '/news/' in url or '/blog/ in url':
|
||||
return url
|
||||
rurl = url.replace('http://www.businessweek.com/','http://www.businessweek.com/print/')
|
||||
return rurl.replace('/investing/','/investor/')
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for alink in soup.findAll('a'):
|
||||
if alink.string is not None:
|
||||
tstr = alink.string
|
||||
alink.replaceWith(tstr)
|
||||
return soup
|
||||
|
67
resources/recipes/cnd.recipe
Normal file
67
resources/recipes/cnd.recipe
Normal file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Derek Liang <Derek.liang.ca @@@at@@@ gmail.com>'
|
||||
'''
|
||||
cnd.org
|
||||
'''
|
||||
import re
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class TheCND(BasicNewsRecipe):
|
||||
|
||||
title = 'CND'
|
||||
__author__ = 'Derek Liang'
|
||||
description = ''
|
||||
INDEX = 'http://cnd.org'
|
||||
language = 'zh'
|
||||
conversion_options = {'linearize_tables':True}
|
||||
|
||||
remove_tags_before = dict(name='div', id='articleHead')
|
||||
remove_tags_after = dict(id='copyright')
|
||||
remove_tags = [dict(name='table', attrs={'align':'right'}), dict(name='img', attrs={'src':'http://my.cnd.org/images/logo.gif'}), dict(name='hr', attrs={}), dict(name='small', attrs={})]
|
||||
no_stylesheets = True
|
||||
|
||||
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
|
||||
|
||||
def print_version(self, url):
|
||||
if url.find('news/article.php') >= 0:
|
||||
return re.sub("^[^=]*", "http://my.cnd.org/modules/news/print.php?storyid", url)
|
||||
else:
|
||||
return re.sub("^[^=]*", "http://my.cnd.org/modules/wfsection/print.php?articleid", url)
|
||||
|
||||
def parse_index(self):
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
|
||||
feeds = []
|
||||
articles = {}
|
||||
|
||||
for a in soup.findAll('a', attrs={'target':'_cnd'}):
|
||||
url = a['href']
|
||||
if url.find('article.php') < 0 :
|
||||
continue
|
||||
if url.startswith('/'):
|
||||
url = 'http://cnd.org'+url
|
||||
title = self.tag_to_string(a)
|
||||
self.log('\tFound article: ', title, 'at', url)
|
||||
date = a.nextSibling
|
||||
if (date is not None) and len(date)>2:
|
||||
if not articles.has_key(date):
|
||||
articles[date] = []
|
||||
articles[date].append({'title':title, 'url':url, 'description': '', 'date':''})
|
||||
self.log('\t\tAppend to : ', date)
|
||||
|
||||
self.log('log articles', articles)
|
||||
mostCurrent = sorted(articles).pop()
|
||||
self.title = 'CND ' + mostCurrent
|
||||
|
||||
feeds.append((self.title, articles[mostCurrent]))
|
||||
|
||||
return feeds
|
||||
|
||||
def populate_article_metadata(self, article, soup, first):
|
||||
header = soup.find('h3')
|
||||
self.log('header: ' + self.tag_to_string(header))
|
||||
pass
|
||||
|
@ -4,7 +4,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
class LeMonde(BasicNewsRecipe):
|
||||
title = 'Le Monde'
|
||||
__author__ = 'veezh'
|
||||
description = 'Actualités'
|
||||
description = u'Actualit\xe9s'
|
||||
oldest_article = 1
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
|
58
resources/recipes/nrc-nl-epub.recipe
Normal file
58
resources/recipes/nrc-nl-epub.recipe
Normal file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#Based on Lars Jacob's Taz Digiabo recipe
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, veezh'
|
||||
|
||||
'''
|
||||
www.nrc.nl
|
||||
'''
|
||||
import os, urllib2, zipfile
|
||||
import time
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
|
||||
class NRCHandelsblad(BasicNewsRecipe):
|
||||
|
||||
title = u'NRC Handelsblad'
|
||||
description = u'De EPUB-versie van NRC'
|
||||
language = 'nl'
|
||||
lang = 'nl-NL'
|
||||
|
||||
__author__ = 'veezh'
|
||||
|
||||
conversion_options = {
|
||||
'no_default_epub_cover' : True
|
||||
}
|
||||
|
||||
def build_index(self):
|
||||
today = time.strftime("%Y%m%d")
|
||||
domain = "http://digitaleeditie.nrc.nl"
|
||||
|
||||
url = domain + "/digitaleeditie/helekrant/epub/nrc_" + today + ".epub"
|
||||
# print url
|
||||
|
||||
try:
|
||||
f = urllib2.urlopen(url)
|
||||
except urllib2.HTTPError:
|
||||
self.report_progress(0,_('Kan niet inloggen om editie te downloaden'))
|
||||
raise ValueError('Krant van vandaag nog niet beschikbaar')
|
||||
|
||||
tmp = PersistentTemporaryFile(suffix='.epub')
|
||||
self.report_progress(0,_('downloading epub'))
|
||||
tmp.write(f.read())
|
||||
tmp.close()
|
||||
|
||||
zfile = zipfile.ZipFile(tmp.name, 'r')
|
||||
self.report_progress(0,_('extracting epub'))
|
||||
|
||||
zfile.extractall(self.output_dir)
|
||||
|
||||
tmp.close()
|
||||
index = os.path.join(self.output_dir, 'content.opf')
|
||||
|
||||
self.report_progress(1,_('epub downloaded and extracted'))
|
||||
|
||||
return index
|
62
resources/recipes/wenxuecity-znjy.recipe
Normal file
62
resources/recipes/wenxuecity-znjy.recipe
Normal file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Derek Liang <Derek.liang.ca @@@at@@@ gmail.com>'
|
||||
'''
|
||||
wenxuecity.com
|
||||
'''
|
||||
import re
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class TheCND(BasicNewsRecipe):
|
||||
|
||||
title = 'wenxuecity - znjy'
|
||||
__author__ = 'Derek Liang'
|
||||
description = ''
|
||||
INDEX = 'http://bbs.wenxuecity.com/znjy/?elite=1'
|
||||
language = 'zh'
|
||||
conversion_options = {'linearize_tables':True}
|
||||
|
||||
remove_tags_before = dict(name='div', id='message')
|
||||
remove_tags_after = dict(name='div', id='message')
|
||||
remove_tags = [dict(name='div', id='postmeta'), dict(name='div', id='footer')]
|
||||
no_stylesheets = True
|
||||
|
||||
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
|
||||
|
||||
def print_version(self, url):
|
||||
return url + '?print'
|
||||
|
||||
def parse_index(self):
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
|
||||
feeds = []
|
||||
articles = {}
|
||||
|
||||
for a in soup.findAll('a', attrs={'class':'post'}):
|
||||
url = a['href']
|
||||
if url.startswith('/'):
|
||||
url = 'http://bbs.wenxuecity.com'+url
|
||||
title = self.tag_to_string(a)
|
||||
self.log('\tFound article: ', title, ' at:', url)
|
||||
dateReg = re.search( '(\d\d?)/(\d\d?)/(\d\d)', self.tag_to_string(a.parent) )
|
||||
date = '%(y)s/%(m)02d/%(d)02d' % {'y' : dateReg.group(3), 'm' : int(dateReg.group(1)), 'd' : int(dateReg.group(2)) }
|
||||
if not articles.has_key(date):
|
||||
articles[date] = []
|
||||
articles[date].append({'title':title, 'url':url, 'description': '', 'date':''})
|
||||
self.log('\t\tAppend to : ', date)
|
||||
|
||||
self.log('log articles', articles)
|
||||
mostCurrent = sorted(articles).pop()
|
||||
self.title = '文学城 - 子女教育 - ' + mostCurrent
|
||||
|
||||
feeds.append((self.title, articles[mostCurrent]))
|
||||
|
||||
return feeds
|
||||
|
||||
def populate_article_metadata(self, article, soup, first):
|
||||
header = soup.find('h3')
|
||||
self.log('header: ' + self.tag_to_string(header))
|
||||
pass
|
||||
|
@ -46,7 +46,7 @@ class WallStreetJournal(BasicNewsRecipe):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
if self.username is not None and self.password is not None:
|
||||
br.open('http://commerce.wsj.com/auth/login')
|
||||
br.select_form(nr=0)
|
||||
br.select_form(nr=1)
|
||||
br['user'] = self.username
|
||||
br['password'] = self.password
|
||||
res = br.submit()
|
||||
|
@ -474,10 +474,10 @@ from calibre.devices.binatone.driver import README
|
||||
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
||||
from calibre.devices.edge.driver import EDGE
|
||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
|
||||
SOVOS, PICO
|
||||
SOVOS, PICO, SUNSTECH_EB700
|
||||
from calibre.devices.sne.driver import SNE
|
||||
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
|
||||
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD
|
||||
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR
|
||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||
from calibre.devices.kobo.driver import KOBO
|
||||
|
||||
@ -579,7 +579,7 @@ plugins += [
|
||||
ELONEX,
|
||||
TECLAST_K3,
|
||||
NEWSMY,
|
||||
PICO,
|
||||
PICO, SUNSTECH_EB700,
|
||||
IPAPYRUS,
|
||||
SOVOS,
|
||||
EDGE,
|
||||
@ -600,6 +600,7 @@ plugins += [
|
||||
VELOCITYMICRO,
|
||||
PDNOVEL_KOBO,
|
||||
LUMIREAD,
|
||||
ALURATEK_COLOR,
|
||||
ITUNES,
|
||||
]
|
||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||
|
@ -204,3 +204,23 @@ class LUMIREAD(USBMS):
|
||||
with open(cfilepath+'.jpg', 'wb') as f:
|
||||
f.write(metadata.thumbnail[-1])
|
||||
|
||||
class ALURATEK_COLOR(USBMS):
|
||||
|
||||
name = 'Aluratek Color Device Interface'
|
||||
gui_name = 'Aluratek Color'
|
||||
description = _('Communicate with the Aluratek Color')
|
||||
author = 'Kovid Goyal'
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
|
||||
# Ordered list of supported formats
|
||||
FORMATS = ['epub', 'fb2', 'txt', 'pdf']
|
||||
|
||||
VENDOR_ID = [0x1f3a]
|
||||
PRODUCT_ID = [0x1000]
|
||||
BCD = [0x0002]
|
||||
|
||||
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'books'
|
||||
|
||||
VENDOR_NAME = 'USB_2.0'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB_FLASH_DRIVER'
|
||||
|
||||
|
@ -58,9 +58,16 @@ class PRS505(USBMS):
|
||||
SUPPORTS_USE_AUTHOR_SORT = True
|
||||
EBOOK_DIR_MAIN = 'database/media/books'
|
||||
|
||||
ALL_BY_TITLE = _('All by title')
|
||||
ALL_BY_AUTHOR = _('All by author')
|
||||
|
||||
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
|
||||
'to turn into collections on the device. Possibilities include: ')+\
|
||||
'series, tags, authors'
|
||||
'series, tags, authors' +\
|
||||
_('. Two special collections are available: %s:%s and %s:%s. Add '
|
||||
'these values to the list to enable them. The collections will be '
|
||||
'given the name provided after the ":" character.')%(
|
||||
'abt', ALL_BY_TITLE, 'aba', ALL_BY_AUTHOR)
|
||||
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
|
||||
|
||||
plugboard = None
|
||||
@ -151,7 +158,7 @@ class PRS505(USBMS):
|
||||
blists[i] = booklists[i]
|
||||
opts = self.settings()
|
||||
if opts.extra_customization:
|
||||
collections = [x.lower().strip() for x in
|
||||
collections = [x.strip() for x in
|
||||
opts.extra_customization.split(',')]
|
||||
else:
|
||||
collections = []
|
||||
@ -179,6 +186,8 @@ class PRS505(USBMS):
|
||||
self.plugboard_func = pb_func
|
||||
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
return # Disabled as the SONY's don't need this thumbnail anyway and
|
||||
# older models don't auto delete it
|
||||
if metadata.thumbnail and metadata.thumbnail[-1]:
|
||||
path = path.replace('/', os.sep)
|
||||
is_main = path.startswith(self._main_prefix)
|
||||
|
@ -410,6 +410,9 @@ class XMLCache(object):
|
||||
newmi = book.deepcopy_metadata()
|
||||
newmi.template_to_attribute(book, plugboard)
|
||||
newmi.set('_new_book', getattr(book, '_new_book', False))
|
||||
book.set('_pb_title_sort',
|
||||
newmi.get('title_sort', newmi.get('title', None)))
|
||||
book.set('_pb_author_sort', newmi.get('author_sort', ''))
|
||||
else:
|
||||
newmi = book
|
||||
(gtz_count, ltz_count, use_tz_var) = \
|
||||
|
@ -72,3 +72,13 @@ class SOVOS(TECLAST_K3):
|
||||
VENDOR_NAME = 'RK28XX'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC'
|
||||
|
||||
class SUNSTECH_EB700(TECLAST_K3):
|
||||
name = 'Sunstech EB700 device interface'
|
||||
gui_name = 'EB700'
|
||||
description = _('Communicate with the Sunstech EB700 reader.')
|
||||
|
||||
FORMATS = ['epub', 'fb2', 'pdf', 'pdb', 'txt']
|
||||
|
||||
VENDOR_NAME = 'SUNEB700'
|
||||
WINDOWS_MAIN_MEM = 'USB-MSC'
|
||||
|
||||
|
@ -132,9 +132,24 @@ class CollectionsBookList(BookList):
|
||||
use_renaming_rules = prefs['manage_device_metadata'] == 'on_connect'
|
||||
|
||||
collections = {}
|
||||
# This map of sets is used to avoid linear searches when testing for
|
||||
# book equality
|
||||
|
||||
# get the special collection names
|
||||
all_by_author = ''
|
||||
all_by_title = ''
|
||||
ca = []
|
||||
for c in collection_attributes:
|
||||
if c.startswith('aba:') and c[4:]:
|
||||
all_by_author = c[4:]
|
||||
elif c.startswith('abt:') and c[4:]:
|
||||
all_by_title = c[4:]
|
||||
else:
|
||||
ca.append(c.lower())
|
||||
collection_attributes = ca
|
||||
|
||||
for book in self:
|
||||
tsval = book.get('_pb_title_sort',
|
||||
book.get('title_sort', book.get('title', 'zzzz')))
|
||||
asval = book.get('_pb_author_sort', book.get('author_sort', ''))
|
||||
# Make sure we can identify this book via the lpath
|
||||
lpath = getattr(book, 'lpath', None)
|
||||
if lpath is None:
|
||||
@ -211,22 +226,29 @@ class CollectionsBookList(BookList):
|
||||
collections[cat_name] = {}
|
||||
if use_renaming_rules and sort_attr:
|
||||
sort_val = book.get(sort_attr, None)
|
||||
collections[cat_name][lpath] = \
|
||||
(book, sort_val, book.get('title_sort', 'zzzz'))
|
||||
collections[cat_name][lpath] = (book, sort_val, tsval)
|
||||
elif is_series:
|
||||
if doing_dc:
|
||||
collections[cat_name][lpath] = \
|
||||
(book, book.get('series_index', sys.maxint),
|
||||
book.get('title_sort', 'zzzz'))
|
||||
(book, book.get('series_index', sys.maxint), tsval)
|
||||
else:
|
||||
collections[cat_name][lpath] = \
|
||||
(book, book.get(attr+'_index', sys.maxint),
|
||||
book.get('title_sort', 'zzzz'))
|
||||
(book, book.get(attr+'_index', sys.maxint), tsval)
|
||||
else:
|
||||
if lpath not in collections[cat_name]:
|
||||
collections[cat_name][lpath] = \
|
||||
(book, book.get('title_sort', 'zzzz'),
|
||||
book.get('title_sort', 'zzzz'))
|
||||
collections[cat_name][lpath] = (book, tsval, tsval)
|
||||
|
||||
# All books by author
|
||||
if all_by_author:
|
||||
if all_by_author not in collections:
|
||||
collections[all_by_author] = {}
|
||||
collections[all_by_author][lpath] = (book, asval, tsval)
|
||||
# All books by title
|
||||
if all_by_title:
|
||||
if all_by_title not in collections:
|
||||
collections[all_by_title] = {}
|
||||
collections[all_by_title][lpath] = (book, tsval, asval)
|
||||
|
||||
# Sort collections
|
||||
result = {}
|
||||
|
||||
|
@ -55,9 +55,11 @@ except:
|
||||
|
||||
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
|
||||
|
||||
def title_sort(title):
|
||||
def title_sort(title, order=None):
|
||||
if order is None:
|
||||
order = tweaks['title_series_sorting']
|
||||
title = title.strip()
|
||||
if tweaks['title_series_sorting'] == 'strictly_alphabetic':
|
||||
if order == 'strictly_alphabetic':
|
||||
return title
|
||||
if title and title[0] in _ignore_starts:
|
||||
title = title[1:]
|
||||
|
@ -11,12 +11,11 @@ import os, re, uuid, logging
|
||||
from mimetypes import types_map
|
||||
from collections import defaultdict
|
||||
from itertools import count
|
||||
from urlparse import urldefrag, urlparse, urlunparse
|
||||
from urlparse import urldefrag, urlparse, urlunparse, urljoin
|
||||
from urllib import unquote as urlunquote
|
||||
from urlparse import urljoin
|
||||
|
||||
from lxml import etree, html
|
||||
from cssutils import CSSParser
|
||||
from cssutils import CSSParser, parseString, parseStyle, replaceUrls
|
||||
from cssutils.css import CSSRule
|
||||
|
||||
import calibre
|
||||
@ -88,11 +87,11 @@ def XLINK(name):
|
||||
def CALIBRE(name):
|
||||
return '{%s}%s' % (CALIBRE_NS, name)
|
||||
|
||||
_css_url_re = re.compile(r'url\((.*?)\)', re.I)
|
||||
_css_url_re = re.compile(r'url\s*\((.*?)\)', re.I)
|
||||
_css_import_re = re.compile(r'@import "(.*?)"')
|
||||
_archive_re = re.compile(r'[^ ]+')
|
||||
|
||||
def iterlinks(root):
|
||||
def iterlinks(root, find_links_in_css=True):
|
||||
'''
|
||||
Iterate over all links in a OEB Document.
|
||||
|
||||
@ -134,6 +133,8 @@ def iterlinks(root):
|
||||
yield (el, attr, attribs[attr], 0)
|
||||
|
||||
|
||||
if not find_links_in_css:
|
||||
continue
|
||||
if tag == XHTML('style') and el.text:
|
||||
for match in _css_url_re.finditer(el.text):
|
||||
yield (el, None, match.group(1), match.start(1))
|
||||
@ -180,7 +181,7 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
|
||||
'''
|
||||
if resolve_base_href:
|
||||
resolve_base_href(root)
|
||||
for el, attrib, link, pos in iterlinks(root):
|
||||
for el, attrib, link, pos in iterlinks(root, find_links_in_css=False):
|
||||
new_link = link_repl_func(link.strip())
|
||||
if new_link == link:
|
||||
continue
|
||||
@ -203,6 +204,40 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
|
||||
new = cur[:pos] + new_link + cur[pos+len(link):]
|
||||
el.attrib[attrib] = new
|
||||
|
||||
def set_property(v):
|
||||
if v.CSS_PRIMITIVE_VALUE == v.cssValueType and \
|
||||
v.CSS_URI == v.primitiveType:
|
||||
v.setStringValue(v.CSS_URI,
|
||||
link_repl_func(v.getStringValue()))
|
||||
|
||||
for el in root.iter():
|
||||
try:
|
||||
tag = el.tag
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
if tag == XHTML('style') and el.text and \
|
||||
(_css_url_re.search(el.text) is not None or '@import' in
|
||||
el.text):
|
||||
stylesheet = parseString(el.text)
|
||||
replaceUrls(stylesheet, link_repl_func)
|
||||
el.text = '\n'+stylesheet.cssText + '\n'
|
||||
|
||||
if 'style' in el.attrib:
|
||||
text = el.attrib['style']
|
||||
if _css_url_re.search(text) is not None:
|
||||
stext = parseStyle(text)
|
||||
for p in stext.getProperties(all=True):
|
||||
v = p.cssValue
|
||||
if v.CSS_VALUE_LIST == v.cssValueType:
|
||||
for item in v:
|
||||
set_property(item)
|
||||
elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
|
||||
set_property(v)
|
||||
el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r',
|
||||
' ')
|
||||
|
||||
|
||||
|
||||
EPUB_MIME = types_map['.epub']
|
||||
XHTML_MIME = types_map['.xhtml']
|
||||
@ -622,7 +657,10 @@ class Metadata(object):
|
||||
attrib[key] = prefixname(value, nsrmap)
|
||||
if namespace(self.term) == DC11_NS:
|
||||
elem = element(parent, self.term, attrib=attrib)
|
||||
elem.text = self.value
|
||||
try:
|
||||
elem.text = self.value
|
||||
except:
|
||||
elem.text = repr(self.value)
|
||||
else:
|
||||
elem = element(parent, OPF('meta'), attrib=attrib)
|
||||
elem.attrib['name'] = prefixname(self.term, nsrmap)
|
||||
|
@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import posixpath
|
||||
from urlparse import urldefrag
|
||||
from urlparse import urldefrag, urlparse
|
||||
|
||||
from lxml import etree
|
||||
import cssutils
|
||||
@ -67,6 +67,10 @@ class RenameFiles(object): # {{{
|
||||
|
||||
def url_replacer(self, orig_url):
|
||||
url = urlnormalize(orig_url)
|
||||
parts = urlparse(url)
|
||||
if parts.scheme:
|
||||
# Only rewrite local URLs
|
||||
return orig_url
|
||||
path, frag = urldefrag(url)
|
||||
if self.renamed_items_map:
|
||||
orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item)
|
||||
|
@ -138,6 +138,10 @@ class CheckIntegrity(QProgressDialog):
|
||||
'You should check them manually. This can '
|
||||
'happen if you manipulate the files in the '
|
||||
'library folder directly.'), det_msg=det_msg, show=True)
|
||||
else:
|
||||
info_dialog(self, _('No errors found'),
|
||||
_('The integrity check completed with no uncorrectable errors found.'),
|
||||
show=True)
|
||||
self.reset()
|
||||
|
||||
# }}}
|
||||
@ -162,6 +166,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
self.choose_menu = QMenu(self.gui)
|
||||
self.qaction.setMenu(self.choose_menu)
|
||||
|
||||
|
||||
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
|
||||
self.choose_menu.addAction(self.action_choose)
|
||||
|
||||
@ -172,6 +177,11 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
self.delete_menu = QMenu(_('Delete library'))
|
||||
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
|
||||
|
||||
ac = self.create_action(spec=(_('Pick a random book'), 'catalog.png',
|
||||
None, None), attr='action_pick_random')
|
||||
ac.triggered.connect(self.pick_random)
|
||||
self.choose_menu.addAction(ac)
|
||||
|
||||
self.rename_separator = self.choose_menu.addSeparator()
|
||||
|
||||
self.switch_actions = []
|
||||
@ -209,6 +219,12 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
self.maintenance_menu.addAction(ac)
|
||||
self.choose_menu.addMenu(self.maintenance_menu)
|
||||
|
||||
def pick_random(self, *args):
|
||||
import random
|
||||
pick = random.randint(0, self.gui.library_view.model().rowCount(None))
|
||||
self.gui.library_view.set_current_row(pick)
|
||||
self.gui.library_view.scroll_to_row(pick)
|
||||
|
||||
def library_name(self):
|
||||
db = self.gui.library_view.model().db
|
||||
path = db.library_path
|
||||
|
@ -5,18 +5,19 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re, os
|
||||
|
||||
from lxml import html
|
||||
from lxml.html import soupparser
|
||||
|
||||
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
|
||||
QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt, QTabWidget, \
|
||||
QSyntaxHighlighter, QColor, QChar
|
||||
from PyQt4.QtWebKit import QWebView
|
||||
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
|
||||
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
|
||||
from PyQt4.QtWebKit import QWebView, QWebPage
|
||||
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre import xml_replace_entities
|
||||
|
||||
from calibre.gui2 import open_url
|
||||
|
||||
class PageAction(QAction): # {{{
|
||||
|
||||
@ -44,6 +45,18 @@ class PageAction(QAction): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class BlockStyleAction(QAction): # {{{
|
||||
|
||||
def __init__(self, text, name, view):
|
||||
QAction.__init__(self, text, view)
|
||||
self._name = name
|
||||
self.triggered.connect(self.apply_style)
|
||||
|
||||
def apply_style(self, *args):
|
||||
self.parent().exec_command('formatBlock', self._name)
|
||||
|
||||
# }}}
|
||||
|
||||
class EditorWidget(QWebView): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@ -90,14 +103,109 @@ class EditorWidget(QWebView): # {{{
|
||||
ac = PageAction(wac, icon, text, checkable, self)
|
||||
setattr(self, 'action_'+name, ac)
|
||||
|
||||
self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'),
|
||||
self)
|
||||
self.action_color.triggered.connect(self.foreground_color)
|
||||
|
||||
self.action_background = QAction(QIcon(I('format-fill-color')),
|
||||
_('Background color'), self)
|
||||
self.action_background.triggered.connect(self.background_color)
|
||||
|
||||
self.action_block_style = QAction(QIcon(I('format-text-heading')),
|
||||
_('Style text block'), self)
|
||||
self.action_block_style.setToolTip(
|
||||
_('Style the selected text block'))
|
||||
self.block_style_menu = QMenu(self)
|
||||
self.action_block_style.setMenu(self.block_style_menu)
|
||||
self.block_style_actions = []
|
||||
for text, name in [
|
||||
(_('Normal'), 'p'),
|
||||
(_('Heading') +' 1', 'h1'),
|
||||
(_('Heading') +' 2', 'h2'),
|
||||
(_('Heading') +' 3', 'h3'),
|
||||
(_('Heading') +' 4', 'h4'),
|
||||
(_('Heading') +' 5', 'h5'),
|
||||
(_('Heading') +' 6', 'h6'),
|
||||
(_('Pre-formatted'), 'pre'),
|
||||
(_('Blockquote'), 'blockquote'),
|
||||
(_('Address'), 'address'),
|
||||
]:
|
||||
ac = BlockStyleAction(text, name, self)
|
||||
self.block_style_menu.addAction(ac)
|
||||
self.block_style_actions.append(ac)
|
||||
|
||||
self.action_insert_link = QAction(QIcon(I('insert-link.png')),
|
||||
_('Insert link'), self)
|
||||
self.action_insert_link.triggered.connect(self.insert_link)
|
||||
|
||||
self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
|
||||
self.page().linkClicked.connect(self.link_clicked)
|
||||
|
||||
def link_clicked(self, url):
|
||||
open_url(url)
|
||||
|
||||
def foreground_color(self):
|
||||
col = QColorDialog.getColor(Qt.black, self,
|
||||
_('Choose foreground color'), QColorDialog.ShowAlphaChannel)
|
||||
if col.isValid():
|
||||
self.exec_command('foreColor', unicode(col.name()))
|
||||
|
||||
def background_color(self):
|
||||
col = QColorDialog.getColor(Qt.white, self,
|
||||
_('Choose background color'), QColorDialog.ShowAlphaChannel)
|
||||
if col.isValid():
|
||||
self.exec_command('hiliteColor', unicode(col.name()))
|
||||
|
||||
def insert_link(self, *args):
|
||||
link, ok = QInputDialog.getText(self, _('Create link'),
|
||||
_('Enter URL'))
|
||||
if not ok:
|
||||
return
|
||||
url = self.parse_link(unicode(link))
|
||||
if url.isValid():
|
||||
url = unicode(url.toString())
|
||||
self.exec_command('createLink', url)
|
||||
|
||||
def parse_link(self, link):
|
||||
link = link.strip()
|
||||
has_schema = re.match(r'^[a-zA-Z]+:', link)
|
||||
if has_schema is not None:
|
||||
url = QUrl(link, QUrl.TolerantMode)
|
||||
if url.isValid():
|
||||
return url
|
||||
if os.path.exists(link):
|
||||
return QUrl.fromLocalFile(link)
|
||||
|
||||
if has_schema is None:
|
||||
first, _, rest = link.partition('.')
|
||||
prefix = 'http'
|
||||
if first == 'ftp':
|
||||
prefix = 'ftp'
|
||||
url = QUrl(prefix +'://'+link, QUrl.TolerantMode)
|
||||
if url.isValid():
|
||||
return url
|
||||
|
||||
return QUrl(link, QUrl.TolerantMode)
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(150, 150)
|
||||
|
||||
def exec_command(self, cmd, arg=None):
|
||||
frame = self.page().mainFrame()
|
||||
if arg is not None:
|
||||
js = 'document.execCommand("%s", false, "%s");' % (cmd, arg)
|
||||
else:
|
||||
js = 'document.execCommand("%s", false, null);' % cmd
|
||||
frame.evaluateJavaScript(js)
|
||||
|
||||
@dynamic_property
|
||||
def html(self):
|
||||
|
||||
def fget(self):
|
||||
ans = u''
|
||||
check = unicode(self.page().mainFrame().toPlainText()).strip()
|
||||
if not check:
|
||||
return ans
|
||||
try:
|
||||
raw = unicode(self.page().mainFrame().toHtml())
|
||||
raw = xml_to_unicode(raw, strip_encoding_pats=True,
|
||||
@ -348,7 +456,7 @@ class Highlighter(QSyntaxHighlighter):
|
||||
|
||||
# }}}
|
||||
|
||||
class Editor(QWidget):
|
||||
class Editor(QWidget): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
@ -404,6 +512,15 @@ class Editor(QWidget):
|
||||
self.toolbar1.addAction(ac)
|
||||
self.toolbar1.addSeparator()
|
||||
|
||||
self.toolbar1.addAction(self.editor.action_color)
|
||||
self.toolbar1.addAction(self.editor.action_background)
|
||||
self.toolbar1.addSeparator()
|
||||
|
||||
self.toolbar1.addAction(self.editor.action_block_style)
|
||||
w = self.toolbar1.widgetForAction(self.editor.action_block_style)
|
||||
w.setPopupMode(w.InstantPopup)
|
||||
self.toolbar1.addAction(self.editor.action_insert_link)
|
||||
|
||||
self.code_edit.textChanged.connect(self.code_dirtied)
|
||||
self.editor.page().contentsChanged.connect(self.wyswyg_dirtied)
|
||||
|
||||
@ -411,18 +528,21 @@ class Editor(QWidget):
|
||||
def html(self):
|
||||
def fset(self, v):
|
||||
self.editor.html = v
|
||||
return property(fget=lambda self:self.editor.html, fset=fset)
|
||||
def fget(self):
|
||||
self.tabs.setCurrentIndex(0)
|
||||
return self.editor.html
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def change_tab(self, index):
|
||||
#print 'reloading:', (index and self.wyswyg_dirty) or (not index and
|
||||
# self.source_dirty)
|
||||
if index == 1: # changing to code view
|
||||
if self.wyswyg_dirty:
|
||||
self.code_edit.setPlainText(self.html)
|
||||
self.code_edit.setPlainText(self.editor.html)
|
||||
self.wyswyg_dirty = False
|
||||
elif index == 0: #changing to wyswyg
|
||||
if self.source_dirty:
|
||||
self.html = unicode(self.code_edit.toPlainText())
|
||||
self.editor.html = unicode(self.code_edit.toPlainText())
|
||||
self.source_dirty = False
|
||||
|
||||
def wyswyg_dirtied(self, *args):
|
||||
@ -431,6 +551,8 @@ class Editor(QWidget):
|
||||
def code_dirtied(self, *args):
|
||||
self.source_dirty = True
|
||||
|
||||
# }}}
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication([])
|
||||
w = Editor()
|
||||
|
@ -587,8 +587,6 @@ class BulkSeries(BulkBase):
|
||||
else:
|
||||
s_index = self.db.get_custom_extra(book_id, num=self.col_id,
|
||||
index_is_id=True)
|
||||
if s_index is None:
|
||||
s_index = 1.0
|
||||
extras.append(s_index)
|
||||
self.db.set_custom_bulk(book_ids, val, extras=extras,
|
||||
num=self.col_id, notify=notify)
|
||||
|
@ -16,6 +16,7 @@ from calibre.gui2 import error_dialog, NONE, info_dialog, config
|
||||
from calibre.gui2.widgets import ProgressIndicator
|
||||
from calibre import strftime, force_unicode
|
||||
from calibre.customize.ui import get_isbndb_key, set_isbndb_key
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
_hung_fetchers = set([])
|
||||
|
||||
@ -72,27 +73,33 @@ class Matches(QAbstractTableModel):
|
||||
def summary(self, row):
|
||||
return self.matches[row].comments
|
||||
|
||||
def data_as_text(self, book, col):
|
||||
if col == 0 and book.title is not None:
|
||||
return book.title
|
||||
elif col == 1:
|
||||
return ', '.join(book.authors)
|
||||
elif col == 2 and book.author_sort is not None:
|
||||
return book.author_sort
|
||||
elif col == 3 and book.publisher is not None:
|
||||
return book.publisher
|
||||
elif col == 4 and book.isbn is not None:
|
||||
return book.isbn
|
||||
elif col == 5 and hasattr(book.pubdate, 'timetuple'):
|
||||
return strftime('%b %Y', book.pubdate.timetuple())
|
||||
elif col == 6 and book.has_cover:
|
||||
return 'y'
|
||||
elif col == 7 and book.comments:
|
||||
return 'y'
|
||||
return ''
|
||||
|
||||
def data(self, index, role):
|
||||
row, col = index.row(), index.column()
|
||||
book = self.matches[row]
|
||||
if role == Qt.DisplayRole:
|
||||
res = None
|
||||
if col == 0:
|
||||
res = book.title
|
||||
elif col == 1:
|
||||
res = ', '.join(book.authors)
|
||||
elif col == 2:
|
||||
res = book.author_sort
|
||||
elif col == 3:
|
||||
res = book.publisher
|
||||
elif col == 4:
|
||||
res = book.isbn
|
||||
elif col == 5:
|
||||
if hasattr(book.pubdate, 'timetuple'):
|
||||
res = strftime('%b %Y', book.pubdate.timetuple())
|
||||
if not res:
|
||||
return NONE
|
||||
return QVariant(res)
|
||||
res = self.data_as_text(book, col)
|
||||
if col <= 5 and res:
|
||||
return QVariant(res)
|
||||
return NONE
|
||||
elif role == Qt.DecorationRole:
|
||||
if col == 6 and book.has_cover:
|
||||
return self.yes_icon
|
||||
@ -100,6 +107,16 @@ class Matches(QAbstractTableModel):
|
||||
return self.yes_icon
|
||||
return NONE
|
||||
|
||||
def sort(self, col, order, reset=True):
|
||||
if not self.matches:
|
||||
return
|
||||
descending = order == Qt.DescendingOrder
|
||||
self.matches.sort(None,
|
||||
lambda x: sort_key(unicode(force_unicode(self.data_as_text(x, col)))),
|
||||
descending)
|
||||
if reset:
|
||||
self.reset()
|
||||
|
||||
class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
|
||||
HANG_TIME = 75 #seconds
|
||||
@ -136,6 +153,11 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
||||
self.show_summary)
|
||||
self.matches.setMouseTracking(True)
|
||||
# Enabling sorting and setting a sort column will not change the initial
|
||||
# order of the results, as they are filled in later
|
||||
self.matches.setSortingEnabled(True)
|
||||
self.matches.horizontalHeader().sectionClicked.connect(self.show_sort_indicator)
|
||||
self.matches.horizontalHeader().setSortIndicatorShown(False)
|
||||
self.fetch_metadata()
|
||||
self.opt_get_social_metadata.setChecked(config['get_social_metadata'])
|
||||
self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata'])
|
||||
@ -243,3 +265,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
def chosen(self, index):
|
||||
self.matches.setCurrentIndex(index)
|
||||
self.accept()
|
||||
|
||||
def show_sort_indicator(self, *args):
|
||||
self.matches.horizontalHeader().setSortIndicatorShown(True)
|
||||
|
||||
|
@ -12,6 +12,7 @@ from PyQt4 import QtGui
|
||||
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
||||
from calibre.ebooks.metadata.book.base import composite_formatter
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
from calibre.gui2 import error_dialog
|
||||
@ -311,6 +312,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
def prepare_search_and_replace(self):
|
||||
self.search_for.initialize('bulk_edit_search_for')
|
||||
self.replace_with.initialize('bulk_edit_replace_with')
|
||||
self.s_r_template.initialize('bulk_edit_template')
|
||||
self.test_text.initialize('bulk_edit_test_test')
|
||||
self.all_fields = ['']
|
||||
self.writable_fields = ['']
|
||||
@ -325,9 +327,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
if f in ['sort'] or fm[f]['datatype'] == 'composite':
|
||||
self.all_fields.append(f)
|
||||
self.all_fields.sort()
|
||||
self.all_fields.insert(1, '{template}')
|
||||
self.writable_fields.sort()
|
||||
self.search_field.setMaxVisibleItems(20)
|
||||
self.destination_field.setMaxVisibleItems(20)
|
||||
self.search_field.setMaxVisibleItems(25)
|
||||
self.destination_field.setMaxVisibleItems(25)
|
||||
offset = 10
|
||||
self.s_r_number_of_books = min(10, len(self.ids))
|
||||
for i in range(1,self.s_r_number_of_books+1):
|
||||
@ -403,22 +406,28 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
self.test_text.editTextChanged[str].connect(self.s_r_paint_results)
|
||||
self.comma_separated.stateChanged.connect(self.s_r_paint_results)
|
||||
self.case_sensitive.stateChanged.connect(self.s_r_paint_results)
|
||||
self.s_r_template.lost_focus.connect(self.s_r_template_changed)
|
||||
self.central_widget.setCurrentIndex(0)
|
||||
|
||||
self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive)
|
||||
self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive)
|
||||
self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitive)
|
||||
|
||||
self.s_r_search_mode_changed(self.search_mode.currentIndex())
|
||||
|
||||
def s_r_get_field(self, mi, field):
|
||||
if field:
|
||||
if field == '{template}':
|
||||
v = composite_formatter.safe_format\
|
||||
(unicode(self.s_r_template.text()), mi, _('S/R TEMPLATE ERROR'), mi)
|
||||
return [v]
|
||||
fm = self.db.metadata_for_field(field)
|
||||
if field == 'sort':
|
||||
val = mi.get('title_sort', None)
|
||||
else:
|
||||
val = mi.get(field, None)
|
||||
if val is None:
|
||||
val = []
|
||||
val = [] if fm['is_multiple'] else ['']
|
||||
elif not fm['is_multiple']:
|
||||
val = [val]
|
||||
elif field == 'authors':
|
||||
@ -427,7 +436,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
val = []
|
||||
return val
|
||||
|
||||
def s_r_template_changed(self):
|
||||
self.s_r_search_field_changed(self.search_field.currentIndex())
|
||||
|
||||
def s_r_search_field_changed(self, idx):
|
||||
if self.search_mode.currentIndex() != 0 and idx == 1: # Template
|
||||
self.s_r_template.setVisible(True)
|
||||
self.template_label.setVisible(True)
|
||||
else:
|
||||
self.s_r_template.setVisible(False)
|
||||
self.template_label.setVisible(False)
|
||||
for i in range(0, self.s_r_number_of_books):
|
||||
w = getattr(self, 'book_%d_text'%(i+1))
|
||||
mi = self.db.get_metadata(self.ids[i], index_is_id=True)
|
||||
@ -590,11 +608,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
if not dest:
|
||||
dest = source
|
||||
dfm = self.db.field_metadata[dest]
|
||||
|
||||
mi = self.db.get_metadata(id, index_is_id=True,)
|
||||
val = mi.get(source)
|
||||
if val is None:
|
||||
return
|
||||
val = self.s_r_do_regexp(mi)
|
||||
val = self.s_r_do_destination(mi, val)
|
||||
if dfm['is_multiple']:
|
||||
|
@ -508,6 +508,29 @@ Future conversion of these books will use the default settings.</string>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="template_label">
|
||||
<property name="text">
|
||||
<string>Te&mplate:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>s_r_template</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="HistoryLineEdit" name="s_r_template">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Enter a template to be used as the source for the search/replace</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="xlabel_2">
|
||||
<property name="text">
|
||||
<string>&Search for:</string>
|
||||
@ -517,7 +540,7 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="5" column="1">
|
||||
<widget class="HistoryLineEdit" name="search_for">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
@ -530,7 +553,7 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<item row="5" column="2">
|
||||
<widget class="QCheckBox" name="case_sensitive">
|
||||
<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>
|
||||
@ -543,7 +566,7 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="xlabel_4">
|
||||
<property name="text">
|
||||
<string>&Replace with:</string>
|
||||
@ -553,14 +576,14 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<item row="6" column="1">
|
||||
<widget class="HistoryLineEdit" name="replace_with">
|
||||
<property name="toolTip">
|
||||
<string>The replacement text. The matched search text will be replaced with this string</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<item row="6" column="2">
|
||||
<layout class="QHBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_41">
|
||||
@ -595,7 +618,7 @@ field is processed. In regular expression mode, only the matched text is process
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="destination_field_label">
|
||||
<property name="text">
|
||||
<string>&Destination field:</string>
|
||||
@ -605,14 +628,15 @@ field is processed. In regular expression mode, only the matched text is process
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="destination_field">
|
||||
<property name="toolTip">
|
||||
<string>The field that the text will be put into after all replacements. If blank, the source field is used.</string>
|
||||
<string>The field that the text will be put into after all replacements.
|
||||
If blank, the source field is used if the field is modifiable</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<item row="7" column="2">
|
||||
<layout class="QHBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="replace_mode_label">
|
||||
@ -660,7 +684,7 @@ nothing should be put between the original text and the inserted text</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<item row="8" column="1">
|
||||
<widget class="QLabel" name="xlabel_3">
|
||||
<property name="text">
|
||||
<string>Test &text</string>
|
||||
@ -670,7 +694,7 @@ nothing should be put between the original text and the inserted text</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="2">
|
||||
<item row="8" column="2">
|
||||
<widget class="QLabel" name="label_51">
|
||||
<property name="text">
|
||||
<string>Test re&sult</string>
|
||||
@ -791,6 +815,7 @@ nothing should be put between the original text and the inserted text</string>
|
||||
<tabstop>central_widget</tabstop>
|
||||
<tabstop>search_field</tabstop>
|
||||
<tabstop>search_mode</tabstop>
|
||||
<tabstop>s_r_template</tabstop>
|
||||
<tabstop>search_for</tabstop>
|
||||
<tabstop>case_sensitive</tabstop>
|
||||
<tabstop>replace_with</tabstop>
|
||||
|
@ -34,6 +34,7 @@ from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
||||
from calibre.gui2.preferences.social import SocialMetadata
|
||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
from calibre import strftime
|
||||
from calibre.library.comments import comments_to_html
|
||||
|
||||
class CoverFetcher(Thread): # {{{
|
||||
|
||||
@ -195,7 +196,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
_file + _(" is not a valid picture"))
|
||||
d.exec_()
|
||||
else:
|
||||
self.cover_path.setText(_file)
|
||||
self.cover.setPixmap(pix)
|
||||
self.update_cover_tooltip()
|
||||
self.cover_changed = True
|
||||
@ -409,7 +409,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
if mi.series_index is not None:
|
||||
self.series_index.setValue(float(mi.series_index))
|
||||
if mi.comments and mi.comments.strip():
|
||||
self.comments.setPlainText(mi.comments)
|
||||
comments = comments_to_html(mi.comments)
|
||||
self.comments.html = comments
|
||||
|
||||
|
||||
def sync_formats(self):
|
||||
@ -556,7 +557,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
if rating > 0:
|
||||
self.rating.setValue(int(rating/2.))
|
||||
comments = self.db.comments(row)
|
||||
self.comments.setPlainText(comments if comments else '')
|
||||
if comments and comments.strip():
|
||||
comments = comments_to_html(comments)
|
||||
self.comments.html = comments
|
||||
cover = self.db.cover(row)
|
||||
pubdate = db.pubdate(self.id, index_is_id=True)
|
||||
self.pubdate.setDate(QDate(pubdate.year, pubdate.month,
|
||||
@ -806,10 +809,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.pubdate.setDate(QDate(dt.year, dt.month, dt.day))
|
||||
summ = book.comments
|
||||
if summ:
|
||||
prefix = unicode(self.comments.toPlainText())
|
||||
prefix = self.comments.html
|
||||
if prefix:
|
||||
prefix += '\n'
|
||||
self.comments.setPlainText(prefix + summ)
|
||||
self.comments.html = prefix + comments_to_html(summ)
|
||||
if book.rating is not None:
|
||||
self.rating.setValue(int(book.rating))
|
||||
if book.tags:
|
||||
@ -899,7 +902,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.db.set_series_index(self.id, self.series_index.value(),
|
||||
notify=False, commit=False)
|
||||
self.db.set_comment(self.id,
|
||||
unicode(self.comments.toPlainText()).strip(),
|
||||
self.comments.html,
|
||||
notify=False, commit=False)
|
||||
d = self.pubdate.date()
|
||||
d = qt_to_dt(d)
|
||||
@ -936,16 +939,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
QDialog.reject(self, *args)
|
||||
|
||||
def read_state(self):
|
||||
wg = dynamic.get('metasingle_window_geometry', None)
|
||||
ss = dynamic.get('metasingle_splitter_state', None)
|
||||
wg = dynamic.get('metasingle_window_geometry2', None)
|
||||
ss = dynamic.get('metasingle_splitter_state2', None)
|
||||
if wg is not None:
|
||||
self.restoreGeometry(wg)
|
||||
if ss is not None:
|
||||
self.splitter.restoreState(ss)
|
||||
|
||||
def save_state(self):
|
||||
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry()))
|
||||
dynamic.set('metasingle_splitter_state',
|
||||
dynamic.set('metasingle_window_geometry2', bytes(self.saveGeometry()))
|
||||
dynamic.set('metasingle_splitter_state2',
|
||||
bytes(self.splitter.saveState()))
|
||||
|
||||
def break_cycles(self):
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>887</width>
|
||||
<height>750</height>
|
||||
<width>994</width>
|
||||
<height>716</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -43,8 +43,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>879</width>
|
||||
<height>711</height>
|
||||
<width>986</width>
|
||||
<height>677</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
@ -66,8 +66,8 @@
|
||||
<attribute name="title">
|
||||
<string>&Basic metadata</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="0">
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@ -496,28 +496,147 @@ Using this button to create author sort will change author sort from red to gree
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>&Comments</string>
|
||||
<spacer name="verticalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTextEdit" name="comments">
|
||||
<property name="tabChangesFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="acceptRichText">
|
||||
<bool>false</bool>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="layoutWidget_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="bc_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Book Cover</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="ImageView" name="cover" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="_4">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMaximumSize</enum>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Change &cover image:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>cover_button</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="_5">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cover_button">
|
||||
<property name="text">
|
||||
<string>&Browse</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="trim_cover_button">
|
||||
<property name="toolTip">
|
||||
<string>Remove border (if any) from cover</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>T&rim</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/trim.png</normaloff>:/images/trim.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="reset_cover">
|
||||
<property name="toolTip">
|
||||
<string>Reset cover to default</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Remove</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="_6">
|
||||
<item>
|
||||
<widget class="QPushButton" name="fetch_cover_button">
|
||||
<property name="text">
|
||||
<string>Download co&ver</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="generate_cover_button">
|
||||
<property name="toolTip">
|
||||
<string>Generate a default cover based on the title and author</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Generate cover</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="layoutWidget_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="af_group_box">
|
||||
<property name="sizePolicy">
|
||||
@ -546,6 +665,12 @@ Using this button to create author sort will change author sort from red to gree
|
||||
<height>140</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DropOnly</enum>
|
||||
</property>
|
||||
@ -644,129 +769,22 @@ Using this button to create author sort will change author sort from red to gree
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="bc_box">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Book Cover</string>
|
||||
<string>&Comments</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="ImageView" name="cover" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="_4">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMaximumSize</enum>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Change &cover image:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>cover_path</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="_5">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="cover_path">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cover_button">
|
||||
<property name="text">
|
||||
<string>&Browse</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="trim_cover_button">
|
||||
<property name="toolTip">
|
||||
<string>Remove border (if any) from cover</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>T&rim</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/trim.png</normaloff>:/images/trim.png</iconset>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="reset_cover">
|
||||
<property name="toolTip">
|
||||
<string>Reset cover to default</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="_6">
|
||||
<item>
|
||||
<widget class="QPushButton" name="fetch_cover_button">
|
||||
<property name="text">
|
||||
<string>Download co&ver</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="generate_cover_button">
|
||||
<property name="toolTip">
|
||||
<string>Generate a default cover based on the title and author</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Generate cover</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="Editor" name="comments" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@ -828,6 +846,12 @@ Using this button to create author sort will change author sort from red to gree
|
||||
<header>calibre/gui2/widgets.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>Editor</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">calibre/gui2/comments_editor.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>title</tabstop>
|
||||
@ -848,13 +872,11 @@ Using this button to create author sort will change author sort from red to gree
|
||||
<tabstop>date</tabstop>
|
||||
<tabstop>pubdate</tabstop>
|
||||
<tabstop>fetch_metadata_button</tabstop>
|
||||
<tabstop>comments</tabstop>
|
||||
<tabstop>button_set_cover</tabstop>
|
||||
<tabstop>button_set_metadata</tabstop>
|
||||
<tabstop>formats</tabstop>
|
||||
<tabstop>add_format_button</tabstop>
|
||||
<tabstop>remove_format_button</tabstop>
|
||||
<tabstop>cover_path</tabstop>
|
||||
<tabstop>cover_button</tabstop>
|
||||
<tabstop>trim_cover_button</tabstop>
|
||||
<tabstop>reset_cover</tabstop>
|
||||
|
@ -23,7 +23,6 @@ from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
||||
from calibre.utils.search_query_parser import SearchQueryParser
|
||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
||||
REGEXP_MATCH, MetadataBackup
|
||||
from calibre.library.cli import parse_series_string
|
||||
from calibre import strftime, isbytestring, prepare_string_for_xml
|
||||
from calibre.constants import filesystem_encoding, DEBUG
|
||||
from calibre.gui2.library import DEFAULT_SORT
|
||||
@ -725,9 +724,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
return False
|
||||
val = qt_to_dt(val, as_utc=False)
|
||||
elif typ == 'series':
|
||||
val, s_index = parse_series_string(self.db, label, value.toString())
|
||||
if not val:
|
||||
val = s_index = None
|
||||
val = unicode(value.toString()).strip()
|
||||
elif typ == 'composite':
|
||||
tmpl = unicode(value.toString()).strip()
|
||||
disp = cc['display']
|
||||
|
@ -695,8 +695,10 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
if not index.isValid():
|
||||
return NONE
|
||||
# set up to position at the category label
|
||||
path = self.path_for_index(self.parent(index))
|
||||
# set up to reposition at the same item. We can do this except if
|
||||
# working with the last item and that item is deleted, in which case
|
||||
# we position at the parent label
|
||||
path = index.model().path_for_index(index)
|
||||
val = unicode(value.toString())
|
||||
if not val:
|
||||
error_dialog(self.tags_view, _('Item is blank'),
|
||||
@ -947,18 +949,22 @@ class TagBrowserMixin(object): # {{{
|
||||
for old_id in to_rename[text]:
|
||||
rename_func(old_id, new_name=unicode(text))
|
||||
|
||||
# Clean up everything, as information could have changed for many books.
|
||||
self.library_view.model().refresh()
|
||||
self.tags_view.set_new_model()
|
||||
self.tags_view.recount()
|
||||
self.saved_search.clear()
|
||||
self.search.clear()
|
||||
# Clean up the library view
|
||||
self.do_tag_item_renamed()
|
||||
self.tags_view.set_new_model() # does a refresh for free
|
||||
|
||||
def do_tag_item_renamed(self):
|
||||
# Clean up library view and search
|
||||
self.library_view.model().refresh()
|
||||
self.saved_search.clear()
|
||||
self.search.clear()
|
||||
# get information to redo the selection
|
||||
rows = [r.row() for r in \
|
||||
self.library_view.selectionModel().selectedRows()]
|
||||
m = self.library_view.model()
|
||||
ids = [m.id(r) for r in rows]
|
||||
|
||||
m.refresh(reset=False)
|
||||
m.research()
|
||||
self.library_view.select_rows(ids)
|
||||
# refreshing the tags view happens at the emit()/call() site
|
||||
|
||||
def do_author_sort_edit(self, parent, id):
|
||||
db = self.library_view.model().db
|
||||
|
@ -3,8 +3,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
'''
|
||||
# Imports {{{
|
||||
import os, math, re, glob, sys
|
||||
from base64 import b64encode
|
||||
from functools import partial
|
||||
@ -19,11 +18,14 @@ from calibre.utils.config import Config, StringConfig
|
||||
from calibre.utils.localization import get_language
|
||||
from calibre.gui2.viewer.config_ui import Ui_Dialog
|
||||
from calibre.gui2.viewer.flip import SlideFlip
|
||||
from calibre.gui2.viewer.gestures import Gestures
|
||||
from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig
|
||||
from calibre.constants import iswindows
|
||||
from calibre import prints, guess_type
|
||||
from calibre.gui2.viewer.keys import SHORTCUTS
|
||||
|
||||
# }}}
|
||||
|
||||
bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = \
|
||||
hyphenator = images = hyphen_pats = None
|
||||
|
||||
@ -33,6 +35,7 @@ def load_builtin_fonts():
|
||||
QFontDatabase.addApplicationFont(f)
|
||||
return 'Liberation Serif', 'Liberation Sans', 'Liberation Mono'
|
||||
|
||||
# Config {{{
|
||||
def config(defaults=None):
|
||||
desc = _('Options to customize the ebook viewer')
|
||||
if defaults is None:
|
||||
@ -137,8 +140,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
str(self.hyphenate_default_lang.itemData(idx).toString()))
|
||||
return QDialog.accept(self, *args)
|
||||
|
||||
# }}}
|
||||
|
||||
class Document(QWebPage):
|
||||
class Document(QWebPage): # {{{
|
||||
|
||||
def set_font_settings(self):
|
||||
opts = config().parse()
|
||||
@ -449,7 +453,9 @@ class Document(QWebPage):
|
||||
self.height+amount)
|
||||
self.setPreferredContentsSize(s)
|
||||
|
||||
class EntityDeclarationProcessor(object):
|
||||
# }}}
|
||||
|
||||
class EntityDeclarationProcessor(object): # {{{
|
||||
|
||||
def __init__(self, html):
|
||||
self.declared_entities = {}
|
||||
@ -460,14 +466,16 @@ class EntityDeclarationProcessor(object):
|
||||
self.processed_html = html
|
||||
for key, val in self.declared_entities.iteritems():
|
||||
self.processed_html = self.processed_html.replace('&%s;'%key, val)
|
||||
# }}}
|
||||
|
||||
class DocumentView(QWebView):
|
||||
class DocumentView(QWebView): # {{{
|
||||
|
||||
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
||||
|
||||
def __init__(self, *args):
|
||||
QWebView.__init__(self, *args)
|
||||
self.flipper = SlideFlip(self)
|
||||
self.gestures = Gestures()
|
||||
self.is_auto_repeat_event = False
|
||||
self.debug_javascript = False
|
||||
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
|
||||
@ -953,6 +961,29 @@ class DocumentView(QWebView):
|
||||
self.manager.viewport_resized(self.scroll_fraction)
|
||||
return ret
|
||||
|
||||
def event(self, ev):
|
||||
typ = ev.type()
|
||||
if typ == ev.TouchBegin:
|
||||
try:
|
||||
self.gestures.start_gesture('touch', ev)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
elif typ == ev.TouchEnd:
|
||||
try:
|
||||
gesture = self.gestures.end_gesture('touch', ev, self.rect())
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if gesture is not None:
|
||||
ev.accept()
|
||||
if gesture == 'lineleft':
|
||||
self.next_page()
|
||||
elif gesture == 'lineright':
|
||||
self.previous_page()
|
||||
return True
|
||||
return QWebView.event(self, ev)
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
opos = self.document.ypos
|
||||
ret = QWebView.mouseReleaseEvent(self, ev)
|
||||
@ -961,4 +992,5 @@ class DocumentView(QWebView):
|
||||
self.manager.scrolled(self.scroll_fraction)
|
||||
return ret
|
||||
|
||||
# }}}
|
||||
|
||||
|
61
src/calibre/gui2/viewer/gestures.py
Normal file
61
src/calibre/gui2/viewer/gestures.py
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import time
|
||||
|
||||
class Gestures(object):
|
||||
|
||||
def __init__(self):
|
||||
self.in_progress = {}
|
||||
|
||||
def get_boundary_point(self, event):
|
||||
t = time.time()
|
||||
id_ = None
|
||||
if hasattr(event, 'touchPoints'):
|
||||
tps = list(event.touchPoints())
|
||||
tp = None
|
||||
for t in tps:
|
||||
if t.isPrimary():
|
||||
tp = t
|
||||
break
|
||||
if tp is None:
|
||||
tp = tps[0]
|
||||
gp, p = tp.screenPos(), tp.pos()
|
||||
id_ = tp.id()
|
||||
else:
|
||||
gp, p = event.globalPos(), event.pos()
|
||||
return (t, gp, p, id_)
|
||||
|
||||
def start_gesture(self, typ, event):
|
||||
self.in_progress[typ] = self.get_boundary_point(event)
|
||||
|
||||
def is_in_progress(self, typ):
|
||||
return typ in self.in_progress
|
||||
|
||||
def end_gesture(self, typ, event, widget_rect):
|
||||
if not self.is_in_progress(typ):
|
||||
return
|
||||
start = self.in_progress[typ]
|
||||
end = self.get_boundary_point(event)
|
||||
if start[3] != end[3]:
|
||||
return
|
||||
timespan = end[0] - start[0]
|
||||
start_pos, end_pos = start[1], end[1]
|
||||
xspan = end_pos.x() - start_pos.x()
|
||||
yspan = end_pos.y() - start_pos.y()
|
||||
|
||||
width = widget_rect.width()
|
||||
|
||||
if timespan < 1.1 and abs(xspan) >= width/5. and \
|
||||
abs(yspan) < abs(xspan)/5.:
|
||||
# Quick horizontal gesture
|
||||
return 'line'+('left' if xspan < 0 else 'right')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
@ -524,6 +524,8 @@ class EnComboBox(QComboBox):
|
||||
|
||||
class HistoryLineEdit(QComboBox):
|
||||
|
||||
lost_focus = pyqtSignal()
|
||||
|
||||
def __init__(self, *args):
|
||||
QComboBox.__init__(self, *args)
|
||||
self.setEditable(True)
|
||||
@ -559,6 +561,10 @@ class HistoryLineEdit(QComboBox):
|
||||
def text(self):
|
||||
return self.currentText()
|
||||
|
||||
def focusOutEvent(self, e):
|
||||
QComboBox.focusOutEvent(self, e)
|
||||
self.lost_focus.emit()
|
||||
|
||||
class ComboBoxWithHelp(QComboBox):
|
||||
'''
|
||||
A combobox where item 0 is help text. CurrentText will return '' for item 0.
|
||||
|
@ -445,6 +445,9 @@ class CustomColumns(object):
|
||||
index_is_id=True)
|
||||
val = self.custom_data_adapters[data['datatype']](val, data)
|
||||
|
||||
if data['datatype'] == 'series' and extra is None:
|
||||
(val, extra) = self._get_series_values(val)
|
||||
|
||||
if data['normalized']:
|
||||
if data['datatype'] == 'enumeration' and (
|
||||
val and val not in data['display']['enum_values']):
|
||||
|
@ -2133,9 +2133,27 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
|
||||
self.conn.commit()
|
||||
|
||||
series_index_pat = re.compile(r'(.*)\s+\[([.0-9]+)\]$')
|
||||
|
||||
def _get_series_values(self, val):
|
||||
if not val:
|
||||
return (val, None)
|
||||
match = self.series_index_pat.match(val.strip())
|
||||
if match is not None:
|
||||
idx = match.group(2)
|
||||
try:
|
||||
idx = float(idx)
|
||||
return (match.group(1).strip(), idx)
|
||||
except:
|
||||
pass
|
||||
return (val, None)
|
||||
|
||||
def set_series(self, id, series, notify=True, commit=True):
|
||||
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
|
||||
self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1')
|
||||
self.conn.execute('''DELETE FROM series
|
||||
WHERE (SELECT COUNT(id) FROM books_series_link
|
||||
WHERE series=series.id) < 1''')
|
||||
(series, idx) = self._get_series_values(series)
|
||||
if series:
|
||||
if not isinstance(series, unicode):
|
||||
series = series.decode(preferred_encoding, 'replace')
|
||||
@ -2147,6 +2165,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
else:
|
||||
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
|
||||
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
|
||||
if idx:
|
||||
self.set_series_index(id, idx, notify=notify, commit=commit)
|
||||
self.dirtied([id], commit=False)
|
||||
if commit:
|
||||
self.conn.commit()
|
||||
|
@ -7,6 +7,7 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, traceback, cStringIO, re, shutil
|
||||
from functools import partial
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.utils.config import Config, StringConfig, tweaks
|
||||
@ -139,8 +140,7 @@ class SafeFormat(TemplateFormatter):
|
||||
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||
sanitize_func=ascii_filename, replace_whitespace=False,
|
||||
to_lowercase=False):
|
||||
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
|
||||
tsfmt = title_sort if library_order else lambda x: x
|
||||
tsfmt = partial(title_sort, order=tweaks['save_template_title_series_sorting'])
|
||||
format_args = FORMAT_ARGS.copy()
|
||||
format_args.update(mi.all_non_none_fields())
|
||||
if mi.title:
|
||||
|
@ -16,7 +16,6 @@ from datetime import datetime
|
||||
from functools import partial
|
||||
|
||||
from calibre.ebooks.metadata import title_sort, author_to_author_sort
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.date import parse_date, isoformat
|
||||
from calibre import isbytestring, force_unicode
|
||||
from calibre.constants import iswindows, DEBUG
|
||||
|
@ -181,16 +181,17 @@ How do I use |app| with my iPad/iPhone/iTouch?
|
||||
Over the air
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the *free* Stanza app, available from the Apple app store. You need at least Stanza version 3.0. Stanza allows you to access your |app| collection wirelessly, over the air.
|
||||
|
||||
First perform the following steps in |app|
|
||||
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the calibre sontent server, which makes your collection available over the net. First perform the following steps in |app|
|
||||
|
||||
* Set the Preferred Output Format in |app| to EPUB (The output format can be set under :guilabel:`Preferences->Interface->Behavior`)
|
||||
* Set the output profile to iPad (this will work for iPhone/iPods as well), under :guilabel:`Preferences->Conversion->Common Options->Page Setup`
|
||||
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
||||
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
||||
|
||||
Install the free Stanza reader app on your iPad/iPhone/iTouch using iTunes.
|
||||
Now on your iPad/iPhone you have two choices, use either iBooks (version 1.2 and later) or Stanza (version 3.0 and later). Both are available free from the app store.
|
||||
|
||||
Using Stanza
|
||||
***************
|
||||
|
||||
Now you should be able to access your books on your iPhone by opening Stanza. Go to "Get Books" and then click the "Shared" tab. Under Shared you will see an entry "Books in calibre". If you don't, make sure your iPad/iPhone is connected using the WiFi network in your house, not 3G. If the |app| catalog is still not detected in Stanza, you can add it manually in Stanza. To do this, click the "Shared" tab, then click the "Edit" button and then click "Add book source" to add a new book source. In the Add Book Source screen enter whatever name you like and in the URL field, enter the following::
|
||||
|
||||
@ -200,6 +201,18 @@ Replace ``192.168.1.2`` with the local IP address of the computer running |app|.
|
||||
|
||||
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
|
||||
|
||||
Using iBooks
|
||||
**************
|
||||
|
||||
Start the Safari browser and type in the IP address and port of the computer running the calibre server, like this::
|
||||
|
||||
http://192.168.1.2:8080/
|
||||
|
||||
Replace ``192.168.1.2`` with the local IP address of the computer running |app|. If you have changed the port the |app| content server is running on, you will have to change ``8080`` as well to the new port. The local IP address is the IP address you computer is assigned on your home network. A quick Google search will tell you how to find out your local IP address.
|
||||
|
||||
You wills ee a list of books in Safari, just click on the epub link for whichever book you want to read, Safari will then prompt you to open it with iBooks.
|
||||
|
||||
|
||||
With the USB cable
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -203,16 +203,17 @@ All the functions listed under single-function mode can be used in program mode,
|
||||
|
||||
The following functions are available in addition to those described in single-function mode. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions):
|
||||
|
||||
* ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers.
|
||||
* ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers.
|
||||
* ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression
|
||||
* ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
|
||||
* ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers.
|
||||
* ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers.
|
||||
* ``field(name)`` -- returns the metadata field named by ``name``.
|
||||
* ``multiply`` -- returns x * y. Throws an exception if either x or y are not numbers.
|
||||
* ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers.
|
||||
* ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments.
|
||||
* ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
|
||||
* ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
|
||||
* ``subtract`` -- returns x - y. Throws an exception if either x or y are not numbers.
|
||||
* ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers.
|
||||
* ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value.
|
||||
|
||||
Special notes for save/send templates
|
||||
-------------------------------------
|
||||
|
@ -57,6 +57,11 @@ class _Parser(object):
|
||||
y = float(y if y else 0)
|
||||
return ops[op](x, y)
|
||||
|
||||
def _template(self, template):
|
||||
template = template.replace('[[', '{').replace(']]', '}')
|
||||
return self.parent.safe_format(template, self.parent.kwargs, 'TEMPLATE',
|
||||
self.parent.book)
|
||||
|
||||
local_functions = {
|
||||
'add' : (2, partial(_math, op='+')),
|
||||
'assign' : (2, _assign),
|
||||
@ -68,6 +73,7 @@ class _Parser(object):
|
||||
'strcmp' : (5, _strcmp),
|
||||
'substr' : (3, lambda s, x, y, z: x[int(y): len(x) if int(z) == 0 else int(z)]),
|
||||
'subtract' : (2, partial(_math, op='-')),
|
||||
'template' : (1, _template)
|
||||
}
|
||||
|
||||
def __init__(self, val, prog, parent):
|
||||
|
Loading…
x
Reference in New Issue
Block a user