mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54: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'
|
__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
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class BWmagazine(BasicNewsRecipe):
|
class BusinessWeek(BasicNewsRecipe):
|
||||||
title = 'BusinessWeek Magazine'
|
title = 'Business Week'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Kovid Goyal and 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.'
|
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.'
|
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'
|
category = 'Business, business news, stock market, stock market news, financial advice, company profiles, financial advice, global economy, technology news'
|
||||||
oldest_article = 10
|
oldest_article = 7
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 200
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf-8'
|
encoding = 'utf8'
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'en'
|
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'
|
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 = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
, 'tags' : category
|
, 'tags' : category
|
||||||
, 'publisher' : publisher
|
, 'publisher' : publisher
|
||||||
, 'language' : language
|
, '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):
|
def print_version(self, url):
|
||||||
rurl = url.rpartition('?')[0]
|
if '/news/' in url or '/blog/ in url':
|
||||||
if rurl == '':
|
return url
|
||||||
rurl = url
|
rurl = url.replace('http://www.businessweek.com/','http://www.businessweek.com/print/')
|
||||||
return rurl.replace('.com/magazine/','.com/print/magazine/')
|
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):
|
class LeMonde(BasicNewsRecipe):
|
||||||
title = 'Le Monde'
|
title = 'Le Monde'
|
||||||
__author__ = 'veezh'
|
__author__ = 'veezh'
|
||||||
description = 'Actualités'
|
description = u'Actualit\xe9s'
|
||||||
oldest_article = 1
|
oldest_article = 1
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
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()
|
br = BasicNewsRecipe.get_browser()
|
||||||
if self.username is not None and self.password is not None:
|
if self.username is not None and self.password is not None:
|
||||||
br.open('http://commerce.wsj.com/auth/login')
|
br.open('http://commerce.wsj.com/auth/login')
|
||||||
br.select_form(nr=0)
|
br.select_form(nr=1)
|
||||||
br['user'] = self.username
|
br['user'] = self.username
|
||||||
br['password'] = self.password
|
br['password'] = self.password
|
||||||
res = br.submit()
|
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.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
||||||
from calibre.devices.edge.driver import EDGE
|
from calibre.devices.edge.driver import EDGE
|
||||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
|
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.sne.driver import SNE
|
||||||
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
|
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.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||||
from calibre.devices.kobo.driver import KOBO
|
from calibre.devices.kobo.driver import KOBO
|
||||||
|
|
||||||
@ -579,7 +579,7 @@ plugins += [
|
|||||||
ELONEX,
|
ELONEX,
|
||||||
TECLAST_K3,
|
TECLAST_K3,
|
||||||
NEWSMY,
|
NEWSMY,
|
||||||
PICO,
|
PICO, SUNSTECH_EB700,
|
||||||
IPAPYRUS,
|
IPAPYRUS,
|
||||||
SOVOS,
|
SOVOS,
|
||||||
EDGE,
|
EDGE,
|
||||||
@ -600,6 +600,7 @@ plugins += [
|
|||||||
VELOCITYMICRO,
|
VELOCITYMICRO,
|
||||||
PDNOVEL_KOBO,
|
PDNOVEL_KOBO,
|
||||||
LUMIREAD,
|
LUMIREAD,
|
||||||
|
ALURATEK_COLOR,
|
||||||
ITUNES,
|
ITUNES,
|
||||||
]
|
]
|
||||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
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:
|
with open(cfilepath+'.jpg', 'wb') as f:
|
||||||
f.write(metadata.thumbnail[-1])
|
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
|
SUPPORTS_USE_AUTHOR_SORT = True
|
||||||
EBOOK_DIR_MAIN = 'database/media/books'
|
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 '
|
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
|
||||||
'to turn into collections on the device. Possibilities include: ')+\
|
'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'])
|
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
|
||||||
|
|
||||||
plugboard = None
|
plugboard = None
|
||||||
@ -151,7 +158,7 @@ class PRS505(USBMS):
|
|||||||
blists[i] = booklists[i]
|
blists[i] = booklists[i]
|
||||||
opts = self.settings()
|
opts = self.settings()
|
||||||
if opts.extra_customization:
|
if opts.extra_customization:
|
||||||
collections = [x.lower().strip() for x in
|
collections = [x.strip() for x in
|
||||||
opts.extra_customization.split(',')]
|
opts.extra_customization.split(',')]
|
||||||
else:
|
else:
|
||||||
collections = []
|
collections = []
|
||||||
@ -179,6 +186,8 @@ class PRS505(USBMS):
|
|||||||
self.plugboard_func = pb_func
|
self.plugboard_func = pb_func
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata, filepath):
|
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]:
|
if metadata.thumbnail and metadata.thumbnail[-1]:
|
||||||
path = path.replace('/', os.sep)
|
path = path.replace('/', os.sep)
|
||||||
is_main = path.startswith(self._main_prefix)
|
is_main = path.startswith(self._main_prefix)
|
||||||
|
@ -410,6 +410,9 @@ class XMLCache(object):
|
|||||||
newmi = book.deepcopy_metadata()
|
newmi = book.deepcopy_metadata()
|
||||||
newmi.template_to_attribute(book, plugboard)
|
newmi.template_to_attribute(book, plugboard)
|
||||||
newmi.set('_new_book', getattr(book, '_new_book', False))
|
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:
|
else:
|
||||||
newmi = book
|
newmi = book
|
||||||
(gtz_count, ltz_count, use_tz_var) = \
|
(gtz_count, ltz_count, use_tz_var) = \
|
||||||
|
@ -72,3 +72,13 @@ class SOVOS(TECLAST_K3):
|
|||||||
VENDOR_NAME = 'RK28XX'
|
VENDOR_NAME = 'RK28XX'
|
||||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC'
|
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'
|
use_renaming_rules = prefs['manage_device_metadata'] == 'on_connect'
|
||||||
|
|
||||||
collections = {}
|
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:
|
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
|
# Make sure we can identify this book via the lpath
|
||||||
lpath = getattr(book, 'lpath', None)
|
lpath = getattr(book, 'lpath', None)
|
||||||
if lpath is None:
|
if lpath is None:
|
||||||
@ -211,22 +226,29 @@ class CollectionsBookList(BookList):
|
|||||||
collections[cat_name] = {}
|
collections[cat_name] = {}
|
||||||
if use_renaming_rules and sort_attr:
|
if use_renaming_rules and sort_attr:
|
||||||
sort_val = book.get(sort_attr, None)
|
sort_val = book.get(sort_attr, None)
|
||||||
collections[cat_name][lpath] = \
|
collections[cat_name][lpath] = (book, sort_val, tsval)
|
||||||
(book, sort_val, book.get('title_sort', 'zzzz'))
|
|
||||||
elif is_series:
|
elif is_series:
|
||||||
if doing_dc:
|
if doing_dc:
|
||||||
collections[cat_name][lpath] = \
|
collections[cat_name][lpath] = \
|
||||||
(book, book.get('series_index', sys.maxint),
|
(book, book.get('series_index', sys.maxint), tsval)
|
||||||
book.get('title_sort', 'zzzz'))
|
|
||||||
else:
|
else:
|
||||||
collections[cat_name][lpath] = \
|
collections[cat_name][lpath] = \
|
||||||
(book, book.get(attr+'_index', sys.maxint),
|
(book, book.get(attr+'_index', sys.maxint), tsval)
|
||||||
book.get('title_sort', 'zzzz'))
|
|
||||||
else:
|
else:
|
||||||
if lpath not in collections[cat_name]:
|
if lpath not in collections[cat_name]:
|
||||||
collections[cat_name][lpath] = \
|
collections[cat_name][lpath] = (book, tsval, tsval)
|
||||||
(book, book.get('title_sort', 'zzzz'),
|
|
||||||
book.get('title_sort', 'zzzz'))
|
# 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
|
# Sort collections
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
|
@ -55,9 +55,11 @@ except:
|
|||||||
|
|
||||||
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
|
_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()
|
title = title.strip()
|
||||||
if tweaks['title_series_sorting'] == 'strictly_alphabetic':
|
if order == 'strictly_alphabetic':
|
||||||
return title
|
return title
|
||||||
if title and title[0] in _ignore_starts:
|
if title and title[0] in _ignore_starts:
|
||||||
title = title[1:]
|
title = title[1:]
|
||||||
|
@ -11,12 +11,11 @@ import os, re, uuid, logging
|
|||||||
from mimetypes import types_map
|
from mimetypes import types_map
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from urlparse import urldefrag, urlparse, urlunparse
|
from urlparse import urldefrag, urlparse, urlunparse, urljoin
|
||||||
from urllib import unquote as urlunquote
|
from urllib import unquote as urlunquote
|
||||||
from urlparse import urljoin
|
|
||||||
|
|
||||||
from lxml import etree, html
|
from lxml import etree, html
|
||||||
from cssutils import CSSParser
|
from cssutils import CSSParser, parseString, parseStyle, replaceUrls
|
||||||
from cssutils.css import CSSRule
|
from cssutils.css import CSSRule
|
||||||
|
|
||||||
import calibre
|
import calibre
|
||||||
@ -88,11 +87,11 @@ def XLINK(name):
|
|||||||
def CALIBRE(name):
|
def CALIBRE(name):
|
||||||
return '{%s}%s' % (CALIBRE_NS, 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 "(.*?)"')
|
_css_import_re = re.compile(r'@import "(.*?)"')
|
||||||
_archive_re = re.compile(r'[^ ]+')
|
_archive_re = re.compile(r'[^ ]+')
|
||||||
|
|
||||||
def iterlinks(root):
|
def iterlinks(root, find_links_in_css=True):
|
||||||
'''
|
'''
|
||||||
Iterate over all links in a OEB Document.
|
Iterate over all links in a OEB Document.
|
||||||
|
|
||||||
@ -134,6 +133,8 @@ def iterlinks(root):
|
|||||||
yield (el, attr, attribs[attr], 0)
|
yield (el, attr, attribs[attr], 0)
|
||||||
|
|
||||||
|
|
||||||
|
if not find_links_in_css:
|
||||||
|
continue
|
||||||
if tag == XHTML('style') and el.text:
|
if tag == XHTML('style') and el.text:
|
||||||
for match in _css_url_re.finditer(el.text):
|
for match in _css_url_re.finditer(el.text):
|
||||||
yield (el, None, match.group(1), match.start(1))
|
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:
|
if resolve_base_href:
|
||||||
resolve_base_href(root)
|
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())
|
new_link = link_repl_func(link.strip())
|
||||||
if new_link == link:
|
if new_link == link:
|
||||||
continue
|
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):]
|
new = cur[:pos] + new_link + cur[pos+len(link):]
|
||||||
el.attrib[attrib] = new
|
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']
|
EPUB_MIME = types_map['.epub']
|
||||||
XHTML_MIME = types_map['.xhtml']
|
XHTML_MIME = types_map['.xhtml']
|
||||||
@ -622,7 +657,10 @@ class Metadata(object):
|
|||||||
attrib[key] = prefixname(value, nsrmap)
|
attrib[key] = prefixname(value, nsrmap)
|
||||||
if namespace(self.term) == DC11_NS:
|
if namespace(self.term) == DC11_NS:
|
||||||
elem = element(parent, self.term, attrib=attrib)
|
elem = element(parent, self.term, attrib=attrib)
|
||||||
elem.text = self.value
|
try:
|
||||||
|
elem.text = self.value
|
||||||
|
except:
|
||||||
|
elem.text = repr(self.value)
|
||||||
else:
|
else:
|
||||||
elem = element(parent, OPF('meta'), attrib=attrib)
|
elem = element(parent, OPF('meta'), attrib=attrib)
|
||||||
elem.attrib['name'] = prefixname(self.term, nsrmap)
|
elem.attrib['name'] = prefixname(self.term, nsrmap)
|
||||||
|
@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import posixpath
|
import posixpath
|
||||||
from urlparse import urldefrag
|
from urlparse import urldefrag, urlparse
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import cssutils
|
import cssutils
|
||||||
@ -67,6 +67,10 @@ class RenameFiles(object): # {{{
|
|||||||
|
|
||||||
def url_replacer(self, orig_url):
|
def url_replacer(self, orig_url):
|
||||||
url = urlnormalize(orig_url)
|
url = urlnormalize(orig_url)
|
||||||
|
parts = urlparse(url)
|
||||||
|
if parts.scheme:
|
||||||
|
# Only rewrite local URLs
|
||||||
|
return orig_url
|
||||||
path, frag = urldefrag(url)
|
path, frag = urldefrag(url)
|
||||||
if self.renamed_items_map:
|
if self.renamed_items_map:
|
||||||
orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item)
|
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 '
|
'You should check them manually. This can '
|
||||||
'happen if you manipulate the files in the '
|
'happen if you manipulate the files in the '
|
||||||
'library folder directly.'), det_msg=det_msg, show=True)
|
'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()
|
self.reset()
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
@ -162,6 +166,7 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
self.choose_menu = QMenu(self.gui)
|
self.choose_menu = QMenu(self.gui)
|
||||||
self.qaction.setMenu(self.choose_menu)
|
self.qaction.setMenu(self.choose_menu)
|
||||||
|
|
||||||
|
|
||||||
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
|
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
|
||||||
self.choose_menu.addAction(self.action_choose)
|
self.choose_menu.addAction(self.action_choose)
|
||||||
|
|
||||||
@ -172,6 +177,11 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
self.delete_menu = QMenu(_('Delete library'))
|
self.delete_menu = QMenu(_('Delete library'))
|
||||||
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
|
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.rename_separator = self.choose_menu.addSeparator()
|
||||||
|
|
||||||
self.switch_actions = []
|
self.switch_actions = []
|
||||||
@ -209,6 +219,12 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
self.maintenance_menu.addAction(ac)
|
self.maintenance_menu.addAction(ac)
|
||||||
self.choose_menu.addMenu(self.maintenance_menu)
|
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):
|
def library_name(self):
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
path = db.library_path
|
path = db.library_path
|
||||||
|
@ -5,18 +5,19 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import re, os
|
||||||
|
|
||||||
from lxml import html
|
from lxml import html
|
||||||
from lxml.html import soupparser
|
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, QWebPage, Qt, QTabWidget, \
|
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
|
||||||
QSyntaxHighlighter, QColor, QChar
|
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
|
||||||
from PyQt4.QtWebKit import QWebView
|
from PyQt4.QtWebKit import QWebView, QWebPage
|
||||||
|
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from calibre import xml_replace_entities
|
from calibre import xml_replace_entities
|
||||||
|
from calibre.gui2 import open_url
|
||||||
|
|
||||||
class PageAction(QAction): # {{{
|
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): # {{{
|
class EditorWidget(QWebView): # {{{
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -90,14 +103,109 @@ class EditorWidget(QWebView): # {{{
|
|||||||
ac = PageAction(wac, icon, text, checkable, self)
|
ac = PageAction(wac, icon, text, checkable, self)
|
||||||
setattr(self, 'action_'+name, ac)
|
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):
|
def sizeHint(self):
|
||||||
return QSize(150, 150)
|
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
|
@dynamic_property
|
||||||
def html(self):
|
def html(self):
|
||||||
|
|
||||||
def fget(self):
|
def fget(self):
|
||||||
ans = u''
|
ans = u''
|
||||||
|
check = unicode(self.page().mainFrame().toPlainText()).strip()
|
||||||
|
if not check:
|
||||||
|
return ans
|
||||||
try:
|
try:
|
||||||
raw = unicode(self.page().mainFrame().toHtml())
|
raw = unicode(self.page().mainFrame().toHtml())
|
||||||
raw = xml_to_unicode(raw, strip_encoding_pats=True,
|
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):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
@ -404,6 +512,15 @@ class Editor(QWidget):
|
|||||||
self.toolbar1.addAction(ac)
|
self.toolbar1.addAction(ac)
|
||||||
self.toolbar1.addSeparator()
|
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.code_edit.textChanged.connect(self.code_dirtied)
|
||||||
self.editor.page().contentsChanged.connect(self.wyswyg_dirtied)
|
self.editor.page().contentsChanged.connect(self.wyswyg_dirtied)
|
||||||
|
|
||||||
@ -411,18 +528,21 @@ class Editor(QWidget):
|
|||||||
def html(self):
|
def html(self):
|
||||||
def fset(self, v):
|
def fset(self, v):
|
||||||
self.editor.html = 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):
|
def change_tab(self, index):
|
||||||
#print 'reloading:', (index and self.wyswyg_dirty) or (not index and
|
#print 'reloading:', (index and self.wyswyg_dirty) or (not index and
|
||||||
# self.source_dirty)
|
# self.source_dirty)
|
||||||
if index == 1: # changing to code view
|
if index == 1: # changing to code view
|
||||||
if self.wyswyg_dirty:
|
if self.wyswyg_dirty:
|
||||||
self.code_edit.setPlainText(self.html)
|
self.code_edit.setPlainText(self.editor.html)
|
||||||
self.wyswyg_dirty = False
|
self.wyswyg_dirty = False
|
||||||
elif index == 0: #changing to wyswyg
|
elif index == 0: #changing to wyswyg
|
||||||
if self.source_dirty:
|
if self.source_dirty:
|
||||||
self.html = unicode(self.code_edit.toPlainText())
|
self.editor.html = unicode(self.code_edit.toPlainText())
|
||||||
self.source_dirty = False
|
self.source_dirty = False
|
||||||
|
|
||||||
def wyswyg_dirtied(self, *args):
|
def wyswyg_dirtied(self, *args):
|
||||||
@ -431,6 +551,8 @@ class Editor(QWidget):
|
|||||||
def code_dirtied(self, *args):
|
def code_dirtied(self, *args):
|
||||||
self.source_dirty = True
|
self.source_dirty = True
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
w = Editor()
|
w = Editor()
|
||||||
|
@ -587,8 +587,6 @@ class BulkSeries(BulkBase):
|
|||||||
else:
|
else:
|
||||||
s_index = self.db.get_custom_extra(book_id, num=self.col_id,
|
s_index = self.db.get_custom_extra(book_id, num=self.col_id,
|
||||||
index_is_id=True)
|
index_is_id=True)
|
||||||
if s_index is None:
|
|
||||||
s_index = 1.0
|
|
||||||
extras.append(s_index)
|
extras.append(s_index)
|
||||||
self.db.set_custom_bulk(book_ids, val, extras=extras,
|
self.db.set_custom_bulk(book_ids, val, extras=extras,
|
||||||
num=self.col_id, notify=notify)
|
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.gui2.widgets import ProgressIndicator
|
||||||
from calibre import strftime, force_unicode
|
from calibre import strftime, force_unicode
|
||||||
from calibre.customize.ui import get_isbndb_key, set_isbndb_key
|
from calibre.customize.ui import get_isbndb_key, set_isbndb_key
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
_hung_fetchers = set([])
|
_hung_fetchers = set([])
|
||||||
|
|
||||||
@ -72,27 +73,33 @@ class Matches(QAbstractTableModel):
|
|||||||
def summary(self, row):
|
def summary(self, row):
|
||||||
return self.matches[row].comments
|
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):
|
def data(self, index, role):
|
||||||
row, col = index.row(), index.column()
|
row, col = index.row(), index.column()
|
||||||
book = self.matches[row]
|
book = self.matches[row]
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
res = None
|
res = self.data_as_text(book, col)
|
||||||
if col == 0:
|
if col <= 5 and res:
|
||||||
res = book.title
|
return QVariant(res)
|
||||||
elif col == 1:
|
return NONE
|
||||||
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)
|
|
||||||
elif role == Qt.DecorationRole:
|
elif role == Qt.DecorationRole:
|
||||||
if col == 6 and book.has_cover:
|
if col == 6 and book.has_cover:
|
||||||
return self.yes_icon
|
return self.yes_icon
|
||||||
@ -100,6 +107,16 @@ class Matches(QAbstractTableModel):
|
|||||||
return self.yes_icon
|
return self.yes_icon
|
||||||
return NONE
|
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):
|
class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||||
|
|
||||||
HANG_TIME = 75 #seconds
|
HANG_TIME = 75 #seconds
|
||||||
@ -136,6 +153,11 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
|||||||
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
||||||
self.show_summary)
|
self.show_summary)
|
||||||
self.matches.setMouseTracking(True)
|
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.fetch_metadata()
|
||||||
self.opt_get_social_metadata.setChecked(config['get_social_metadata'])
|
self.opt_get_social_metadata.setChecked(config['get_social_metadata'])
|
||||||
self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_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):
|
def chosen(self, index):
|
||||||
self.matches.setCurrentIndex(index)
|
self.matches.setCurrentIndex(index)
|
||||||
self.accept()
|
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.metadata_bulk_ui import Ui_MetadataBulkDialog
|
||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
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.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
|
from calibre.gui2 import error_dialog
|
||||||
@ -311,6 +312,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
|||||||
def prepare_search_and_replace(self):
|
def prepare_search_and_replace(self):
|
||||||
self.search_for.initialize('bulk_edit_search_for')
|
self.search_for.initialize('bulk_edit_search_for')
|
||||||
self.replace_with.initialize('bulk_edit_replace_with')
|
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.test_text.initialize('bulk_edit_test_test')
|
||||||
self.all_fields = ['']
|
self.all_fields = ['']
|
||||||
self.writable_fields = ['']
|
self.writable_fields = ['']
|
||||||
@ -325,9 +327,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
|||||||
if f in ['sort'] or fm[f]['datatype'] == 'composite':
|
if f in ['sort'] or fm[f]['datatype'] == 'composite':
|
||||||
self.all_fields.append(f)
|
self.all_fields.append(f)
|
||||||
self.all_fields.sort()
|
self.all_fields.sort()
|
||||||
|
self.all_fields.insert(1, '{template}')
|
||||||
self.writable_fields.sort()
|
self.writable_fields.sort()
|
||||||
self.search_field.setMaxVisibleItems(20)
|
self.search_field.setMaxVisibleItems(25)
|
||||||
self.destination_field.setMaxVisibleItems(20)
|
self.destination_field.setMaxVisibleItems(25)
|
||||||
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):
|
||||||
@ -403,22 +406,28 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
|||||||
self.test_text.editTextChanged[str].connect(self.s_r_paint_results)
|
self.test_text.editTextChanged[str].connect(self.s_r_paint_results)
|
||||||
self.comma_separated.stateChanged.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.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.central_widget.setCurrentIndex(0)
|
||||||
|
|
||||||
self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive)
|
self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive)
|
||||||
self.replace_with.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())
|
self.s_r_search_mode_changed(self.search_mode.currentIndex())
|
||||||
|
|
||||||
def s_r_get_field(self, mi, field):
|
def s_r_get_field(self, mi, field):
|
||||||
if 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)
|
fm = self.db.metadata_for_field(field)
|
||||||
if field == 'sort':
|
if field == 'sort':
|
||||||
val = mi.get('title_sort', None)
|
val = mi.get('title_sort', None)
|
||||||
else:
|
else:
|
||||||
val = mi.get(field, None)
|
val = mi.get(field, None)
|
||||||
if val is None:
|
if val is None:
|
||||||
val = []
|
val = [] if fm['is_multiple'] else ['']
|
||||||
elif not fm['is_multiple']:
|
elif not fm['is_multiple']:
|
||||||
val = [val]
|
val = [val]
|
||||||
elif field == 'authors':
|
elif field == 'authors':
|
||||||
@ -427,7 +436,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
|||||||
val = []
|
val = []
|
||||||
return 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):
|
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):
|
for i in range(0, self.s_r_number_of_books):
|
||||||
w = getattr(self, 'book_%d_text'%(i+1))
|
w = getattr(self, 'book_%d_text'%(i+1))
|
||||||
mi = self.db.get_metadata(self.ids[i], index_is_id=True)
|
mi = self.db.get_metadata(self.ids[i], index_is_id=True)
|
||||||
@ -590,11 +608,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
|||||||
if not dest:
|
if not dest:
|
||||||
dest = source
|
dest = source
|
||||||
dfm = self.db.field_metadata[dest]
|
dfm = self.db.field_metadata[dest]
|
||||||
|
|
||||||
mi = self.db.get_metadata(id, index_is_id=True,)
|
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_regexp(mi)
|
||||||
val = self.s_r_do_destination(mi, val)
|
val = self.s_r_do_destination(mi, val)
|
||||||
if dfm['is_multiple']:
|
if dfm['is_multiple']:
|
||||||
|
@ -508,6 +508,29 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<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">
|
<widget class="QLabel" name="xlabel_2">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Search for:</string>
|
<string>&Search for:</string>
|
||||||
@ -517,7 +540,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="search_for">
|
<widget class="HistoryLineEdit" name="search_for">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
@ -530,7 +553,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="2">
|
<item row="5" 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>
|
||||||
@ -543,7 +566,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_4">
|
<widget class="QLabel" name="xlabel_4">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Replace with:</string>
|
<string>&Replace with:</string>
|
||||||
@ -553,14 +576,14 @@ 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="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="5" column="2">
|
<item row="6" 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">
|
||||||
@ -595,7 +618,7 @@ field is processed. In regular expression mode, only the matched text is process
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" 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>
|
||||||
@ -605,14 +628,15 @@ field is processed. In regular expression mode, only the matched text is process
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="7" 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. 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>
|
</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="replace_mode_label">
|
<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>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="8" column="1">
|
||||||
<widget class="QLabel" name="xlabel_3">
|
<widget class="QLabel" name="xlabel_3">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Test &text</string>
|
<string>Test &text</string>
|
||||||
@ -670,7 +694,7 @@ nothing should be put between the original text and the inserted text</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="2">
|
<item row="8" column="2">
|
||||||
<widget class="QLabel" name="label_51">
|
<widget class="QLabel" name="label_51">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Test re&sult</string>
|
<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>central_widget</tabstop>
|
||||||
<tabstop>search_field</tabstop>
|
<tabstop>search_field</tabstop>
|
||||||
<tabstop>search_mode</tabstop>
|
<tabstop>search_mode</tabstop>
|
||||||
|
<tabstop>s_r_template</tabstop>
|
||||||
<tabstop>search_for</tabstop>
|
<tabstop>search_for</tabstop>
|
||||||
<tabstop>case_sensitive</tabstop>
|
<tabstop>case_sensitive</tabstop>
|
||||||
<tabstop>replace_with</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.preferences.social import SocialMetadata
|
||||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
|
from calibre.library.comments import comments_to_html
|
||||||
|
|
||||||
class CoverFetcher(Thread): # {{{
|
class CoverFetcher(Thread): # {{{
|
||||||
|
|
||||||
@ -195,7 +196,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
_file + _(" is not a valid picture"))
|
_file + _(" is not a valid picture"))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
else:
|
else:
|
||||||
self.cover_path.setText(_file)
|
|
||||||
self.cover.setPixmap(pix)
|
self.cover.setPixmap(pix)
|
||||||
self.update_cover_tooltip()
|
self.update_cover_tooltip()
|
||||||
self.cover_changed = True
|
self.cover_changed = True
|
||||||
@ -409,7 +409,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
if mi.series_index is not None:
|
if mi.series_index is not None:
|
||||||
self.series_index.setValue(float(mi.series_index))
|
self.series_index.setValue(float(mi.series_index))
|
||||||
if mi.comments and mi.comments.strip():
|
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):
|
def sync_formats(self):
|
||||||
@ -556,7 +557,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
if rating > 0:
|
if rating > 0:
|
||||||
self.rating.setValue(int(rating/2.))
|
self.rating.setValue(int(rating/2.))
|
||||||
comments = self.db.comments(row)
|
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)
|
cover = self.db.cover(row)
|
||||||
pubdate = db.pubdate(self.id, index_is_id=True)
|
pubdate = db.pubdate(self.id, index_is_id=True)
|
||||||
self.pubdate.setDate(QDate(pubdate.year, pubdate.month,
|
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))
|
self.pubdate.setDate(QDate(dt.year, dt.month, dt.day))
|
||||||
summ = book.comments
|
summ = book.comments
|
||||||
if summ:
|
if summ:
|
||||||
prefix = unicode(self.comments.toPlainText())
|
prefix = self.comments.html
|
||||||
if prefix:
|
if prefix:
|
||||||
prefix += '\n'
|
prefix += '\n'
|
||||||
self.comments.setPlainText(prefix + summ)
|
self.comments.html = prefix + comments_to_html(summ)
|
||||||
if book.rating is not None:
|
if book.rating is not None:
|
||||||
self.rating.setValue(int(book.rating))
|
self.rating.setValue(int(book.rating))
|
||||||
if book.tags:
|
if book.tags:
|
||||||
@ -899,7 +902,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.db.set_series_index(self.id, self.series_index.value(),
|
self.db.set_series_index(self.id, self.series_index.value(),
|
||||||
notify=False, commit=False)
|
notify=False, commit=False)
|
||||||
self.db.set_comment(self.id,
|
self.db.set_comment(self.id,
|
||||||
unicode(self.comments.toPlainText()).strip(),
|
self.comments.html,
|
||||||
notify=False, commit=False)
|
notify=False, commit=False)
|
||||||
d = self.pubdate.date()
|
d = self.pubdate.date()
|
||||||
d = qt_to_dt(d)
|
d = qt_to_dt(d)
|
||||||
@ -936,16 +939,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
QDialog.reject(self, *args)
|
QDialog.reject(self, *args)
|
||||||
|
|
||||||
def read_state(self):
|
def read_state(self):
|
||||||
wg = dynamic.get('metasingle_window_geometry', None)
|
wg = dynamic.get('metasingle_window_geometry2', None)
|
||||||
ss = dynamic.get('metasingle_splitter_state', None)
|
ss = dynamic.get('metasingle_splitter_state2', None)
|
||||||
if wg is not None:
|
if wg is not None:
|
||||||
self.restoreGeometry(wg)
|
self.restoreGeometry(wg)
|
||||||
if ss is not None:
|
if ss is not None:
|
||||||
self.splitter.restoreState(ss)
|
self.splitter.restoreState(ss)
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry()))
|
dynamic.set('metasingle_window_geometry2', bytes(self.saveGeometry()))
|
||||||
dynamic.set('metasingle_splitter_state',
|
dynamic.set('metasingle_splitter_state2',
|
||||||
bytes(self.splitter.saveState()))
|
bytes(self.splitter.saveState()))
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>887</width>
|
<width>994</width>
|
||||||
<height>750</height>
|
<height>716</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -43,8 +43,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>879</width>
|
<width>986</width>
|
||||||
<height>711</height>
|
<height>677</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
@ -66,8 +66,8 @@
|
|||||||
<attribute name="title">
|
<attribute name="title">
|
||||||
<string>&Basic metadata</string>
|
<string>&Basic metadata</string>
|
||||||
</attribute>
|
</attribute>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
<layout class="QGridLayout" name="gridLayout_5">
|
||||||
<item>
|
<item row="0" column="0">
|
||||||
<widget class="QSplitter" name="splitter">
|
<widget class="QSplitter" name="splitter">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
@ -496,28 +496,147 @@ Using this button to create author sort will change author sort from red to gree
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox_2">
|
<spacer name="verticalSpacer_5">
|
||||||
<property name="title">
|
<property name="orientation">
|
||||||
<string>&Comments</string>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_4">
|
<property name="sizeType">
|
||||||
<item row="0" column="0">
|
<enum>QSizePolicy::Fixed</enum>
|
||||||
<widget class="QTextEdit" name="comments">
|
</property>
|
||||||
<property name="tabChangesFocus">
|
<property name="sizeHint" stdset="0">
|
||||||
<bool>true</bool>
|
<size>
|
||||||
</property>
|
<width>20</width>
|
||||||
<property name="acceptRichText">
|
<height>40</height>
|
||||||
<bool>false</bool>
|
</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>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="layoutWidget_2">
|
<widget class="QWidget" name="layoutWidget">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="af_group_box">
|
<widget class="QGroupBox" name="af_group_box">
|
||||||
<property name="sizePolicy">
|
<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>
|
<height>140</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="baseSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
<property name="dragDropMode">
|
<property name="dragDropMode">
|
||||||
<enum>QAbstractItemView::DropOnly</enum>
|
<enum>QAbstractItemView::DropOnly</enum>
|
||||||
</property>
|
</property>
|
||||||
@ -644,129 +769,22 @@ Using this button to create author sort will change author sort from red to gree
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="bc_box">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>10</verstretch>
|
<verstretch>10</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Book Cover</string>
|
<string>&Comments</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||||
|
<property name="margin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="ImageView" name="cover" native="true">
|
<widget class="Editor" name="comments" 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>
|
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</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>
|
<header>calibre/gui2/widgets.h</header>
|
||||||
<container>1</container>
|
<container>1</container>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>Editor</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header location="global">calibre/gui2/comments_editor.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>title</tabstop>
|
<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>date</tabstop>
|
||||||
<tabstop>pubdate</tabstop>
|
<tabstop>pubdate</tabstop>
|
||||||
<tabstop>fetch_metadata_button</tabstop>
|
<tabstop>fetch_metadata_button</tabstop>
|
||||||
<tabstop>comments</tabstop>
|
|
||||||
<tabstop>button_set_cover</tabstop>
|
<tabstop>button_set_cover</tabstop>
|
||||||
<tabstop>button_set_metadata</tabstop>
|
<tabstop>button_set_metadata</tabstop>
|
||||||
<tabstop>formats</tabstop>
|
<tabstop>formats</tabstop>
|
||||||
<tabstop>add_format_button</tabstop>
|
<tabstop>add_format_button</tabstop>
|
||||||
<tabstop>remove_format_button</tabstop>
|
<tabstop>remove_format_button</tabstop>
|
||||||
<tabstop>cover_path</tabstop>
|
|
||||||
<tabstop>cover_button</tabstop>
|
<tabstop>cover_button</tabstop>
|
||||||
<tabstop>trim_cover_button</tabstop>
|
<tabstop>trim_cover_button</tabstop>
|
||||||
<tabstop>reset_cover</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.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
||||||
REGEXP_MATCH, MetadataBackup
|
REGEXP_MATCH, MetadataBackup
|
||||||
from calibre.library.cli import parse_series_string
|
|
||||||
from calibre import strftime, isbytestring, prepare_string_for_xml
|
from calibre import strftime, isbytestring, prepare_string_for_xml
|
||||||
from calibre.constants import filesystem_encoding, DEBUG
|
from calibre.constants import filesystem_encoding, DEBUG
|
||||||
from calibre.gui2.library import DEFAULT_SORT
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
@ -725,9 +724,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
return False
|
return False
|
||||||
val = qt_to_dt(val, as_utc=False)
|
val = qt_to_dt(val, as_utc=False)
|
||||||
elif typ == 'series':
|
elif typ == 'series':
|
||||||
val, s_index = parse_series_string(self.db, label, value.toString())
|
val = unicode(value.toString()).strip()
|
||||||
if not val:
|
|
||||||
val = s_index = None
|
|
||||||
elif typ == 'composite':
|
elif typ == 'composite':
|
||||||
tmpl = unicode(value.toString()).strip()
|
tmpl = unicode(value.toString()).strip()
|
||||||
disp = cc['display']
|
disp = cc['display']
|
||||||
|
@ -695,8 +695,10 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
def setData(self, index, value, role=Qt.EditRole):
|
def setData(self, index, value, role=Qt.EditRole):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return NONE
|
return NONE
|
||||||
# set up to position at the category label
|
# set up to reposition at the same item. We can do this except if
|
||||||
path = self.path_for_index(self.parent(index))
|
# 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())
|
val = unicode(value.toString())
|
||||||
if not val:
|
if not val:
|
||||||
error_dialog(self.tags_view, _('Item is blank'),
|
error_dialog(self.tags_view, _('Item is blank'),
|
||||||
@ -947,18 +949,22 @@ class TagBrowserMixin(object): # {{{
|
|||||||
for old_id in to_rename[text]:
|
for old_id in to_rename[text]:
|
||||||
rename_func(old_id, new_name=unicode(text))
|
rename_func(old_id, new_name=unicode(text))
|
||||||
|
|
||||||
# Clean up everything, as information could have changed for many books.
|
# Clean up the library view
|
||||||
self.library_view.model().refresh()
|
self.do_tag_item_renamed()
|
||||||
self.tags_view.set_new_model()
|
self.tags_view.set_new_model() # does a refresh for free
|
||||||
self.tags_view.recount()
|
|
||||||
self.saved_search.clear()
|
|
||||||
self.search.clear()
|
|
||||||
|
|
||||||
def do_tag_item_renamed(self):
|
def do_tag_item_renamed(self):
|
||||||
# Clean up library view and search
|
# Clean up library view and search
|
||||||
self.library_view.model().refresh()
|
# get information to redo the selection
|
||||||
self.saved_search.clear()
|
rows = [r.row() for r in \
|
||||||
self.search.clear()
|
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):
|
def do_author_sort_edit(self, parent, id):
|
||||||
db = self.library_view.model().db
|
db = self.library_view.model().db
|
||||||
|
@ -3,8 +3,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
'''
|
# Imports {{{
|
||||||
'''
|
|
||||||
import os, math, re, glob, sys
|
import os, math, re, glob, sys
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@ -19,11 +18,14 @@ from calibre.utils.config import Config, StringConfig
|
|||||||
from calibre.utils.localization import get_language
|
from calibre.utils.localization import get_language
|
||||||
from calibre.gui2.viewer.config_ui import Ui_Dialog
|
from calibre.gui2.viewer.config_ui import Ui_Dialog
|
||||||
from calibre.gui2.viewer.flip import SlideFlip
|
from calibre.gui2.viewer.flip import SlideFlip
|
||||||
|
from calibre.gui2.viewer.gestures import Gestures
|
||||||
from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig
|
from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig
|
||||||
from calibre.constants import iswindows
|
from calibre.constants import iswindows
|
||||||
from calibre import prints, guess_type
|
from calibre import prints, guess_type
|
||||||
from calibre.gui2.viewer.keys import SHORTCUTS
|
from calibre.gui2.viewer.keys import SHORTCUTS
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = \
|
bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = \
|
||||||
hyphenator = images = hyphen_pats = None
|
hyphenator = images = hyphen_pats = None
|
||||||
|
|
||||||
@ -33,6 +35,7 @@ def load_builtin_fonts():
|
|||||||
QFontDatabase.addApplicationFont(f)
|
QFontDatabase.addApplicationFont(f)
|
||||||
return 'Liberation Serif', 'Liberation Sans', 'Liberation Mono'
|
return 'Liberation Serif', 'Liberation Sans', 'Liberation Mono'
|
||||||
|
|
||||||
|
# Config {{{
|
||||||
def config(defaults=None):
|
def config(defaults=None):
|
||||||
desc = _('Options to customize the ebook viewer')
|
desc = _('Options to customize the ebook viewer')
|
||||||
if defaults is None:
|
if defaults is None:
|
||||||
@ -137,8 +140,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
str(self.hyphenate_default_lang.itemData(idx).toString()))
|
str(self.hyphenate_default_lang.itemData(idx).toString()))
|
||||||
return QDialog.accept(self, *args)
|
return QDialog.accept(self, *args)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class Document(QWebPage):
|
class Document(QWebPage): # {{{
|
||||||
|
|
||||||
def set_font_settings(self):
|
def set_font_settings(self):
|
||||||
opts = config().parse()
|
opts = config().parse()
|
||||||
@ -449,7 +453,9 @@ class Document(QWebPage):
|
|||||||
self.height+amount)
|
self.height+amount)
|
||||||
self.setPreferredContentsSize(s)
|
self.setPreferredContentsSize(s)
|
||||||
|
|
||||||
class EntityDeclarationProcessor(object):
|
# }}}
|
||||||
|
|
||||||
|
class EntityDeclarationProcessor(object): # {{{
|
||||||
|
|
||||||
def __init__(self, html):
|
def __init__(self, html):
|
||||||
self.declared_entities = {}
|
self.declared_entities = {}
|
||||||
@ -460,14 +466,16 @@ class EntityDeclarationProcessor(object):
|
|||||||
self.processed_html = html
|
self.processed_html = html
|
||||||
for key, val in self.declared_entities.iteritems():
|
for key, val in self.declared_entities.iteritems():
|
||||||
self.processed_html = self.processed_html.replace('&%s;'%key, val)
|
self.processed_html = self.processed_html.replace('&%s;'%key, val)
|
||||||
|
# }}}
|
||||||
|
|
||||||
class DocumentView(QWebView):
|
class DocumentView(QWebView): # {{{
|
||||||
|
|
||||||
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QWebView.__init__(self, *args)
|
QWebView.__init__(self, *args)
|
||||||
self.flipper = SlideFlip(self)
|
self.flipper = SlideFlip(self)
|
||||||
|
self.gestures = Gestures()
|
||||||
self.is_auto_repeat_event = False
|
self.is_auto_repeat_event = False
|
||||||
self.debug_javascript = False
|
self.debug_javascript = False
|
||||||
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
|
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
|
||||||
@ -953,6 +961,29 @@ class DocumentView(QWebView):
|
|||||||
self.manager.viewport_resized(self.scroll_fraction)
|
self.manager.viewport_resized(self.scroll_fraction)
|
||||||
return ret
|
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):
|
def mouseReleaseEvent(self, ev):
|
||||||
opos = self.document.ypos
|
opos = self.document.ypos
|
||||||
ret = QWebView.mouseReleaseEvent(self, ev)
|
ret = QWebView.mouseReleaseEvent(self, ev)
|
||||||
@ -961,4 +992,5 @@ class DocumentView(QWebView):
|
|||||||
self.manager.scrolled(self.scroll_fraction)
|
self.manager.scrolled(self.scroll_fraction)
|
||||||
return ret
|
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):
|
class HistoryLineEdit(QComboBox):
|
||||||
|
|
||||||
|
lost_focus = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QComboBox.__init__(self, *args)
|
QComboBox.__init__(self, *args)
|
||||||
self.setEditable(True)
|
self.setEditable(True)
|
||||||
@ -559,6 +561,10 @@ class HistoryLineEdit(QComboBox):
|
|||||||
def text(self):
|
def text(self):
|
||||||
return self.currentText()
|
return self.currentText()
|
||||||
|
|
||||||
|
def focusOutEvent(self, e):
|
||||||
|
QComboBox.focusOutEvent(self, e)
|
||||||
|
self.lost_focus.emit()
|
||||||
|
|
||||||
class ComboBoxWithHelp(QComboBox):
|
class ComboBoxWithHelp(QComboBox):
|
||||||
'''
|
'''
|
||||||
A combobox where item 0 is help text. CurrentText will return '' for item 0.
|
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)
|
index_is_id=True)
|
||||||
val = self.custom_data_adapters[data['datatype']](val, data)
|
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['normalized']:
|
||||||
if data['datatype'] == 'enumeration' and (
|
if data['datatype'] == 'enumeration' and (
|
||||||
val and val not in data['display']['enum_values']):
|
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.execute('DELETE FROM tags WHERE id=?', (id,))
|
||||||
self.conn.commit()
|
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):
|
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 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 series:
|
||||||
if not isinstance(series, unicode):
|
if not isinstance(series, unicode):
|
||||||
series = series.decode(preferred_encoding, 'replace')
|
series = series.decode(preferred_encoding, 'replace')
|
||||||
@ -2147,6 +2165,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
else:
|
else:
|
||||||
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
|
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
|
||||||
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
|
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)
|
self.dirtied([id], commit=False)
|
||||||
if commit:
|
if commit:
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
@ -7,6 +7,7 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, traceback, cStringIO, re, shutil
|
import os, traceback, cStringIO, re, shutil
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from calibre.constants import DEBUG
|
from calibre.constants import DEBUG
|
||||||
from calibre.utils.config import Config, StringConfig, tweaks
|
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,
|
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||||
sanitize_func=ascii_filename, replace_whitespace=False,
|
sanitize_func=ascii_filename, replace_whitespace=False,
|
||||||
to_lowercase=False):
|
to_lowercase=False):
|
||||||
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
|
tsfmt = partial(title_sort, order=tweaks['save_template_title_series_sorting'])
|
||||||
tsfmt = title_sort if library_order else lambda x: x
|
|
||||||
format_args = FORMAT_ARGS.copy()
|
format_args = FORMAT_ARGS.copy()
|
||||||
format_args.update(mi.all_non_none_fields())
|
format_args.update(mi.all_non_none_fields())
|
||||||
if mi.title:
|
if mi.title:
|
||||||
|
@ -16,7 +16,6 @@ from datetime import datetime
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from calibre.ebooks.metadata import title_sort, author_to_author_sort
|
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.utils.date import parse_date, isoformat
|
||||||
from calibre import isbytestring, force_unicode
|
from calibre import isbytestring, force_unicode
|
||||||
from calibre.constants import iswindows, DEBUG
|
from calibre.constants import iswindows, DEBUG
|
||||||
|
@ -181,16 +181,17 @@ How do I use |app| with my iPad/iPhone/iTouch?
|
|||||||
Over the air
|
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.
|
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|
|
||||||
|
|
||||||
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 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`
|
* 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.
|
* 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.
|
* 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::
|
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.
|
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
|
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):
|
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
|
* ``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``.
|
* ``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``.
|
* ``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.
|
* ``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``.
|
* ``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'``.
|
* ``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
|
Special notes for save/send templates
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
@ -57,6 +57,11 @@ class _Parser(object):
|
|||||||
y = float(y if y else 0)
|
y = float(y if y else 0)
|
||||||
return ops[op](x, y)
|
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 = {
|
local_functions = {
|
||||||
'add' : (2, partial(_math, op='+')),
|
'add' : (2, partial(_math, op='+')),
|
||||||
'assign' : (2, _assign),
|
'assign' : (2, _assign),
|
||||||
@ -68,6 +73,7 @@ class _Parser(object):
|
|||||||
'strcmp' : (5, _strcmp),
|
'strcmp' : (5, _strcmp),
|
||||||
'substr' : (3, lambda s, x, y, z: x[int(y): len(x) if int(z) == 0 else int(z)]),
|
'substr' : (3, lambda s, x, y, z: x[int(y): len(x) if int(z) == 0 else int(z)]),
|
||||||
'subtract' : (2, partial(_math, op='-')),
|
'subtract' : (2, partial(_math, op='-')),
|
||||||
|
'template' : (1, _template)
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, val, prog, parent):
|
def __init__(self, val, prog, parent):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user