Merge from trunk
@ -4,6 +4,64 @@
|
||||
# for important features/bug fixes.
|
||||
# Also, each release can have new and improved recipes.
|
||||
|
||||
- version: 0.7.10
|
||||
date: 2010-07-23
|
||||
|
||||
new features:
|
||||
- title: "Allow user customization of static resources such as icons and templates"
|
||||
type: major
|
||||
description: >
|
||||
"You can now change the icons used in the User Interface and other static resources. Details on how to
|
||||
do this are at: http://calibre-ebook.com/user_manual/customize.html#overriding-icons-templates-etcetera"
|
||||
|
||||
- title: "Split the 'Send to device' button into two buttons, 'Connect/share' and 'Send to device'. The new 'Send to device' button will now only be available when a device is connected."
|
||||
|
||||
- title: "Store column layout, saved searches and user categories seprately per calibre library. This makes it possible to easily switch between libraries with different custom column setups"
|
||||
|
||||
- title: "See the last modofied date for each format in the edit metadata dialog via a tooltip"
|
||||
tickets: [6252]
|
||||
|
||||
- title: "PD Novel driver: Add support for uploading cover thumbnails to device"
|
||||
|
||||
- title: "More sophisticated metadata extraction from HTML files"
|
||||
tickets: [6223]
|
||||
|
||||
bug fixes:
|
||||
|
||||
- title: "Fix problems with a few windows installs caused by the upgrade to Qt 4.6.3 in the previous release. These would manifest as a not working Add Books button, or deletes not actually deleting files, etc."
|
||||
|
||||
- title: "Restore configurability of toolbar, which was temporarily removed in 0.7.9. You can once again set icon size via Preferences->Interface"
|
||||
|
||||
- title: "Fix regression in iTunes driver in 0.7.9 when sending series info"
|
||||
|
||||
- title: "Search: Fix parsing of search terms that contain a word that starts with 'and' or 'or' and is not the first word"
|
||||
|
||||
- title: "When merging records also merge metadata in custom columns"
|
||||
tickets: [6120]
|
||||
|
||||
- title: "When scrolling to show a particular row, handle the case when the first column is a custom column"
|
||||
tickets: [6176]
|
||||
|
||||
- title: "Fix SD card detection for The Augen Book"
|
||||
tickets: [6224]
|
||||
|
||||
- title: "CHM Input: Fix a couple of bugs that could cause crashes"
|
||||
tickets: [6240]
|
||||
|
||||
- title: "Conversion pipeline: Handle zero width elements with non zero indents gracefully"
|
||||
tickets: [6230]
|
||||
|
||||
new recipes:
|
||||
- title: "daum.net"
|
||||
author: trustin
|
||||
|
||||
- title: "MIT Technology Review, Alternet, Waco Tribune Herald and Orlando Sentinel"
|
||||
author: rty
|
||||
|
||||
improved recipes:
|
||||
- The BBC
|
||||
- heise
|
||||
|
||||
- version: 0.7.9
|
||||
date: 2010-07-17
|
||||
|
||||
|
5123
resources/images/connect_share.svg
Normal file
After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 5.3 KiB |
1009
resources/images/dictionary.svg
Normal file
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 21 KiB |
@ -3,14 +3,13 @@ __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
news.bbc.co.uk
|
||||
'''
|
||||
|
||||
import re
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class BBC(BasicNewsRecipe):
|
||||
title = 'The BBC'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Global news and current affairs from the British Broadcasting Corporation'
|
||||
title = 'BBC News'
|
||||
__author__ = 'Darko Miletic, Starson17'
|
||||
description = 'News from UK. '
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
@ -23,7 +22,6 @@ class BBC(BasicNewsRecipe):
|
||||
publication_type = 'newsportal'
|
||||
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
|
||||
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
|
||||
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
@ -33,14 +31,15 @@ class BBC(BasicNewsRecipe):
|
||||
}
|
||||
|
||||
keep_only_tags = [
|
||||
dict(attrs={'id' :['meta-information','story-body']})
|
||||
,dict(attrs={'class':['mxb' ,'storybody' ]})
|
||||
dict(name='div', attrs={'class':['layout-block-a layout-block']})
|
||||
,dict(attrs={'class':['story-body','storybody']})
|
||||
]
|
||||
remove_tags = [
|
||||
dict(name=['object','link','table'])
|
||||
,dict(attrs={'class':['caption','caption full-width','story-actions','hidden','sharesb','audioInStoryC']})
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
|
||||
'story-feature wide ', 'story-feature narrow']})
|
||||
]
|
||||
remove_tags_after = dict(attrs={'class':'sharesb'})
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
|
||||
feeds = [
|
||||
|
@ -8,7 +8,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class BBC(BasicNewsRecipe):
|
||||
title = 'BBC News (fast)'
|
||||
__author__ = 'Darko Miletic'
|
||||
__author__ = 'Darko Miletic, Starson17'
|
||||
description = 'News from UK. A much faster version that does not download pictures'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 100
|
||||
@ -31,14 +31,16 @@ class BBC(BasicNewsRecipe):
|
||||
}
|
||||
|
||||
keep_only_tags = [
|
||||
dict(attrs={'id' :['meta-information','story-body']})
|
||||
,dict(attrs={'class':['mxb' ,'storybody' ]})
|
||||
dict(name='div', attrs={'class':['layout-block-a layout-block']})
|
||||
,dict(attrs={'class':['story-body','storybody']})
|
||||
]
|
||||
remove_tags = [
|
||||
dict(name=['object','link','table','img'])
|
||||
,dict(attrs={'class':['caption','caption full-width','story-actions','hidden','sharesb','audioInStoryC']})
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
|
||||
'story-feature wide ', 'story-feature narrow']})
|
||||
, dict(name=['img'])
|
||||
]
|
||||
remove_tags_after = dict(attrs={'class':'sharesb'})
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
|
||||
feeds = [
|
||||
|
112
resources/recipes/daum_net.recipe
Normal file
@ -0,0 +1,112 @@
|
||||
import re
|
||||
from datetime import date, timedelta
|
||||
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class MediaDaumRecipe(BasicNewsRecipe):
|
||||
title = u'\uBBF8\uB514\uC5B4 \uB2E4\uC74C \uC624\uB298\uC758 \uC8FC\uC694 \uB274\uC2A4'
|
||||
description = 'Articles from media.daum.net'
|
||||
__author__ = 'trustin'
|
||||
language = 'ko'
|
||||
max_articles = 100
|
||||
|
||||
timefmt = ''
|
||||
masthead_url = 'http://img-media.daum-img.net/2010ci/service_news.gif'
|
||||
cover_margins = (18,18,'grey99')
|
||||
no_stylesheets = True
|
||||
remove_tags_before = dict(id='GS_con')
|
||||
remove_tags_after = dict(id='GS_con')
|
||||
remove_tags = [dict(attrs={'class':[
|
||||
'bline',
|
||||
'GS_vod',
|
||||
]}),
|
||||
dict(id=[
|
||||
'GS_swf_poll',
|
||||
'ad250',
|
||||
]),
|
||||
dict(name=['script', 'noscript', 'style', 'object'])]
|
||||
preprocess_regexps = [
|
||||
(re.compile(r'<\s+', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '< '),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*){3,}', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: ''),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</div>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</div>'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</p>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</p>'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</td>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</td>'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</strong>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</strong>'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</b>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</b>'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</em>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</em>'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</i>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</i>'),
|
||||
(re.compile(u'\(\uB05D\)[ \t\r\n]*<br[^>]*>.*</div>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</div>'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<div', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '<div'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<p', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '<p'),
|
||||
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<table', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '<table'),
|
||||
(re.compile(r'<strong>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '<strong>'),
|
||||
(re.compile(r'<b>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '<b>'),
|
||||
(re.compile(r'<em>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '<em>'),
|
||||
(re.compile(r'<i>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '<i>'),
|
||||
(re.compile(u'(<br[^>]*>[ \t\r\n]*)*(\u25B6|\u25CF|\u261E|\u24D2|\(c\))*\[[^\]]*(\u24D2|\(c\)|\uAE30\uC0AC|\uC778\uAE30[^\]]*\uB274\uC2A4)[^\]]*\].*</div>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: '</div>'),
|
||||
]
|
||||
|
||||
def parse_index(self):
|
||||
today = date.today();
|
||||
articles = []
|
||||
articles = self.parse_list_page(articles, today)
|
||||
articles = self.parse_list_page(articles, today - timedelta(1))
|
||||
return [('\uBBF8\uB514\uC5B4 \uB2E4\uC74C \uC624\uB298\uC758 \uC8FC\uC694 \uB274\uC2A4', articles)]
|
||||
|
||||
|
||||
def parse_list_page(self, articles, date):
|
||||
if len(articles) >= self.max_articles:
|
||||
return articles
|
||||
|
||||
for page in range(1, 10):
|
||||
soup = self.index_to_soup('http://media.daum.net/primary/total/list.html?cateid=100044&date=%(date)s&page=%(page)d' % {'date': date.strftime('%Y%m%d'), 'page': page})
|
||||
done = True
|
||||
for item in soup.findAll('dl'):
|
||||
dt = item.find('dt', { 'class': 'tit' })
|
||||
dd = item.find('dd', { 'class': 'txt' })
|
||||
if dt is None:
|
||||
break
|
||||
a = dt.find('a', href=True)
|
||||
url = 'http://media.daum.net/primary/total/' + a['href']
|
||||
title = self.tag_to_string(dt)
|
||||
if dd is None:
|
||||
description = ''
|
||||
else:
|
||||
description = self.tag_to_string(dd)
|
||||
articles.append(dict(title=title, description=description, url=url, content=''))
|
||||
done = len(articles) >= self.max_articles
|
||||
if done:
|
||||
break
|
||||
if done:
|
||||
break
|
||||
return articles
|
||||
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.strip_anchors(soup)
|
||||
|
||||
def strip_anchors(self, soup):
|
||||
for para in soup.findAll(True):
|
||||
aTags = para.findAll('a')
|
||||
for a in aTags:
|
||||
if a.img is None:
|
||||
a.replaceWith(a.renderContents().decode('utf-8','replace'))
|
||||
return soup
|
@ -19,6 +19,32 @@ class heiseDe(BasicNewsRecipe):
|
||||
max_articles_per_feed = 40
|
||||
no_stylesheets = True
|
||||
|
||||
extra_css = '''
|
||||
.bild_links, .bild_bu_links {
|
||||
float:left;
|
||||
line-height:105%;
|
||||
margin:12px 1.4em 12px 0;
|
||||
}
|
||||
|
||||
.bild_rechts, .bild_bu {
|
||||
float:right;
|
||||
line-height:105%;
|
||||
margin:12px 0 12px 1em;
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
.bild_zentriert {
|
||||
clear:both;
|
||||
line-height:105%;
|
||||
margin:.2em auto;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
span.bild_links, span.bild_rechts, span.bild_zentriert {
|
||||
display:block;
|
||||
}
|
||||
'''
|
||||
|
||||
remove_tags = [dict(id='navi_top'),
|
||||
dict(id='navi_bottom'),
|
||||
dict(id='logo'),
|
||||
|
@ -6,6 +6,7 @@ class AdvancedUserRecipe1257302745(BasicNewsRecipe):
|
||||
language = 'en'
|
||||
__author__ = 'onyxrev'
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
|
||||
remove_tags_before = {'class':'storytitle'}
|
||||
remove_tags_after = dict(name='div', attrs={'id':'storytext' })
|
||||
|
@ -2,6 +2,7 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys, os, re, logging, time, mimetypes, \
|
||||
__builtin__, warnings, multiprocessing
|
||||
from urllib import getproxies
|
||||
@ -13,12 +14,13 @@ from functools import partial
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
|
||||
|
||||
from calibre.startup import plugins, winutil, winutilerror
|
||||
from calibre.constants import iswindows, isosx, islinux, isfreebsd, isfrozen, \
|
||||
terminal_controller, preferred_encoding, \
|
||||
__appname__, __version__, __author__, \
|
||||
win32event, win32api, winerror, fcntl, \
|
||||
filesystem_encoding
|
||||
filesystem_encoding, plugins, config_dir
|
||||
from calibre.startup import winutil, winutilerror
|
||||
|
||||
import mechanize
|
||||
|
||||
if False:
|
||||
@ -486,7 +488,6 @@ def ipython(user_ns=None):
|
||||
sys.argv = ['ipython']
|
||||
if user_ns is None:
|
||||
user_ns = locals()
|
||||
from calibre.utils.config import config_dir
|
||||
ipydir = os.path.join(config_dir, ('_' if iswindows else '.')+'ipython')
|
||||
os.environ['IPYTHONDIR'] = ipydir
|
||||
if not os.path.exists(ipydir):
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.9'
|
||||
__version__ = '0.7.10'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
@ -14,7 +14,7 @@ numeric_version = tuple(_ver)
|
||||
Various run time constants.
|
||||
'''
|
||||
|
||||
import sys, locale, codecs
|
||||
import sys, locale, codecs, os
|
||||
from calibre.utils.terminfo import TerminalController
|
||||
|
||||
terminal_controller = TerminalController(sys.stdout)
|
||||
@ -47,7 +47,7 @@ def debug():
|
||||
global DEBUG
|
||||
DEBUG = True
|
||||
|
||||
################################################################################
|
||||
# plugins {{{
|
||||
plugins = None
|
||||
if plugins is None:
|
||||
# Load plugins
|
||||
@ -80,3 +80,22 @@ if plugins is None:
|
||||
return plugins
|
||||
|
||||
plugins = load_plugins()
|
||||
# }}}
|
||||
|
||||
# config_dir {{{
|
||||
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
|
||||
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
|
||||
elif iswindows:
|
||||
if plugins['winutil'][0] is None:
|
||||
raise Exception(plugins['winutil'][1])
|
||||
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
|
||||
if not os.access(config_dir, os.W_OK|os.X_OK):
|
||||
config_dir = os.path.expanduser('~')
|
||||
config_dir = os.path.join(config_dir, 'calibre')
|
||||
elif isosx:
|
||||
config_dir = os.path.expanduser('~/Library/Preferences/calibre')
|
||||
else:
|
||||
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
|
||||
config_dir = os.path.join(bdir, 'calibre')
|
||||
# }}}
|
||||
|
||||
|
@ -2668,7 +2668,7 @@ class ITUNES(DriverBase):
|
||||
index = metadata.series_index
|
||||
integer = int(index)
|
||||
fraction = index-integer
|
||||
series_index = '%04d%%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
|
||||
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
|
||||
if lb_added:
|
||||
lb_added.SortName = "%s %s" % (metadata.series, series_index)
|
||||
lb_added.Genre = metadata.series
|
||||
|
@ -43,6 +43,7 @@ class THEBOOK(N516):
|
||||
BCD = [0x399]
|
||||
MAIN_MEMORY_VOLUME_LABEL = 'The Book Main Memory'
|
||||
EBOOK_DIR_MAIN = 'My books'
|
||||
WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE'
|
||||
|
||||
class ALEX(N516):
|
||||
|
||||
|
@ -6,6 +6,8 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
|
||||
from calibre.devices.usbms.driver import USBMS
|
||||
|
||||
class PALMPRE(USBMS):
|
||||
@ -83,7 +85,14 @@ class PDNOVEL(USBMS):
|
||||
|
||||
VENDOR_NAME = 'ANDROID'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
|
||||
THUMBNAIL_HEIGHT = 144
|
||||
|
||||
EBOOK_DIR_MAIN = 'eBooks'
|
||||
SUPPORTS_SUB_DIRS = False
|
||||
|
||||
def upload_cover(self, path, filename, metadata):
|
||||
coverdata = getattr(metadata, 'thumbnail', None)
|
||||
if coverdata and coverdata[2]:
|
||||
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
||||
coverfile.write(coverdata[2])
|
||||
|
||||
|
@ -177,6 +177,7 @@ class CHMInput(InputFormatPlugin):
|
||||
|
||||
chapter_path = None
|
||||
if match_string(node.tag, 'object') and match_string(node.attrib['type'], 'text/sitemap'):
|
||||
chapter_title = None
|
||||
for child in node:
|
||||
if match_string(child.tag,'param') and match_string(child.attrib['name'], 'name'):
|
||||
chapter_title = child.attrib['value']
|
||||
|
@ -295,11 +295,10 @@ class MetaInformation(object):
|
||||
if val is not None:
|
||||
setattr(self, attr, val)
|
||||
|
||||
if mi.tags:
|
||||
if replace_metadata:
|
||||
self.tags = mi.tags
|
||||
else:
|
||||
self.tags += mi.tags
|
||||
if replace_metadata:
|
||||
self.tags = mi.tags
|
||||
elif mi.tags:
|
||||
self.tags += mi.tags
|
||||
self.tags = list(set(self.tags))
|
||||
|
||||
if mi.author_sort_map:
|
||||
@ -313,14 +312,17 @@ class MetaInformation(object):
|
||||
if len(other_cover) > len(self_cover):
|
||||
self.cover_data = mi.cover_data
|
||||
|
||||
my_comments = getattr(self, 'comments', '')
|
||||
other_comments = getattr(mi, 'comments', '')
|
||||
if not my_comments:
|
||||
my_comments = ''
|
||||
if not other_comments:
|
||||
other_comments = ''
|
||||
if len(other_comments.strip()) > len(my_comments.strip()):
|
||||
self.comments = other_comments
|
||||
if replace_metadata:
|
||||
self.comments = getattr(mi, 'comments', '')
|
||||
else:
|
||||
my_comments = getattr(self, 'comments', '')
|
||||
other_comments = getattr(mi, 'comments', '')
|
||||
if not my_comments:
|
||||
my_comments = ''
|
||||
if not other_comments:
|
||||
other_comments = ''
|
||||
if len(other_comments.strip()) > len(my_comments.strip()):
|
||||
self.comments = other_comments
|
||||
|
||||
other_lang = getattr(mi, 'language', None)
|
||||
if other_lang and other_lang.lower() != 'und':
|
||||
|
@ -12,11 +12,15 @@ import re
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre import entity_to_unicode
|
||||
from calibre.utils.date import parse_date
|
||||
|
||||
def get_metadata(stream):
|
||||
src = stream.read()
|
||||
return get_metadata_(src)
|
||||
|
||||
def get_meta_regexp_(name):
|
||||
return re.compile('<meta name=[\'"]' + name + '[\'"] content=[\'"](.+?)[\'"]\s*/?>', re.IGNORECASE)
|
||||
|
||||
def get_metadata_(src, encoding=None):
|
||||
if not isinstance(src, unicode):
|
||||
if not encoding:
|
||||
@ -24,6 +28,9 @@ def get_metadata_(src, encoding=None):
|
||||
else:
|
||||
src = src.decode(encoding, 'replace')
|
||||
|
||||
# Meta data definitions as in
|
||||
# http://www.mobileread.com/forums/showpost.php?p=712544&postcount=9
|
||||
|
||||
# Title
|
||||
title = None
|
||||
pat = re.compile(r'<!--.*?TITLE=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
|
||||
@ -35,6 +42,13 @@ def get_metadata_(src, encoding=None):
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
title = match.group(1)
|
||||
if not title:
|
||||
for x in ('Title','DC.title','DCTERMS.title'):
|
||||
pat = get_meta_regexp_(x)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
title = match.group(1)
|
||||
break
|
||||
|
||||
# Author
|
||||
author = None
|
||||
@ -42,7 +56,15 @@ def get_metadata_(src, encoding=None):
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
author = match.group(2).replace(',', ';')
|
||||
else:
|
||||
for x in ('Author','DC.creator.aut','DCTERMS.creator.aut'):
|
||||
pat = get_meta_regexp_(x)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
author = match.group(1)
|
||||
break
|
||||
|
||||
# Create MetaInformation with Title and Author
|
||||
ent_pat = re.compile(r'&(\S+)?;')
|
||||
if title:
|
||||
title = ent_pat.sub(entity_to_unicode, title)
|
||||
@ -51,18 +73,158 @@ def get_metadata_(src, encoding=None):
|
||||
mi = MetaInformation(title, [author] if author else None)
|
||||
|
||||
# Publisher
|
||||
publisher = None
|
||||
pat = re.compile(r'<!--.*?PUBLISHER=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
mi.publisher = match.group(2)
|
||||
publisher = match.group(2)
|
||||
else:
|
||||
for x in ('Publisher','DC.publisher','DCTERMS.publisher'):
|
||||
pat = get_meta_regexp_(x)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
publisher = match.group(1)
|
||||
break
|
||||
if publisher:
|
||||
mi.publisher = ent_pat.sub(entity_to_unicode, publisher)
|
||||
|
||||
# ISBN
|
||||
isbn = None
|
||||
pat = re.compile(r'<!--.*?ISBN=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
isbn = match.group(1)
|
||||
else:
|
||||
for x in ('ISBN','DC.identifier.ISBN','DCTERMS.identifier.ISBN'):
|
||||
pat = get_meta_regexp_(x)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
isbn = match.group(1)
|
||||
break
|
||||
if isbn:
|
||||
mi.isbn = re.sub(r'[^0-9xX]', '', isbn)
|
||||
|
||||
# LANGUAGE
|
||||
language = None
|
||||
pat = re.compile(r'<!--.*?LANGUAGE=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
language = match.group(1)
|
||||
else:
|
||||
for x in ('DC.language','DCTERMS.language'):
|
||||
pat = get_meta_regexp_(x)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
language = match.group(1)
|
||||
break
|
||||
if language:
|
||||
mi.language = language
|
||||
|
||||
# PUBDATE
|
||||
pubdate = None
|
||||
pat = re.compile(r'<!--.*?PUBDATE=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
pubdate = match.group(1)
|
||||
else:
|
||||
for x in ('Pubdate','Date of publication','DC.date.published','DC.date.publication','DC.date.issued','DCTERMS.issued'):
|
||||
pat = get_meta_regexp_(x)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
pubdate = match.group(1)
|
||||
break
|
||||
if pubdate:
|
||||
try:
|
||||
mi.pubdate = parse_date(pubdate)
|
||||
except:
|
||||
pass
|
||||
|
||||
# TIMESTAMP
|
||||
timestamp = None
|
||||
pat = re.compile(r'<!--.*?TIMESTAMP=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
timestamp = match.group(1)
|
||||
else:
|
||||
for x in ('Timestamp','Date of creation','DC.date.created','DC.date.creation','DCTERMS.created'):
|
||||
pat = get_meta_regexp_(x)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
timestamp = match.group(1)
|
||||
break
|
||||
if timestamp:
|
||||
try:
|
||||
mi.timestamp = parse_date(timestamp)
|
||||
except:
|
||||
pass
|
||||
|
||||
# SERIES
|
||||
series = None
|
||||
pat = re.compile(r'<!--.*?SERIES=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
series = match.group(1)
|
||||
else:
|
||||
pat = get_meta_regexp_("Series")
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
series = match.group(1)
|
||||
if series:
|
||||
mi.series = ent_pat.sub(entity_to_unicode, series)
|
||||
|
||||
# RATING
|
||||
rating = None
|
||||
pat = re.compile(r'<!--.*?RATING=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
rating = match.group(1)
|
||||
else:
|
||||
pat = get_meta_regexp_("Rating")
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
rating = match.group(1)
|
||||
if rating:
|
||||
try:
|
||||
mi.rating = float(rating)
|
||||
if mi.rating < 0:
|
||||
mi.rating = 0
|
||||
if mi.rating > 5:
|
||||
mi.rating /= 2.
|
||||
if mi.rating > 5:
|
||||
mi.rating = 0
|
||||
except:
|
||||
pass
|
||||
|
||||
# COMMENTS
|
||||
comments = None
|
||||
pat = re.compile(r'<!--.*?COMMENTS=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
comments = match.group(1)
|
||||
else:
|
||||
pat = get_meta_regexp_("Comments")
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
comments = match.group(1)
|
||||
if comments:
|
||||
mi.comments = ent_pat.sub(entity_to_unicode, comments)
|
||||
|
||||
# TAGS
|
||||
tags = None
|
||||
pat = re.compile(r'<!--.*?TAGS=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
tags = match.group(1)
|
||||
else:
|
||||
pat = get_meta_regexp_("Tags")
|
||||
match = pat.search(src)
|
||||
if match:
|
||||
tags = match.group(1)
|
||||
if tags:
|
||||
mi.tags = [x.strip() for x in ent_pat.sub(entity_to_unicode,
|
||||
tags).split(",")]
|
||||
|
||||
# Ready to return MetaInformation
|
||||
return mi
|
||||
|
||||
|
||||
|
@ -262,8 +262,11 @@ class CSSFlattener(object):
|
||||
indent = asfloat(style['text-indent'], 0)
|
||||
left += margin
|
||||
if (left + indent) < 0:
|
||||
percent = (margin - indent) / style['width']
|
||||
cssdict['margin-left'] = "%d%%" % (percent * 100)
|
||||
try:
|
||||
percent = (margin - indent) / style['width']
|
||||
cssdict['margin-left'] = "%d%%" % (percent * 100)
|
||||
except ZeroDivisionError:
|
||||
pass
|
||||
left -= indent
|
||||
if 'display' in cssdict and cssdict['display'] == 'in-line':
|
||||
cssdict['display'] = 'inline'
|
||||
|
@ -578,9 +578,7 @@ class DeleteAction(object): # {{{
|
||||
if row is not None:
|
||||
ci = view.model().index(row, 0)
|
||||
if ci.isValid():
|
||||
view.setCurrentIndex(ci)
|
||||
sm = view.selectionModel()
|
||||
sm.select(ci, sm.Select)
|
||||
view.set_current_row(row)
|
||||
else:
|
||||
if not confirm('<p>'+_('The selected books will be '
|
||||
'<b>permanently deleted</b> '
|
||||
@ -806,11 +804,11 @@ class EditMetadataAction(object): # {{{
|
||||
for src_id in src_ids:
|
||||
src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True)
|
||||
if src_mi.comments and orig_dest_comments != src_mi.comments:
|
||||
if not dest_mi.comments or len(dest_mi.comments) == 0:
|
||||
if not dest_mi.comments:
|
||||
dest_mi.comments = src_mi.comments
|
||||
else:
|
||||
dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments)
|
||||
if src_mi.title and src_mi.title and (not dest_mi.title or
|
||||
if src_mi.title and (not dest_mi.title or
|
||||
dest_mi.title == _('Unknown')):
|
||||
dest_mi.title = src_mi.title
|
||||
if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] ==
|
||||
@ -821,8 +819,7 @@ class EditMetadataAction(object): # {{{
|
||||
if not dest_mi.tags:
|
||||
dest_mi.tags = src_mi.tags
|
||||
else:
|
||||
for tag in src_mi.tags:
|
||||
dest_mi.tags.append(tag)
|
||||
dest_mi.tags.extend(src_mi.tags)
|
||||
if src_mi.cover and not dest_mi.cover:
|
||||
dest_mi.cover = src_mi.cover
|
||||
if not dest_mi.publisher:
|
||||
@ -833,6 +830,44 @@ class EditMetadataAction(object): # {{{
|
||||
dest_mi.series = src_mi.series
|
||||
dest_mi.series_index = src_mi.series_index
|
||||
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
|
||||
|
||||
for key in db.field_metadata: #loop thru all defined fields
|
||||
if db.field_metadata[key]['is_custom']:
|
||||
colnum = db.field_metadata[key]['colnum']
|
||||
# Get orig_dest_comments before it gets changed
|
||||
if db.field_metadata[key]['datatype'] == 'comments':
|
||||
orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
|
||||
for src_id in src_ids:
|
||||
dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
|
||||
src_value = db.get_custom(src_id, num=colnum, index_is_id=True)
|
||||
if db.field_metadata[key]['datatype'] == 'comments':
|
||||
if src_value and src_value != orig_dest_value:
|
||||
if not dest_value:
|
||||
db.set_custom(dest_id, src_value, num=colnum)
|
||||
else:
|
||||
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
|
||||
db.set_custom(dest_id, dest_value, num=colnum)
|
||||
if db.field_metadata[key]['datatype'] in \
|
||||
('bool', 'int', 'float', 'rating', 'datetime') \
|
||||
and not dest_value:
|
||||
db.set_custom(dest_id, src_value, num=colnum)
|
||||
if db.field_metadata[key]['datatype'] == 'series' \
|
||||
and not dest_value:
|
||||
if src_value:
|
||||
src_index = db.get_custom_extra(src_id, num=colnum, index_is_id=True)
|
||||
db.set_custom(dest_id, src_value, num=colnum, extra=src_index)
|
||||
if db.field_metadata[key]['datatype'] == 'text' \
|
||||
and not db.field_metadata[key]['is_multiple'] \
|
||||
and not dest_value:
|
||||
db.set_custom(dest_id, src_value, num=colnum)
|
||||
if db.field_metadata[key]['datatype'] == 'text' \
|
||||
and db.field_metadata[key]['is_multiple']:
|
||||
if src_value:
|
||||
if not dest_value:
|
||||
dest_value = src_value
|
||||
else:
|
||||
dest_value.extend(src_value)
|
||||
db.set_custom(dest_id, dest_value, num=colnum)
|
||||
# }}}
|
||||
|
||||
def edit_device_collections(self, view, oncard=None):
|
||||
|
@ -72,7 +72,14 @@ class DeviceJob(BaseJob): # {{{
|
||||
if self._aborted:
|
||||
return
|
||||
self.failed = True
|
||||
self._details = unicode(err) + '\n\n' + \
|
||||
try:
|
||||
ex = unicode(err)
|
||||
except:
|
||||
try:
|
||||
ex = str(err).decode(preferred_encoding, 'replace')
|
||||
except:
|
||||
ex = repr(err)
|
||||
self._details = ex + '\n\n' + \
|
||||
traceback.format_exc()
|
||||
self.exception = err
|
||||
finally:
|
||||
@ -395,8 +402,6 @@ class DeviceAction(QAction): # {{{
|
||||
class DeviceMenu(QMenu): # {{{
|
||||
|
||||
fetch_annotations = pyqtSignal()
|
||||
connect_to_folder = pyqtSignal()
|
||||
connect_to_itunes = pyqtSignal()
|
||||
disconnect_mounted_device = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@ -408,26 +413,6 @@ class DeviceMenu(QMenu): # {{{
|
||||
self.set_default_menu = QMenu(_('Set default send to device action'))
|
||||
self.set_default_menu.setIcon(QIcon(I('config.svg')))
|
||||
|
||||
opts = email_config().parse()
|
||||
default_account = None
|
||||
if opts.accounts:
|
||||
self.email_to_menu = self.addMenu(_('Email to')+'...')
|
||||
keys = sorted(opts.accounts.keys())
|
||||
for account in keys:
|
||||
formats, auto, default = opts.accounts[account]
|
||||
dest = 'mail:'+account+';'+formats
|
||||
if default:
|
||||
default_account = (dest, False, False, I('mail.svg'),
|
||||
_('Email to')+' '+account)
|
||||
action1 = DeviceAction(dest, False, False, I('mail.svg'),
|
||||
_('Email to')+' '+account)
|
||||
action2 = DeviceAction(dest, True, False, I('mail.svg'),
|
||||
_('Email to')+' '+account+ _(' and delete from library'))
|
||||
map(self.email_to_menu.addAction, (action1, action2))
|
||||
map(self._memory.append, (action1, action2))
|
||||
self.email_to_menu.addSeparator()
|
||||
action1.a_s.connect(self.action_triggered)
|
||||
action2.a_s.connect(self.action_triggered)
|
||||
|
||||
basic_actions = [
|
||||
('main:', False, False, I('reader.svg'),
|
||||
@ -457,13 +442,6 @@ class DeviceMenu(QMenu): # {{{
|
||||
]
|
||||
|
||||
|
||||
if default_account is not None:
|
||||
for x in (basic_actions, delete_actions):
|
||||
ac = list(default_account)
|
||||
if x is delete_actions:
|
||||
ac[1] = True
|
||||
x.insert(1, tuple(ac))
|
||||
|
||||
for menu in (self, self.set_default_menu):
|
||||
for actions, desc in (
|
||||
(basic_actions, ''),
|
||||
@ -502,21 +480,7 @@ class DeviceMenu(QMenu): # {{{
|
||||
config['default_send_to_device_action'] = repr(action)
|
||||
|
||||
self.group.triggered.connect(self.change_default_action)
|
||||
if opts.accounts:
|
||||
self.addSeparator()
|
||||
self.addMenu(self.email_to_menu)
|
||||
|
||||
self.addSeparator()
|
||||
mitem = self.addAction(QIcon(I('document_open.svg')), _('Connect to folder'))
|
||||
mitem.setEnabled(True)
|
||||
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
|
||||
self.connect_to_folder_action = mitem
|
||||
|
||||
mitem = self.addAction(QIcon(I('devices/itunes.png')),
|
||||
_('Connect to iTunes'))
|
||||
mitem.setEnabled(True)
|
||||
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
|
||||
self.connect_to_itunes_action = mitem
|
||||
|
||||
mitem = self.addAction(QIcon(I('eject.svg')), _('Eject device'))
|
||||
mitem.setEnabled(False)
|
||||
@ -638,6 +602,8 @@ class DeviceMixin(object): # {{{
|
||||
self.device_error_dialog = error_dialog(self, _('Error'),
|
||||
_('Error communicating with device'), ' ')
|
||||
self.device_error_dialog.setModal(Qt.NonModal)
|
||||
self.share_conn_menu.connect_to_folder.connect(self.connect_to_folder)
|
||||
self.share_conn_menu.connect_to_itunes.connect(self.connect_to_itunes)
|
||||
self.emailer = Emailer()
|
||||
self.emailer.start()
|
||||
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
||||
@ -675,21 +641,20 @@ class DeviceMixin(object): # {{{
|
||||
|
||||
def create_device_menu(self):
|
||||
self._sync_menu = DeviceMenu(self)
|
||||
self.share_conn_menu.build_email_entries(self._sync_menu)
|
||||
self.action_sync.setMenu(self._sync_menu)
|
||||
self.connect(self._sync_menu,
|
||||
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||
self.dispatch_sync_event)
|
||||
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
|
||||
self._sync_menu.connect_to_folder.connect(self.connect_to_folder)
|
||||
self._sync_menu.connect_to_itunes.connect(self.connect_to_itunes)
|
||||
self._sync_menu.disconnect_mounted_device.connect(self.disconnect_mounted_device)
|
||||
if self.device_connected:
|
||||
self._sync_menu.connect_to_folder_action.setEnabled(False)
|
||||
self._sync_menu.connect_to_itunes_action.setEnabled(False)
|
||||
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
|
||||
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
|
||||
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
|
||||
else:
|
||||
self._sync_menu.connect_to_folder_action.setEnabled(True)
|
||||
self._sync_menu.connect_to_itunes_action.setEnabled(True)
|
||||
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
|
||||
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
|
||||
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
|
||||
|
||||
def device_job_exception(self, job):
|
||||
@ -726,16 +691,16 @@ class DeviceMixin(object): # {{{
|
||||
|
||||
def set_device_menu_items_state(self, connected):
|
||||
if connected:
|
||||
self._sync_menu.connect_to_folder_action.setEnabled(False)
|
||||
self._sync_menu.connect_to_itunes_action.setEnabled(False)
|
||||
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
|
||||
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
|
||||
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
|
||||
self._sync_menu.enable_device_actions(True,
|
||||
self.device_manager.device.card_prefix(),
|
||||
self.device_manager.device)
|
||||
self.eject_action.setEnabled(True)
|
||||
else:
|
||||
self._sync_menu.connect_to_folder_action.setEnabled(True)
|
||||
self._sync_menu.connect_to_itunes_action.setEnabled(True)
|
||||
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
|
||||
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
|
||||
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
|
||||
self._sync_menu.enable_device_actions(False)
|
||||
self.eject_action.setEnabled(False)
|
||||
@ -983,6 +948,8 @@ class DeviceMixin(object): # {{{
|
||||
else:
|
||||
self.status_bar.show_message(_('Sent by email:') + ', '.join(good),
|
||||
5000)
|
||||
if remove:
|
||||
self.library_view.model().delete_books_by_id(remove)
|
||||
|
||||
def cover_to_thumbnail(self, data):
|
||||
p = QPixmap()
|
||||
|
@ -10,7 +10,7 @@ import os
|
||||
from PyQt4.Qt import QDialog
|
||||
|
||||
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
|
||||
from calibre.gui2 import error_dialog, choose_dir, warning_dialog
|
||||
from calibre.gui2 import error_dialog, choose_dir
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre import isbytestring, patheq
|
||||
from calibre.utils.config import prefs
|
||||
@ -62,12 +62,6 @@ class ChooseLibrary(QDialog, Ui_Dialog):
|
||||
return True
|
||||
|
||||
def perform_action(self, ac, loc):
|
||||
if ac in ('new', 'existing'):
|
||||
warning_dialog(self.parent(), _('Custom columns'),
|
||||
_('If you use custom columns and they differ between '
|
||||
'libraries, you will have various problems. Best '
|
||||
'to ensure you have the same custom columns in each '
|
||||
'library.'), show=True)
|
||||
if ac in ('new', 'existing'):
|
||||
prefs['library_path'] = loc
|
||||
self.callback(loc)
|
||||
|
@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
from PyQt4.Qt import QDialog
|
||||
from PyQt4.Qt import Qt, QDialog
|
||||
from calibre.gui2.dialogs.comments_dialog_ui import Ui_CommentsDialog
|
||||
|
||||
class CommentsDialog(QDialog, Ui_CommentsDialog):
|
||||
@ -12,6 +12,11 @@ class CommentsDialog(QDialog, Ui_CommentsDialog):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_CommentsDialog.__init__(self)
|
||||
self.setupUi(self)
|
||||
# Remove help icon on title bar
|
||||
icon = self.windowIcon()
|
||||
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
if text is not None:
|
||||
self.textbox.setPlainText(text)
|
||||
self.textbox.setTabChangesFocus(True)
|
||||
|
@ -195,22 +195,32 @@ class PluginModel(QAbstractItemModel):
|
||||
|
||||
class CategoryModel(QStringListModel):
|
||||
|
||||
CATEGORIES = [
|
||||
('general', _('General'), 'dialog_information.svg'),
|
||||
('interface', _('Interface'), 'lookfeel.svg'),
|
||||
('conversion', _('Conversion'), 'convert.svg'),
|
||||
('email', _('Email\nDelivery'), 'mail.svg'),
|
||||
('add/save', _('Add/Save'), 'save.svg'),
|
||||
('advanced', _('Advanced'), 'view.svg'),
|
||||
('server', _('Content\nServer'), 'network-server.svg'),
|
||||
('plugins', _('Plugins'), 'plugins.svg'),
|
||||
]
|
||||
|
||||
def __init__(self, *args):
|
||||
QStringListModel.__init__(self, *args)
|
||||
self.setStringList([_('General'), _('Interface'), _('Conversion'),
|
||||
_('Email\nDelivery'), _('Add/Save'),
|
||||
_('Advanced'), _('Content\nServer'), _('Plugins')])
|
||||
self.icons = list(map(QVariant, map(QIcon,
|
||||
[I('dialog_information.svg'), I('lookfeel.svg'),
|
||||
I('convert.svg'),
|
||||
I('mail.svg'), I('save.svg'), I('view.svg'),
|
||||
I('network-server.svg'), I('plugins.svg')])))
|
||||
self.setStringList([x[1] for x in self.CATEGORIES])
|
||||
|
||||
def data(self, index, role):
|
||||
if role == Qt.DecorationRole:
|
||||
return self.icons[index.row()]
|
||||
return QVariant(QIcon(I(self.CATEGORIES[index.row()][2])))
|
||||
return QStringListModel.data(self, index, role)
|
||||
|
||||
def index_for_name(self, name):
|
||||
for i, x in enumerate(self.CATEGORIES):
|
||||
if x[0] == name:
|
||||
return self.index(i)
|
||||
return self.index(0)
|
||||
|
||||
class EmailAccounts(QAbstractTableModel):
|
||||
|
||||
def __init__(self, accounts):
|
||||
@ -332,7 +342,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
def category_current_changed(self, n, p):
|
||||
self.stackedWidget.setCurrentIndex(n.row())
|
||||
|
||||
def __init__(self, parent, library_view, server=None):
|
||||
def __init__(self, parent, library_view, server=None,
|
||||
initial_category='general'):
|
||||
ResizableDialog.__init__(self, parent)
|
||||
self._category_model = CategoryModel()
|
||||
|
||||
@ -461,7 +472,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.button_osx_symlinks.setVisible(isosx)
|
||||
self.separate_cover_flow.setChecked(config['separate_cover_flow'])
|
||||
self.setup_email_page()
|
||||
self.category_view.setCurrentIndex(self.category_view.model().index(0))
|
||||
self.delete_news.setEnabled(bool(self.sync_news.isChecked()))
|
||||
self.connect(self.sync_news, SIGNAL('toggled(bool)'),
|
||||
self.delete_news.setEnabled)
|
||||
@ -488,6 +498,22 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.opt_gui_layout.setCurrentIndex(li)
|
||||
self.opt_disable_animations.setChecked(config['disable_animations'])
|
||||
self.opt_show_donate_button.setChecked(config['show_donate_button'])
|
||||
idx = 0
|
||||
for i, x in enumerate([(_('Small'), 'small'), (_('Medium'), 'medium'),
|
||||
(_('Large'), 'large')]):
|
||||
if x[1] == gprefs.get('toolbar_icon_size', 'medium'):
|
||||
idx = i
|
||||
self.opt_toolbar_icon_size.addItem(x[0], x[1])
|
||||
self.opt_toolbar_icon_size.setCurrentIndex(idx)
|
||||
idx = 0
|
||||
for i, x in enumerate([(_('Automatic'), 'auto'), (_('Always'), 'always'),
|
||||
(_('Never'), 'never')]):
|
||||
if x[1] == gprefs.get('toolbar_text', 'auto'):
|
||||
idx = i
|
||||
self.opt_toolbar_text.addItem(x[0], x[1])
|
||||
self.opt_toolbar_text.setCurrentIndex(idx)
|
||||
|
||||
self.category_view.setCurrentIndex(self.category_view.model().index_for_name(initial_category))
|
||||
|
||||
def check_port_value(self, *args):
|
||||
port = self.port.value()
|
||||
@ -857,6 +883,10 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
config['disable_animations'] = bool(self.opt_disable_animations.isChecked())
|
||||
config['show_donate_button'] = bool(self.opt_show_donate_button.isChecked())
|
||||
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
|
||||
for x in ('toolbar_icon_size', 'toolbar_text'):
|
||||
w = getattr(self, 'opt_'+x)
|
||||
data = w.itemData(w.currentIndex()).toString()
|
||||
gprefs[x] = unicode(data)
|
||||
fmts = []
|
||||
for i in range(self.viewer.count()):
|
||||
if self.viewer.item(i).checkState() == Qt.Checked:
|
||||
@ -942,6 +972,5 @@ if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
d=ConfigDialog(None, LibraryDatabase2('/tmp'))
|
||||
d.category_view.setCurrentIndex(d.category_view.model().index(0))
|
||||
d.show()
|
||||
app.exec_()
|
||||
|
@ -346,21 +346,21 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<item row="8" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="sync_news">
|
||||
<property name="text">
|
||||
<string>Automatically send downloaded &news to ebook reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0" colspan="2">
|
||||
<item row="9" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="delete_news">
|
||||
<property name="text">
|
||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0" colspan="2">
|
||||
<item row="10" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
@ -377,7 +377,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="10" column="0" colspan="2">
|
||||
<item row="11" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
@ -580,6 +580,41 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>&Toolbar</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>&Icon size:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_toolbar_icon_size</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="opt_toolbar_text"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Show &text under icons:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_toolbar_text</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_6">
|
||||
|
@ -44,6 +44,11 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_QCreateCustomColumn.__init__(self)
|
||||
self.setupUi(self)
|
||||
# Remove help icon on title bar
|
||||
icon = self.windowIcon()
|
||||
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.simple_error = partial(error_dialog, self, show=True,
|
||||
show_copy_button=False)
|
||||
self.connect(self.button_box, SIGNAL("accepted()"), self.accept)
|
||||
|
@ -21,6 +21,10 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_EditAuthorsDialog.__init__(self)
|
||||
self.setupUi(self)
|
||||
# Remove help icon on title bar
|
||||
icon = self.windowIcon()
|
||||
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.buttonBox.accepted.connect(self.accepted)
|
||||
|
||||
|
@ -27,10 +27,11 @@ from calibre.ebooks.metadata import string_to_authors, \
|
||||
from calibre.ebooks.metadata.library_thing import cover_from_isbn
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.utils.config import prefs, tweaks
|
||||
from calibre.utils.date import qt_to_dt
|
||||
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
|
||||
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
||||
from calibre.gui2.dialogs.config.social import SocialMetadata
|
||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
from calibre import strftime
|
||||
|
||||
class CoverFetcher(QThread):
|
||||
|
||||
@ -75,13 +76,20 @@ class CoverFetcher(QThread):
|
||||
|
||||
|
||||
class Format(QListWidgetItem):
|
||||
def __init__(self, parent, ext, size, path=None):
|
||||
|
||||
def __init__(self, parent, ext, size, path=None, timestamp=None):
|
||||
self.path = path
|
||||
self.ext = ext
|
||||
self.size = float(size)/(1024*1024)
|
||||
text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
|
||||
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
||||
text, parent, QListWidgetItem.UserType)
|
||||
if timestamp is not None:
|
||||
ts = timestamp.astimezone(local_tz)
|
||||
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
|
||||
text = _('Last modified: %s')%t
|
||||
self.setToolTip(text)
|
||||
self.setStatusTip(text)
|
||||
|
||||
|
||||
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
@ -151,14 +159,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
nfile = run_plugins_on_import(_file)
|
||||
if nfile is not None:
|
||||
_file = nfile
|
||||
size = os.stat(_file).st_size
|
||||
stat = os.stat(_file)
|
||||
size = stat.st_size
|
||||
ext = os.path.splitext(_file)[1].lower().replace('.', '')
|
||||
timestamp = utcfromtimestamp(stat.st_mtime)
|
||||
for row in range(self.formats.count()):
|
||||
fmt = self.formats.item(row)
|
||||
if fmt.ext.lower() == ext:
|
||||
self.formats.takeItem(row)
|
||||
break
|
||||
Format(self.formats, ext, size, path=_file)
|
||||
Format(self.formats, ext, size, path=_file, timestamp=timestamp)
|
||||
self.formats_changed = True
|
||||
added = True
|
||||
if bad_perms:
|
||||
@ -379,9 +389,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
if not ext:
|
||||
ext = ''
|
||||
size = self.db.sizeof_format(row, ext)
|
||||
timestamp = self.db.format_last_modified(self.id, ext)
|
||||
if size is None:
|
||||
continue
|
||||
Format(self.formats, ext, size)
|
||||
Format(self.formats, ext, size, timestamp=timestamp)
|
||||
|
||||
|
||||
self.initialize_combos()
|
||||
|
@ -25,8 +25,8 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
|
||||
self.current_search_name = None
|
||||
self.searches = {}
|
||||
self.searches_to_delete = []
|
||||
for name in saved_searches.names():
|
||||
self.searches[name] = saved_searches.lookup(name)
|
||||
for name in saved_searches().names():
|
||||
self.searches[name] = saved_searches().lookup(name)
|
||||
|
||||
self.populate_search_list()
|
||||
if initial_search is not None and initial_search in self.searches:
|
||||
@ -78,7 +78,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
|
||||
if self.current_search_name:
|
||||
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
|
||||
for name in self.searches_to_delete:
|
||||
saved_searches.delete(name)
|
||||
saved_searches().delete(name)
|
||||
for name in self.searches:
|
||||
saved_searches.add(name, self.searches[name])
|
||||
saved_searches().add(name, self.searches[name])
|
||||
QDialog.accept(self)
|
||||
|
@ -62,6 +62,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
self.search_done)
|
||||
self.disconnect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
|
||||
self.search.search_done)
|
||||
self.search.search.disconnect()
|
||||
self.recipe_model = None
|
||||
|
||||
def search_done(self, *args):
|
||||
|
@ -7,7 +7,6 @@ from PyQt4.QtCore import SIGNAL, Qt
|
||||
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
||||
|
||||
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.constants import islinux
|
||||
|
||||
@ -29,6 +28,11 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
Ui_TagCategories.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
# Remove help icon on title bar
|
||||
icon = self.windowIcon()
|
||||
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.db = db
|
||||
self.applied_items = []
|
||||
|
||||
@ -63,7 +67,7 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
self.all_items.append(t)
|
||||
self.all_items_dict[label+':'+n] = t
|
||||
|
||||
self.categories = dict.copy(prefs['user_categories'])
|
||||
self.categories = dict.copy(db.prefs.get('user_categories', {}))
|
||||
if self.categories is None:
|
||||
self.categories = {}
|
||||
for cat in self.categories:
|
||||
@ -182,7 +186,7 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
|
||||
def accept(self):
|
||||
self.save_category()
|
||||
prefs['user_categories'] = self.categories
|
||||
self.db.prefs['user_categories'] = self.categories
|
||||
QDialog.accept(self)
|
||||
|
||||
def save_category(self):
|
||||
|
@ -43,6 +43,10 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_TagListEditor.__init__(self)
|
||||
self.setupUi(self)
|
||||
# Remove help icon on title bar
|
||||
icon = self.windowIcon()
|
||||
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.to_rename = {}
|
||||
self.to_delete = []
|
||||
|
@ -59,6 +59,7 @@ class LibraryViewMixin(object): # {{{
|
||||
self.action_open_containing_folder,
|
||||
self.action_show_book_details,
|
||||
self.action_del,
|
||||
self.action_conn_share,
|
||||
add_to_library = None,
|
||||
edit_device_collections=None,
|
||||
similar_menu=similar_menu)
|
||||
@ -67,21 +68,24 @@ class LibraryViewMixin(object): # {{{
|
||||
edit_device_collections = (_('Manage collections'),
|
||||
partial(self.edit_device_collections, oncard=None))
|
||||
self.memory_view.set_context_menu(None, None, None,
|
||||
self.action_view, self.action_save, None, None, self.action_del,
|
||||
self.action_view, self.action_save, None, None,
|
||||
self.action_del, None,
|
||||
add_to_library=add_to_library,
|
||||
edit_device_collections=edit_device_collections)
|
||||
|
||||
edit_device_collections = (_('Manage collections'),
|
||||
partial(self.edit_device_collections, oncard='carda'))
|
||||
self.card_a_view.set_context_menu(None, None, None,
|
||||
self.action_view, self.action_save, None, None, self.action_del,
|
||||
self.action_view, self.action_save, None, None,
|
||||
self.action_del, None,
|
||||
add_to_library=add_to_library,
|
||||
edit_device_collections=edit_device_collections)
|
||||
|
||||
edit_device_collections = (_('Manage collections'),
|
||||
partial(self.edit_device_collections, oncard='cardb'))
|
||||
self.card_b_view.set_context_menu(None, None, None,
|
||||
self.action_view, self.action_save, None, None, self.action_del,
|
||||
self.action_view, self.action_save, None, None,
|
||||
self.action_del, None,
|
||||
add_to_library=add_to_library,
|
||||
edit_device_collections=edit_device_collections)
|
||||
|
||||
|
@ -16,14 +16,14 @@ from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \
|
||||
from calibre.constants import __appname__, isosx
|
||||
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
|
||||
from calibre.gui2.throbber import ThrobbingButton
|
||||
from calibre.gui2 import config, open_url
|
||||
from calibre.gui2 import config, open_url, gprefs
|
||||
from calibre.gui2.widgets import ComboBoxWithHelp
|
||||
from calibre import human_readable
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||
from calibre.utils.smtp import config as email_config
|
||||
|
||||
ICON_SIZE = 48
|
||||
|
||||
class SaveMenu(QMenu): # {{{
|
||||
|
||||
@ -228,12 +228,11 @@ class ToolBar(QToolBar): # {{{
|
||||
self.setFloatable(False)
|
||||
self.setOrientation(Qt.Horizontal)
|
||||
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
|
||||
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
||||
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
||||
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
|
||||
self.donate = donate
|
||||
self.apply_settings()
|
||||
|
||||
self.all_actions = actions
|
||||
self.donate = donate
|
||||
self.location_manager = location_manager
|
||||
self.location_manager.locations_changed.connect(self.build_bar)
|
||||
self.d_widget = QWidget()
|
||||
@ -242,6 +241,17 @@ class ToolBar(QToolBar): # {{{
|
||||
donate.setAutoRaise(True)
|
||||
donate.setCursor(Qt.PointingHandCursor)
|
||||
self.build_bar()
|
||||
self.preferred_width = self.sizeHint().width()
|
||||
|
||||
def apply_settings(self):
|
||||
sz = gprefs.get('toolbar_icon_size', 'medium')
|
||||
sz = {'small':24, 'medium':48, 'large':64}[sz]
|
||||
self.setIconSize(QSize(sz, sz))
|
||||
style = Qt.ToolButtonTextUnderIcon
|
||||
if gprefs.get('toolbar_text', 'auto') == 'never':
|
||||
style = Qt.ToolButtonIconOnly
|
||||
self.setToolButtonStyle(style)
|
||||
self.donate.set_normal_icon_size(sz, sz)
|
||||
|
||||
def contextMenuEvent(self, *args):
|
||||
pass
|
||||
@ -262,7 +272,9 @@ class ToolBar(QToolBar): # {{{
|
||||
ch.setCursor(Qt.PointingHandCursor)
|
||||
ch.setAutoRaise(True)
|
||||
if ac.menu() is not None:
|
||||
ch.setPopupMode(ch.MenuButtonPopup)
|
||||
name = getattr(ac, 'action_name', None)
|
||||
ch.setPopupMode(ch.InstantPopup if name == 'conn_share'
|
||||
else ch.MenuButtonPopup)
|
||||
|
||||
for x in actions:
|
||||
self.addAction(x)
|
||||
@ -292,11 +304,16 @@ class ToolBar(QToolBar): # {{{
|
||||
a.setText(text)
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
style = Qt.ToolButtonTextUnderIcon
|
||||
if self.size().width() < 1260:
|
||||
style = Qt.ToolButtonIconOnly
|
||||
self.setToolButtonStyle(style)
|
||||
QToolBar.resizeEvent(self, ev)
|
||||
style = Qt.ToolButtonTextUnderIcon
|
||||
p = gprefs.get('toolbar_text', 'auto')
|
||||
if p == 'never':
|
||||
style = Qt.ToolButtonIconOnly
|
||||
|
||||
if p == 'auto' and self.preferred_width > self.width()+35:
|
||||
style = Qt.ToolButtonIconOnly
|
||||
|
||||
self.setToolButtonStyle(style)
|
||||
|
||||
def database_changed(self, db):
|
||||
pass
|
||||
@ -306,6 +323,62 @@ class ToolBar(QToolBar): # {{{
|
||||
class Action(QAction):
|
||||
pass
|
||||
|
||||
class ShareConnMenu(QMenu): # {{{
|
||||
|
||||
connect_to_folder = pyqtSignal()
|
||||
connect_to_itunes = pyqtSignal()
|
||||
config_email = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QMenu.__init__(self, parent)
|
||||
mitem = self.addAction(QIcon(I('devices/folder.svg')), _('Connect to folder'))
|
||||
mitem.setEnabled(True)
|
||||
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
|
||||
self.connect_to_folder_action = mitem
|
||||
|
||||
mitem = self.addAction(QIcon(I('devices/itunes.png')),
|
||||
_('Connect to iTunes'))
|
||||
mitem.setEnabled(True)
|
||||
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
|
||||
self.connect_to_itunes_action = mitem
|
||||
self.addSeparator()
|
||||
self.email_actions = []
|
||||
|
||||
def build_email_entries(self, sync_menu):
|
||||
from calibre.gui2.device import DeviceAction
|
||||
for ac in self.email_actions:
|
||||
self.removeAction(ac)
|
||||
self.email_actions = []
|
||||
opts = email_config().parse()
|
||||
if opts.accounts:
|
||||
self.email_to_menu = QMenu(_('Email to')+'...', self)
|
||||
keys = sorted(opts.accounts.keys())
|
||||
for account in keys:
|
||||
formats, auto, default = opts.accounts[account]
|
||||
dest = 'mail:'+account+';'+formats
|
||||
action1 = DeviceAction(dest, False, False, I('mail.svg'),
|
||||
_('Email to')+' '+account)
|
||||
action2 = DeviceAction(dest, True, False, I('mail.svg'),
|
||||
_('Email to')+' '+account+ _(' and delete from library'))
|
||||
map(self.email_to_menu.addAction, (action1, action2))
|
||||
if default:
|
||||
map(self.addAction, (action1, action2))
|
||||
map(self.email_actions.append, (action1, action2))
|
||||
self.email_to_menu.addSeparator()
|
||||
action1.a_s.connect(sync_menu.action_triggered)
|
||||
action2.a_s.connect(sync_menu.action_triggered)
|
||||
ac = self.addMenu(self.email_to_menu)
|
||||
self.email_actions.append(ac)
|
||||
else:
|
||||
ac = self.addAction(_('Setup email based sharing of books'))
|
||||
self.email_actions.append(ac)
|
||||
ac.triggered.connect(self.setup_email)
|
||||
|
||||
def setup_email(self, *args):
|
||||
self.config_email.emit()
|
||||
|
||||
# }}}
|
||||
|
||||
class MainWindowMixin(object):
|
||||
|
||||
def __init__(self, db):
|
||||
@ -321,7 +394,6 @@ class MainWindowMixin(object):
|
||||
self.centralwidget.setLayout(self._central_widget_layout)
|
||||
self.resize(1012, 740)
|
||||
self.donate_button = ThrobbingButton(self.centralwidget)
|
||||
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
|
||||
self.location_manager = LocationManager(self)
|
||||
|
||||
self.init_scheduler(db)
|
||||
@ -341,7 +413,6 @@ class MainWindowMixin(object):
|
||||
self.scheduler.start_recipe_fetch.connect(
|
||||
self.download_scheduled_recipe, type=Qt.QueuedConnection)
|
||||
|
||||
|
||||
def read_toolbar_settings(self):
|
||||
pass
|
||||
|
||||
@ -372,18 +443,19 @@ class MainWindowMixin(object):
|
||||
setattr(self, 'action_'+name, action)
|
||||
all_actions.append(action)
|
||||
|
||||
ac(0, 7, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
|
||||
ac(0, 0, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
|
||||
ac(1, 1, 0, 'edit', _('Edit metadata'), 'edit_input.svg', _('E'))
|
||||
ac(2, 2, 3, 'convert', _('Convert books'), 'convert.svg', _('C'))
|
||||
ac(3, 3, 0, 'view', _('View'), 'view.svg', _('V'))
|
||||
ac(4, 4, 3, 'choose_library', _('%d books')%0, 'lt.png',
|
||||
ac(-1, 4, 0, 'sync', _('Send to device'), 'sync.svg')
|
||||
ac(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png',
|
||||
tooltip=_('Choose calibre library to work with'))
|
||||
ac(5, 5, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
|
||||
ac(6, 6, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
|
||||
ac(7, 0, 0, 'sync', _('Send to device'), 'sync.svg')
|
||||
ac(8, 8, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
|
||||
ac(9, 9, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
|
||||
ac(10, 10, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
|
||||
ac(6, 6, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
|
||||
ac(7, 7, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
|
||||
ac(8, 8, 0, 'conn_share', _('Connect/share'), 'connect_share.svg')
|
||||
ac(9, 9, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
|
||||
ac(10, 10, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
|
||||
ac(11, 11, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
|
||||
|
||||
ac(-1, -1, 0, 'merge', _('Merge book records'), 'merge_books.svg', _('M'))
|
||||
ac(-1, -1, 0, 'open_containing_folder', _('Open containing folder'),
|
||||
@ -402,6 +474,10 @@ class MainWindowMixin(object):
|
||||
self.action_news.setMenu(self.scheduler.news_menu)
|
||||
self.action_news.triggered.connect(
|
||||
self.scheduler.show_dialog)
|
||||
self.share_conn_menu = ShareConnMenu(self)
|
||||
self.share_conn_menu.config_email.connect(partial(self.do_config,
|
||||
initial_category='email'))
|
||||
self.action_conn_share.setMenu(self.share_conn_menu)
|
||||
|
||||
self.action_help.triggered.connect(self.show_help)
|
||||
md = QMenu()
|
||||
@ -528,6 +604,7 @@ class MainWindowMixin(object):
|
||||
for x in (self.preferences_action, self.action_preferences):
|
||||
x.triggered.connect(self.do_config)
|
||||
|
||||
|
||||
return all_actions
|
||||
# }}}
|
||||
|
||||
|
@ -214,13 +214,17 @@ class BooksView(QTableView): # {{{
|
||||
state['column_sizes'][name] = h.sectionSize(i)
|
||||
return state
|
||||
|
||||
def write_state(self, state):
|
||||
db = getattr(self.model(), 'db', None)
|
||||
name = unicode(self.objectName())
|
||||
if name and db is not None:
|
||||
db.prefs.set(name + ' books view state', state)
|
||||
|
||||
def save_state(self):
|
||||
# Only save if we have been initialized (set_database called)
|
||||
if len(self.column_map) > 0 and self.was_restored:
|
||||
state = self.get_state()
|
||||
name = unicode(self.objectName())
|
||||
if name:
|
||||
gprefs.set(name + ' books view state', state)
|
||||
self.write_state(state)
|
||||
|
||||
def cleanup_sort_history(self, sort_history):
|
||||
history = []
|
||||
@ -298,11 +302,27 @@ class BooksView(QTableView): # {{{
|
||||
old_state['column_sizes'][name] += 12
|
||||
return old_state
|
||||
|
||||
def restore_state(self):
|
||||
def get_old_state(self):
|
||||
ans = None
|
||||
name = unicode(self.objectName())
|
||||
old_state = None
|
||||
if name:
|
||||
old_state = gprefs.get(name + ' books view state', None)
|
||||
name += ' books view state'
|
||||
db = getattr(self.model(), 'db', None)
|
||||
if db is not None:
|
||||
ans = db.prefs.get(name, None)
|
||||
if ans is None:
|
||||
ans = gprefs.get(name, None)
|
||||
try:
|
||||
del gprefs[name]
|
||||
except:
|
||||
pass
|
||||
if ans is not None:
|
||||
db.prefs[name] = ans
|
||||
return ans
|
||||
|
||||
|
||||
def restore_state(self):
|
||||
old_state = self.get_old_state()
|
||||
if old_state is None:
|
||||
old_state = self.get_default_state()
|
||||
|
||||
@ -370,7 +390,7 @@ class BooksView(QTableView): # {{{
|
||||
|
||||
# Context Menu {{{
|
||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
||||
save, open_folder, book_details, delete,
|
||||
save, open_folder, book_details, delete, conn_share,
|
||||
similar_menu=None, add_to_library=None,
|
||||
edit_device_collections=None):
|
||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||
@ -381,6 +401,8 @@ class BooksView(QTableView): # {{{
|
||||
self.context_menu.addAction(send_to_device)
|
||||
if convert is not None:
|
||||
self.context_menu.addAction(convert)
|
||||
if conn_share is not None:
|
||||
self.context_menu.addAction(conn_share)
|
||||
self.context_menu.addAction(view)
|
||||
self.context_menu.addAction(save)
|
||||
if open_folder is not None:
|
||||
@ -456,14 +478,20 @@ class BooksView(QTableView): # {{{
|
||||
def set_current_row(self, row, select=True):
|
||||
if row > -1:
|
||||
h = self.horizontalHeader()
|
||||
for i in range(h.count()):
|
||||
if not h.isSectionHidden(i):
|
||||
index = self.model().index(row, i)
|
||||
self.setCurrentIndex(index)
|
||||
if select:
|
||||
sm = self.selectionModel()
|
||||
sm.select(index, sm.ClearAndSelect|sm.Rows)
|
||||
break
|
||||
logical_indices = list(range(h.count()))
|
||||
logical_indices = [x for x in logical_indices if not
|
||||
h.isSectionHidden(x)]
|
||||
pairs = [(x, h.visualIndex(x)) for x in logical_indices if
|
||||
h.visualIndex(x) > -1]
|
||||
if not pairs:
|
||||
pairs = [(0, 0)]
|
||||
pairs.sort(cmp=lambda x,y:cmp(x[1], y[1]))
|
||||
i = pairs[0][0]
|
||||
index = self.model().index(row, i)
|
||||
self.setCurrentIndex(index)
|
||||
if select:
|
||||
sm = self.selectionModel()
|
||||
sm.select(index, sm.ClearAndSelect|sm.Rows)
|
||||
|
||||
def close(self):
|
||||
self._model.close()
|
||||
@ -507,6 +535,19 @@ class DeviceBooksView(BooksView): # {{{
|
||||
self.context_menu.popup(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
def get_old_state(self):
|
||||
ans = None
|
||||
name = unicode(self.objectName())
|
||||
if name:
|
||||
name += ' books view state'
|
||||
ans = gprefs.get(name, None)
|
||||
return ans
|
||||
|
||||
def write_state(self, state):
|
||||
name = unicode(self.objectName())
|
||||
if name:
|
||||
gprefs.set(name + ' books view state', state)
|
||||
|
||||
def set_database(self, db):
|
||||
self._model.set_database(db)
|
||||
self.restore_state()
|
||||
|
@ -10,13 +10,12 @@ import re
|
||||
|
||||
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
|
||||
pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \
|
||||
QAction, QKeySequence
|
||||
QAction, QKeySequence, QTimer
|
||||
|
||||
from calibre.gui2 import config
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
||||
from calibre.gui2.dialogs.search import SearchDialog
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
|
||||
class SearchLineEdit(QLineEdit):
|
||||
@ -83,7 +82,9 @@ class SearchBox2(QComboBox):
|
||||
self.help_state = False
|
||||
self.as_you_type = True
|
||||
self.prev_search = ''
|
||||
self.timer = None
|
||||
self.timer = QTimer()
|
||||
self.timer.setSingleShot(True)
|
||||
self.timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection)
|
||||
self.setInsertPolicy(self.NoInsert)
|
||||
self.setMaxCount(self.MAX_COUNT)
|
||||
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
||||
@ -117,9 +118,6 @@ class SearchBox2(QComboBox):
|
||||
self.search.emit('')
|
||||
self._in_a_search = False
|
||||
self.setEditText(self.help_text)
|
||||
if self.timer is not None: # Turn off any timers that got started in setEditText
|
||||
self.killTimer(self.timer)
|
||||
self.timer = None
|
||||
self.line_edit.home(False)
|
||||
self.line_edit.setStyleSheet(
|
||||
'QLineEdit { color: gray; background-color: %s; }' %
|
||||
@ -148,18 +146,15 @@ class SearchBox2(QComboBox):
|
||||
self._in_a_search = False
|
||||
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
||||
self.do_search()
|
||||
self.timer = self.startTimer(self.__class__.INTERVAL)
|
||||
self.timer.start(1500)
|
||||
|
||||
def mouse_released(self, event):
|
||||
self.normalize_state()
|
||||
if self.as_you_type:
|
||||
self.timer = self.startTimer(self.__class__.INTERVAL)
|
||||
self.timer.start(1500)
|
||||
|
||||
def timerEvent(self, event):
|
||||
self.killTimer(event.timerId())
|
||||
if event.timerId() == self.timer:
|
||||
self.timer = None
|
||||
self.do_search()
|
||||
def timer_event(self):
|
||||
self.do_search()
|
||||
|
||||
def history_selected(self, text):
|
||||
self.emit(SIGNAL('changed()'))
|
||||
@ -213,9 +208,6 @@ class SearchBox2(QComboBox):
|
||||
return
|
||||
self.normalize_state()
|
||||
self.setEditText(txt)
|
||||
if self.timer is not None: # Turn off any timers that got started in setEditText
|
||||
self.killTimer(self.timer)
|
||||
self.timer = None
|
||||
self.search.emit(txt)
|
||||
self.line_edit.end(False)
|
||||
self.initial_state = False
|
||||
@ -259,8 +251,7 @@ class SavedSearchBox(QComboBox):
|
||||
self.setMinimumContentsLength(10)
|
||||
self.tool_tip_text = self.toolTip()
|
||||
|
||||
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
|
||||
self.saved_searches = _saved_searches
|
||||
def initialize(self, _search_box, colorize=False, help_text=_('Search')):
|
||||
self.search_box = _search_box
|
||||
self.help_text = help_text
|
||||
self.colorize = colorize
|
||||
@ -302,11 +293,11 @@ class SavedSearchBox(QComboBox):
|
||||
self.normalize_state()
|
||||
self.search_box.set_search_string(u'search:"%s"' % qname)
|
||||
self.setEditText(qname)
|
||||
self.setToolTip(self.saved_searches.lookup(qname))
|
||||
self.setToolTip(saved_searches().lookup(qname))
|
||||
|
||||
def initialize_saved_search_names(self):
|
||||
self.clear()
|
||||
qnames = self.saved_searches.names()
|
||||
qnames = saved_searches().names()
|
||||
self.addItems(qnames)
|
||||
self.setCurrentIndex(-1)
|
||||
|
||||
@ -319,10 +310,10 @@ class SavedSearchBox(QComboBox):
|
||||
idx = self.currentIndex
|
||||
if idx < 0:
|
||||
return
|
||||
ss = self.saved_searches.lookup(unicode(self.currentText()))
|
||||
ss = saved_searches().lookup(unicode(self.currentText()))
|
||||
if ss is None:
|
||||
return
|
||||
self.saved_searches.delete(unicode(self.currentText()))
|
||||
saved_searches().delete(unicode(self.currentText()))
|
||||
self.clear_to_help()
|
||||
self.search_box.clear_to_help()
|
||||
self.emit(SIGNAL('changed()'))
|
||||
@ -332,8 +323,8 @@ class SavedSearchBox(QComboBox):
|
||||
name = unicode(self.currentText())
|
||||
if self.help_state or not name.strip():
|
||||
name = unicode(self.search_box.text()).replace('"', '')
|
||||
self.saved_searches.delete(name)
|
||||
self.saved_searches.add(name, unicode(self.search_box.text()))
|
||||
saved_searches().delete(name)
|
||||
saved_searches().add(name, unicode(self.search_box.text()))
|
||||
# now go through an initialization cycle to ensure that the combobox has
|
||||
# the new search in it, that it is selected, and that the search box
|
||||
# references the new search instead of the text in the search.
|
||||
@ -348,7 +339,7 @@ class SavedSearchBox(QComboBox):
|
||||
idx = self.currentIndex();
|
||||
if idx < 0:
|
||||
return
|
||||
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
|
||||
self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText())))
|
||||
|
||||
class SearchBoxMixin(object):
|
||||
|
||||
@ -390,11 +381,12 @@ class SearchBoxMixin(object):
|
||||
|
||||
class SavedSearchBoxMixin(object):
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
|
||||
self.saved_searches_changed()
|
||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
|
||||
self.saved_search.initialize(saved_searches, self.search, colorize=True,
|
||||
self.saved_search.initialize(self.search, colorize=True,
|
||||
help_text=_('Saved Searches'))
|
||||
self.connect(self.save_search_button, SIGNAL('clicked()'),
|
||||
self.saved_search.save_search_button_clicked)
|
||||
@ -409,9 +401,12 @@ class SavedSearchBoxMixin(object):
|
||||
b = getattr(self, x+'_search_button')
|
||||
b.setStatusTip(b.toolTip())
|
||||
|
||||
def set_database(self, db):
|
||||
self.db = db
|
||||
self.saved_searches_changed()
|
||||
|
||||
def saved_searches_changed(self):
|
||||
p = prefs['saved_searches'].keys()
|
||||
p = saved_searches().names()
|
||||
p.sort()
|
||||
t = unicode(self.search_restriction.currentText())
|
||||
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
|
||||
|
@ -17,7 +17,6 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
||||
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
from calibre.gui2 import config, NONE
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.library.field_metadata import TagsIcons
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.gui2 import error_dialog
|
||||
@ -224,7 +223,7 @@ class TagsView(QTreeView): # {{{
|
||||
|
||||
# Always show the user categories editor
|
||||
self.context_menu.addSeparator()
|
||||
if category in prefs['user_categories'].keys():
|
||||
if category in self.db.prefs.get('user_categories', {}).keys():
|
||||
self.context_menu.addAction(_('Manage User Categories'),
|
||||
partial(self.context_menu_handler, action='manage_categories',
|
||||
category=category))
|
||||
@ -426,10 +425,10 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
for k in tb_cats.keys():
|
||||
if tb_cats[k]['kind'] in ['user', 'search']:
|
||||
del tb_cats[k]
|
||||
for user_cat in sorted(prefs['user_categories'].keys()):
|
||||
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys()):
|
||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||
if len(saved_searches.names()):
|
||||
if len(saved_searches().names()):
|
||||
tb_cats.add_search_category(label='search', name=_('Searches'))
|
||||
|
||||
# Now get the categories
|
||||
@ -507,11 +506,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if key not in self.db.field_metadata:
|
||||
return
|
||||
if key == 'search':
|
||||
if val in saved_searches.names():
|
||||
if val in saved_searches().names():
|
||||
error_dialog(self.tags_view, _('Duplicate search name'),
|
||||
_('The saved search name %s is already used.')%val).exec_()
|
||||
return False
|
||||
saved_searches.rename(unicode(item.data(role).toString()), val)
|
||||
saved_searches().rename(unicode(item.data(role).toString()), val)
|
||||
self.tags_view.search_item_renamed.emit()
|
||||
else:
|
||||
if key == 'series':
|
||||
|
@ -199,7 +199,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
UpdateMixin.__init__(self, opts)
|
||||
|
||||
####################### Search boxes ########################
|
||||
SavedSearchBoxMixin.__init__(self)
|
||||
SavedSearchBoxMixin.__init__(self, db)
|
||||
SearchBoxMixin.__init__(self)
|
||||
|
||||
####################### Library view ########################
|
||||
@ -351,7 +351,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
||||
|
||||
|
||||
def do_config(self, *args):
|
||||
def do_config(self, checked=False, initial_category='general'):
|
||||
if self.job_manager.has_jobs():
|
||||
d = error_dialog(self, _('Cannot configure'),
|
||||
_('Cannot configure while there are running jobs.'))
|
||||
@ -363,7 +363,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
d.exec_()
|
||||
return
|
||||
d = ConfigDialog(self, self.library_view,
|
||||
server=self.content_server)
|
||||
server=self.content_server, initial_category=initial_category)
|
||||
|
||||
d.exec_()
|
||||
self.content_server = d.server
|
||||
@ -380,6 +380,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
self.tags_view.recount()
|
||||
self.create_device_menu()
|
||||
self.set_device_menu_items_state(bool(self.device_connected))
|
||||
self.tool_bar.apply_settings()
|
||||
|
||||
def library_moved(self, newloc):
|
||||
if newloc is None: return
|
||||
@ -392,6 +393,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
||||
self.status_bar.clear_message()
|
||||
self.search.clear_to_help()
|
||||
self.saved_search.clear_to_help()
|
||||
self.book_details.reset_info()
|
||||
self.library_view.model().count_changed()
|
||||
self.scheduler.database_changed(db)
|
||||
|
@ -448,7 +448,7 @@ class DocumentView(QWebView):
|
||||
self.unimplemented_actions = list(map(self.pageAction,
|
||||
[d.DownloadImageToDisk, d.OpenLinkInNewWindow, d.DownloadLinkToDisk,
|
||||
d.OpenImageInNewWindow, d.OpenLink]))
|
||||
self.dictionary_action = QAction(QIcon(I('dictionary.png')),
|
||||
self.dictionary_action = QAction(QIcon(I('dictionary.svg')),
|
||||
_('&Lookup in dictionary'), self)
|
||||
self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L)
|
||||
self.dictionary_action.triggered.connect(self.lookup)
|
||||
|
@ -615,7 +615,7 @@ class ResultCache(SearchQueryParser):
|
||||
q = self.search_restriction
|
||||
else:
|
||||
q = query
|
||||
if not ignore_search_restriction:
|
||||
if not ignore_search_restriction and self.search_restriction:
|
||||
q = u'%s (%s)' % (self.search_restriction, query)
|
||||
if not q:
|
||||
if return_matches:
|
||||
|
@ -19,6 +19,7 @@ from calibre.library.schema_upgrades import SchemaUpgrade
|
||||
from calibre.library.caches import ResultCache
|
||||
from calibre.library.custom_columns import CustomColumns
|
||||
from calibre.library.sqlite import connect, IntegrityError, DBThread
|
||||
from calibre.library.prefs import DBPrefs
|
||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
|
||||
MetaInformation
|
||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||
@ -29,7 +30,7 @@ from calibre.customize.ui import run_plugins_on_import
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
|
||||
from calibre.utils.config import prefs, tweaks
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.utils.search_query_parser import saved_searches, set_saved_searches
|
||||
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
||||
from calibre.utils.magick_draw import save_cover_data_to
|
||||
|
||||
@ -140,6 +141,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.initialize_dynamic()
|
||||
|
||||
def initialize_dynamic(self):
|
||||
self.prefs = DBPrefs(self)
|
||||
|
||||
# Migrate saved search and user categories to db preference scheme
|
||||
def migrate_preference(key, default):
|
||||
oldval = prefs[key]
|
||||
if oldval != default:
|
||||
self.prefs[key] = oldval
|
||||
prefs[key] = default
|
||||
if key not in self.prefs:
|
||||
self.prefs[key] = default
|
||||
|
||||
migrate_preference('user_categories', {})
|
||||
migrate_preference('saved_searches', {})
|
||||
set_saved_searches(self, 'saved_searches')
|
||||
|
||||
self.conn.executescript('''
|
||||
DROP TRIGGER IF EXISTS author_insert_trg;
|
||||
CREATE TEMP TRIGGER author_insert_trg
|
||||
@ -268,10 +284,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
for k in tb_cats.keys():
|
||||
if tb_cats[k]['kind'] in ['user', 'search']:
|
||||
del tb_cats[k]
|
||||
for user_cat in sorted(prefs['user_categories'].keys()):
|
||||
for user_cat in sorted(self.prefs.get('user_categories', {}).keys()):
|
||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||
if len(saved_searches.names()):
|
||||
if len(saved_searches().names()):
|
||||
tb_cats.add_search_category(label='search', name=_('Searches'))
|
||||
|
||||
self.book_on_device_func = None
|
||||
@ -311,6 +327,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
''' Return last modified time as a UTC datetime object'''
|
||||
return utcfromtimestamp(os.stat(self.dbpath).st_mtime)
|
||||
|
||||
|
||||
def check_if_modified(self):
|
||||
if self.last_modified() > self.last_update_check:
|
||||
self.refresh()
|
||||
@ -581,6 +598,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
def has_format(self, index, format, index_is_id=False):
|
||||
return self.format_abspath(index, format, index_is_id) is not None
|
||||
|
||||
def format_last_modified(self, id_, fmt):
|
||||
path = self.format_abspath(id_, fmt, index_is_id=True)
|
||||
if path is not None:
|
||||
return utcfromtimestamp(os.stat(path).st_mtime)
|
||||
|
||||
def format_abspath(self, index, format, index_is_id=False):
|
||||
'Return absolute path to the ebook file of format `format`'
|
||||
id = index if index_is_id else self.id(index)
|
||||
@ -843,7 +865,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
categories['formats'].sort(key = lambda x:x.name)
|
||||
|
||||
#### Now do the user-defined categories. ####
|
||||
user_categories = prefs['user_categories']
|
||||
user_categories = self.prefs['user_categories']
|
||||
|
||||
# We want to use same node in the user category as in the source
|
||||
# category. To do that, we need to find the original Tag node. There is
|
||||
@ -880,8 +902,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
icon = None
|
||||
if icon_map and 'search' in icon_map:
|
||||
icon = icon_map['search']
|
||||
for srch in saved_searches.names():
|
||||
items.append(Tag(srch, tooltip=saved_searches.lookup(srch), icon=icon))
|
||||
for srch in saved_searches().names():
|
||||
items.append(Tag(srch, tooltip=saved_searches().lookup(srch), icon=icon))
|
||||
if len(items):
|
||||
if icon_map is not None:
|
||||
icon_map['search'] = icon_map['search']
|
||||
|
49
src/calibre/library/prefs.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/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 json
|
||||
|
||||
from calibre.constants import preferred_encoding
|
||||
from calibre.utils.config import to_json, from_json
|
||||
|
||||
class DBPrefs(dict):
|
||||
|
||||
def __init__(self, db):
|
||||
dict.__init__(self)
|
||||
self.db = db
|
||||
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
|
||||
val = self.raw_to_object(val)
|
||||
dict.__setitem__(self, key, val)
|
||||
|
||||
def raw_to_object(self, raw):
|
||||
if not isinstance(raw, unicode):
|
||||
raw = raw.decode(preferred_encoding)
|
||||
return json.loads(raw, object_hook=from_json)
|
||||
|
||||
def to_raw(self, val):
|
||||
return json.dumps(val, indent=2, default=to_json)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||
self.db.conn.commit()
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
raw = self.to_raw(val)
|
||||
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||
self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key,
|
||||
raw))
|
||||
self.db.conn.commit()
|
||||
dict.__setitem__(self, key, val)
|
||||
|
||||
def set(self, key, val):
|
||||
self.__setitem__(key, val)
|
||||
|
||||
|
@ -387,3 +387,13 @@ class SchemaUpgrade(object):
|
||||
|
||||
self.conn.execute('UPDATE authors SET sort=author_to_author_sort(name)')
|
||||
|
||||
def upgrade_version_12(self):
|
||||
'DB based preference store'
|
||||
script = '''
|
||||
DROP TABLE IF EXISTS preferences;
|
||||
CREATE TABLE preferences(id INTEGER PRIMARY KEY,
|
||||
key TEXT NON NULL,
|
||||
val TEXT NON NULL,
|
||||
UNIQUE(key));
|
||||
'''
|
||||
self.conn.executescript(script)
|
||||
|
@ -349,7 +349,7 @@ table of contents, check the :guilabel:`Do not add detected chapters` option.
|
||||
|
||||
If less than the :guilabel:`Chapter threshold` number of chapters were detected, |app| will then add any hyperlinks
|
||||
it finds in the input document to the Table of Contents. This often works well many input documents include a
|
||||
hyperlinked Table of Contents right at the start. The :guilabel:`Number fo links` option can be used to control
|
||||
hyperlinked Table of Contents right at the start. The :guilabel:`Number of links` option can be used to control
|
||||
this behavior. If set to zero, no links are added. If set to a number greater than zero, at most that number of links
|
||||
is added.
|
||||
|
||||
|
@ -9,7 +9,8 @@ Customizing |app|
|
||||
|
||||
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
|
||||
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
|
||||
first, how to use environment variables and *tweaks* to customize |app|'s behavior and then how to
|
||||
first, how to use environment variables and *tweaks* to customize |app|'s behavior, and then how to
|
||||
specify your own static resources like icons and templates to override the defaults and finally how to
|
||||
use *plugins* to add funtionality to |app|.
|
||||
|
||||
.. contents::
|
||||
@ -35,6 +36,20 @@ The default tweaks.py file is reproduced below
|
||||
.. literalinclude:: ../../../resources/default_tweaks.py
|
||||
|
||||
|
||||
Overriding icons, templates, etcetera
|
||||
----------------------------------------
|
||||
|
||||
|app| allows you to override the static resources, like icons, templates, javascript, etc. with customized versions that you like.
|
||||
All static resources are stored in the resources sub-folder of the calibre install location. On Windows, this is usually
|
||||
:file:`C:\Program Files\Calibre2\resources`. On OS X, :file:`/Applications/calibre.app/Contents/Resources/resources/`. On linux, if you are using the binary installer
|
||||
from the calibre website it will be :file:`/opt/calibre/resources`. These paths can change depending on where you choose to install |app|.
|
||||
|
||||
You should not change the files in this resources folder, as your changes will get overwritten the next time you update |app|. Instead, go to
|
||||
:guilabel:`Preferences->Advanced` and click :guilabel:`Open calibre configuration directory`. In this configuration directory, create a sub-folder called resources and place the files you want to override in it. |app| will automatically use your custom file in preference to the builtin one the next time it is started.
|
||||
|
||||
For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is
|
||||
:file:`resources/images/trash.svg`. Assuming you have an alternate icon in svg format called :file:`mytrash.svg` you would save it in the configuration directory as :file:`resources/images/trash.svg`. All the icons used by the calibre user interface are in :file:`resources/images` and its sub-folders.
|
||||
|
||||
A Hello World plugin
|
||||
------------------------
|
||||
|
||||
|
@ -281,7 +281,7 @@ Why doesn't |app| have a column for foo?
|
||||
|
||||
How do I move my |app| library from one computer to another?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking Preferences. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder.
|
||||
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking the calibre icon in the toolbar. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder.
|
||||
|
||||
Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also go to Preferences->Advanced and click the Check database integrity button. It will warn you about missing files, if any, which you should then transfer by hand.
|
||||
|
||||
|
@ -66,7 +66,7 @@ and save it to a new file.
|
||||
|
||||
|
||||
"""
|
||||
import ctypes, sys, os
|
||||
import ctypes, sys, os, glob
|
||||
from ctypes import util
|
||||
iswindows = 'win32' in sys.platform or 'win64' in sys.platform
|
||||
isosx = 'darwin' in sys.platform
|
||||
@ -85,7 +85,8 @@ elif iswindows:
|
||||
_lib = flib if isfrozen else 'CORE_RL_wand_'
|
||||
else:
|
||||
if isfrozen:
|
||||
_lib = os.path.join(sys.frozen_path, 'libMagickWand.so.2')
|
||||
_lib = glob.glob(os.path.join(sys.frozen_path,
|
||||
'libMagickWand.so.*'))[-1]
|
||||
else:
|
||||
_lib = util.find_library('MagickWand')
|
||||
if _lib is None:
|
||||
|
@ -6,30 +6,16 @@ __docformat__ = 'restructuredtext en'
|
||||
'''
|
||||
Manage application-wide preferences.
|
||||
'''
|
||||
import os, re, cPickle, textwrap, traceback, plistlib, json, base64
|
||||
import os, re, cPickle, textwrap, traceback, plistlib, json, base64, datetime
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from optparse import OptionParser as _OptionParser
|
||||
from optparse import IndentedHelpFormatter
|
||||
from calibre.constants import terminal_controller, iswindows, isosx, \
|
||||
__appname__, __version__, __author__, plugins
|
||||
from calibre.utils.lock import LockError, ExclusiveFile
|
||||
from collections import defaultdict
|
||||
|
||||
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
|
||||
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
|
||||
elif iswindows:
|
||||
if plugins['winutil'][0] is None:
|
||||
raise Exception(plugins['winutil'][1])
|
||||
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
|
||||
if not os.access(config_dir, os.W_OK|os.X_OK):
|
||||
config_dir = os.path.expanduser('~')
|
||||
config_dir = os.path.join(config_dir, 'calibre')
|
||||
elif isosx:
|
||||
config_dir = os.path.expanduser('~/Library/Preferences/calibre')
|
||||
else:
|
||||
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
|
||||
config_dir = os.path.join(bdir, 'calibre')
|
||||
from calibre.constants import terminal_controller, config_dir, \
|
||||
__appname__, __version__, __author__
|
||||
from calibre.utils.lock import LockError, ExclusiveFile
|
||||
|
||||
plugin_dir = os.path.join(config_dir, 'plugins')
|
||||
|
||||
@ -632,27 +618,34 @@ class XMLConfig(dict):
|
||||
f.truncate()
|
||||
f.write(raw)
|
||||
|
||||
def to_json(obj):
|
||||
if isinstance(obj, bytearray):
|
||||
return {'__class__': 'bytearray',
|
||||
'__value__': base64.standard_b64encode(bytes(obj))}
|
||||
if isinstance(obj, datetime.datetime):
|
||||
from calibre.utils.date import isoformat
|
||||
return {'__class__': 'datetime.datetime',
|
||||
'__value__': isoformat(obj, as_utc=True)}
|
||||
raise TypeError(repr(obj) + ' is not JSON serializable')
|
||||
|
||||
def from_json(obj):
|
||||
if '__class__' in obj:
|
||||
if obj['__class__'] == 'bytearray':
|
||||
return bytearray(base64.standard_b64decode(obj['__value__']))
|
||||
if obj['__class__'] == 'datetime.datetime':
|
||||
from calibre.utils.date import parse_date
|
||||
return parse_date(obj['__value__'], assume_utc=True)
|
||||
return obj
|
||||
|
||||
class JSONConfig(XMLConfig):
|
||||
|
||||
EXTENSION = '.json'
|
||||
|
||||
def to_json(self, obj):
|
||||
if isinstance(obj, bytearray):
|
||||
return {'__class__': 'bytearray',
|
||||
'__value__': base64.standard_b64encode(bytes(obj))}
|
||||
raise TypeError(repr(obj) + ' is not JSON serializable')
|
||||
|
||||
def from_json(self, obj):
|
||||
if '__class__' in obj:
|
||||
if obj['__class__'] == 'bytearray':
|
||||
return bytearray(base64.standard_b64decode(obj['__value__']))
|
||||
return obj
|
||||
|
||||
def raw_to_object(self, raw):
|
||||
return json.loads(raw.decode('utf-8'), object_hook=self.from_json)
|
||||
return json.loads(raw.decode('utf-8'), object_hook=from_json)
|
||||
|
||||
def to_raw(self):
|
||||
return json.dumps(self, indent=2, default=self.to_json)
|
||||
return json.dumps(self, indent=2, default=to_json)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, key)
|
||||
|
@ -42,6 +42,8 @@ def get_lang():
|
||||
lang = match.group()
|
||||
if lang == 'zh':
|
||||
lang = 'zh_CN'
|
||||
if lang is None:
|
||||
lang = 'en'
|
||||
return lang
|
||||
|
||||
def messages_path(lang):
|
||||
|
@ -9,26 +9,54 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import __builtin__, sys, os
|
||||
|
||||
_dev_path = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
||||
if _dev_path is not None:
|
||||
_dev_path = os.path.join(os.path.abspath(os.path.dirname(_dev_path)), 'resources')
|
||||
if not os.path.exists(_dev_path):
|
||||
_dev_path = None
|
||||
from calibre import config_dir
|
||||
|
||||
_path_cache = {}
|
||||
class PathResolver(object):
|
||||
|
||||
def __init__(self):
|
||||
self.locations = [sys.resources_location]
|
||||
self.cache = {}
|
||||
|
||||
def suitable(path):
|
||||
try:
|
||||
return os.path.exists(path) and os.path.isdir(path) and \
|
||||
os.listdir(path)
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
dev_path = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
||||
if dev_path is not None:
|
||||
dev_path = os.path.join(os.path.abspath(
|
||||
os.path.dirname(dev_path)), 'resources')
|
||||
if suitable(dev_path):
|
||||
self.locations.insert(0, dev_path)
|
||||
|
||||
user_path = os.path.join(config_dir, 'resources')
|
||||
if suitable(user_path):
|
||||
self.locations.insert(0, user_path)
|
||||
|
||||
def __call__(self, path):
|
||||
path = path.replace(os.sep, '/')
|
||||
ans = self.cache.get(path, None)
|
||||
if ans is None:
|
||||
for base in self.locations:
|
||||
fpath = os.path.join(base, *path.split('/'))
|
||||
if os.path.exists(fpath):
|
||||
ans = fpath
|
||||
break
|
||||
|
||||
if ans is None:
|
||||
ans = os.path.join(self.locations[0], *path.split('/'))
|
||||
|
||||
self.cache[path] = ans
|
||||
|
||||
return ans
|
||||
|
||||
_resolver = PathResolver()
|
||||
|
||||
def get_path(path, data=False):
|
||||
global _dev_path
|
||||
path = path.replace(os.sep, '/')
|
||||
base = sys.resources_location
|
||||
if _dev_path is not None:
|
||||
if path in _path_cache:
|
||||
return _path_cache[path]
|
||||
if os.path.exists(os.path.join(_dev_path, *path.split('/'))):
|
||||
base = _dev_path
|
||||
fpath = os.path.join(base, *path.split('/'))
|
||||
if _dev_path is not None:
|
||||
_path_cache[path] = fpath
|
||||
fpath = _resolver(path)
|
||||
if data:
|
||||
return open(fpath, 'rb').read()
|
||||
return fpath
|
||||
|
@ -18,10 +18,11 @@ If this module is run, it will perform a series of unit tests.
|
||||
|
||||
import sys, string, operator
|
||||
|
||||
from calibre.utils.pyparsing import Keyword, Group, Forward, CharsNotIn, Suppress, \
|
||||
OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException
|
||||
from calibre.utils.pyparsing import CaselessKeyword, Group, Forward, CharsNotIn, Suppress, \
|
||||
OneOrMore, MatchFirst, CaselessLiteral, Optional, NoMatch, ParseException
|
||||
from calibre.constants import preferred_encoding
|
||||
from calibre.utils.config import prefs
|
||||
|
||||
|
||||
|
||||
'''
|
||||
This class manages access to the preference holding the saved search queries.
|
||||
@ -32,9 +33,13 @@ class SavedSearchQueries(object):
|
||||
queries = {}
|
||||
opt_name = ''
|
||||
|
||||
def __init__(self, _opt_name):
|
||||
def __init__(self, db, _opt_name):
|
||||
self.opt_name = _opt_name;
|
||||
self.queries = prefs[self.opt_name]
|
||||
self.db = db
|
||||
if db is not None:
|
||||
self.queries = db.prefs.get(self.opt_name, {})
|
||||
else:
|
||||
self.queries = {}
|
||||
|
||||
def force_unicode(self, x):
|
||||
if not isinstance(x, unicode):
|
||||
@ -43,20 +48,20 @@ class SavedSearchQueries(object):
|
||||
|
||||
def add(self, name, value):
|
||||
self.queries[self.force_unicode(name)] = self.force_unicode(value).strip()
|
||||
prefs[self.opt_name] = self.queries
|
||||
self.db.prefs[self.opt_name] = self.queries
|
||||
|
||||
def lookup(self, name):
|
||||
return self.queries.get(self.force_unicode(name), None)
|
||||
|
||||
def delete(self, name):
|
||||
self.queries.pop(self.force_unicode(name), False)
|
||||
prefs[self.opt_name] = self.queries
|
||||
self.db.prefs[self.opt_name] = self.queries
|
||||
|
||||
def rename(self, old_name, new_name):
|
||||
self.queries[self.force_unicode(new_name)] = \
|
||||
self.queries.get(self.force_unicode(old_name), None)
|
||||
self.queries.pop(self.force_unicode(old_name), False)
|
||||
prefs[self.opt_name] = self.queries
|
||||
self.db.prefs[self.opt_name] = self.queries
|
||||
|
||||
def names(self):
|
||||
return sorted(self.queries.keys(),
|
||||
@ -66,8 +71,15 @@ class SavedSearchQueries(object):
|
||||
Create a global instance of the saved searches. It is global so that the searches
|
||||
are common across all instances of the parser (devices, library, etc).
|
||||
'''
|
||||
saved_searches = SavedSearchQueries('saved_searches')
|
||||
ss = SavedSearchQueries(None, None)
|
||||
|
||||
def set_saved_searches(db, opt_name):
|
||||
global ss
|
||||
ss = SavedSearchQueries(db, opt_name)
|
||||
|
||||
def saved_searches():
|
||||
global ss
|
||||
return ss
|
||||
|
||||
class SearchQueryParser(object):
|
||||
'''
|
||||
@ -139,18 +151,19 @@ class SearchQueryParser(object):
|
||||
|
||||
Not = Forward()
|
||||
Not << (Group(
|
||||
Suppress(Keyword("not", caseless=True)) + Not
|
||||
Suppress(CaselessKeyword("not")) + Not
|
||||
).setResultsName("not") | Parenthesis)
|
||||
|
||||
And = Forward()
|
||||
And << (Group(
|
||||
Not + Suppress(Keyword("and", caseless=True)) + And
|
||||
Not + Suppress(CaselessKeyword("and")) + And
|
||||
).setResultsName("and") | Group(
|
||||
Not + OneOrMore(~oneOf("and or", caseless=True) + And)
|
||||
Not + OneOrMore(~MatchFirst(list(map(CaselessKeyword,
|
||||
('and', 'or')))) + And)
|
||||
).setResultsName("and") | Not)
|
||||
|
||||
Or << (Group(
|
||||
And + Suppress(Keyword("or", caseless=True)) + Or
|
||||
And + Suppress(CaselessKeyword("or")) + Or
|
||||
).setResultsName("or") | And)
|
||||
|
||||
if test:
|
||||
@ -158,8 +171,6 @@ class SearchQueryParser(object):
|
||||
self._tests_failed = bool(failed)
|
||||
|
||||
self._parser = Or
|
||||
#self._parser.setDebug(True)
|
||||
#self.parse('(tolstoy)')
|
||||
self._parser.setDebug(False)
|
||||
|
||||
|
||||
@ -209,7 +220,7 @@ class SearchQueryParser(object):
|
||||
raise ParseException(query, len(query), 'undefined saved search', self)
|
||||
if self.recurse_level > 5:
|
||||
self.searches_seen.add(query)
|
||||
return self._parse(saved_searches.lookup(query))
|
||||
return self._parse(saved_searches().lookup(query))
|
||||
except: # convert all exceptions (e.g., missing key) to a parse error
|
||||
raise ParseException(query, len(query), 'undefined saved search', self)
|
||||
return self.get_matches(location, query)
|
||||
@ -283,7 +294,7 @@ class Tester(SearchQueryParser):
|
||||
28: [u"Kushiel's Scion", u'Jacqueline Carey', None, u'lrf,rar'],
|
||||
29: [u'Underworld', u'Don DeLillo', None, u'lrf,rar'],
|
||||
30: [u'Genghis Khan and The Making of the Modern World',
|
||||
u'Jack Weatherford',
|
||||
u'Jack Weatherford Orc',
|
||||
u'Three Rivers Press',
|
||||
u'lrf,zip'],
|
||||
31: [u'The Best and the Brightest',
|
||||
@ -535,6 +546,7 @@ class Tester(SearchQueryParser):
|
||||
'london:thames': set([13]),
|
||||
'publisher:london:thames': set([13]),
|
||||
'"(1977)"': set([13]),
|
||||
'jack weatherford orc': set([30]),
|
||||
}
|
||||
fields = {'title':0, 'author':1, 'publisher':2, 'tag':3}
|
||||
|
||||
@ -574,7 +586,10 @@ class Tester(SearchQueryParser):
|
||||
|
||||
|
||||
def main(args=sys.argv):
|
||||
tester = Tester(test=True)
|
||||
tester = Tester(['authors', 'author', 'series', 'formats', 'format',
|
||||
'publisher', 'rating', 'tags', 'tag', 'comments', 'comment', 'cover',
|
||||
'isbn', 'ondevice', 'pubdate', 'size', 'date', 'title', u'#read',
|
||||
'all', 'search'], test=True)
|
||||
failed = tester.run_tests()
|
||||
if tester._tests_failed or failed:
|
||||
print '>>>>>>>>>>>>>> Tests Failed <<<<<<<<<<<<<<<'
|
||||
|