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 @@
|
|||
'''
|
||||
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 @@
|
|||
|
||||
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 @@
|
|||
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 @@
|
|||
__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 @@
|
|||
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 @@ def load_plugins():
|
|||
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 @@ def _update_iTunes_metadata(self, metadata, db_added, lb_added, this_book):
|
|||
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 @@
|
|||
__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 @@ def _visit_node(self, node, chapters, depth):
|
|||
|
||||
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 @@ def smart_update(self, mi, replace_metadata=False):
|
|||
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 @@ def smart_update(self, mi, replace_metadata=False):
|
|||
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 @@
|
|||
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 @@ def force_int(raw):
|
|||
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 @@ def delete_books(self, *args):
|
|||
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 @@ def merge_metadata(self, dest_id, src_ids):
|
|||
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 @@ def merge_metadata(self, dest_id, src_ids):
|
|||
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 @@ def merge_metadata(self, dest_id, src_ids):
|
|||
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 @@ def run(self):
|
|||
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 @@ def __repr__(self):
|
|||
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 @@ def __init__(self, parent=None):
|
|||
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 @@ def __init__(self, parent=None):
|
|||
]
|
||||
|
||||
|
||||
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 @@ def __init__(self, parent=None):
|
|||
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 @@ def __init__(self):
|
|||
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 @@ def _sync_action_triggered(self, *args):
|
|||
|
||||
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 @@ def device_job_exception(self, job):
|
|||
|
||||
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 @@ def emails_sent(self, results, remove=[]):
|
|||
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 @@
|
|||
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 @@ def check_action(self, ac, loc):
|
|||
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 @@
|
|||
__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 @@ def __init__(self, parent, text):
|
|||
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 @@ def data(self, index, role):
|
|||
|
||||
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 @@ def __init__(self, parent, library_view, server=None):
|
|||
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 @@ def __init__(self, parent, library_view, server=None):
|
|||
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 @@ def accept(self):
|
|||
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 @@ def check_done(self, bad, err):
|
|||
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 @@ def __init__(self, parent, editing, standard_colheads, standard_colnames):
|
|||
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 @@ def __init__(self, parent, db, id_to_select):
|
|||
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.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 @@ def run(self):
|
|||
|
||||
|
||||
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 @@ def _add_formats(self, paths):
|
|||
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 @@ def __init__(self, window, row, db, accepted_callback=None, cancel_all=False):
|
|||
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 @@ def __init__(self, window, initial_search=None):
|
|||
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 @@ def accept(self):
|
|||
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 @@ def break_cycles(self):
|
|||
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.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 @@ def __init__(self, window, db, on_category=None):
|
|||
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 @@ def __init__(self, window, db, on_category=None):
|
|||
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 @@ def select_category(self, idx):
|
|||
|
||||
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 @@ def __init__(self, window, tag_to_match, data, compare):
|
|||
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 @@ def __init__(self, db):
|
|||
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 @@ def __init__(self, db):
|
|||
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 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 @@ def __init__(self, actions, donate, location_manager, parent=None):
|
|||
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 @@ def __init__(self, actions, donate, location_manager, parent=None):
|
|||
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 @@ def setup_tool_button(ac):
|
|||
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 @@ def count_changed(self, new_count):
|
|||
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 @@ def database_changed(self, db):
|
|||
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 @@ def __init__(self, db):
|
|||
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 @@ def init_scheduler(self, db):
|
|||
self.scheduler.start_recipe_fetch.connect(
|
||||
self.download_scheduled_recipe, type=Qt.QueuedConnection)
|
||||
|
||||
|
||||
def read_toolbar_settings(self):
|
||||
pass
|
||||
|
||||
|
|
@ -372,18 +443,19 @@ def ac(normal_order, device_order, separator_before,
|
|||
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 @@ def ac(normal_order, device_order, separator_before,
|
|||
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 @@ def ac(normal_order, device_order, separator_before,
|
|||
for x in (self.preferences_action, self.action_preferences):
|
||||
x.triggered.connect(self.do_config)
|
||||
|
||||
|
||||
return all_actions
|
||||
# }}}
|
||||
|
||||
|
|
|
|||
|
|
@ -214,13 +214,17 @@ def get_state(self):
|
|||
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 @@ def get_default_state(self):
|
|||
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 @@ def database_changed(self, db):
|
|||
|
||||
# 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 @@ def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
|||
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 @@ def scroll_to_row(self, row):
|
|||
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 @@ def contextMenuEvent(self, event):
|
|||
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 @@
|
|||
|
||||
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 @@ def __init__(self, parent=None):
|
|||
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 @@ def clear_to_help(self):
|
|||
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 @@ def key_pressed(self, event):
|
|||
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 @@ def set_search_string(self, txt):
|
|||
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 @@ def __init__(self, parent=None):
|
|||
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 @@ def saved_search_selected(self, qname):
|
|||
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 @@ def delete_search_button_clicked(self):
|
|||
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 @@ def save_search_button_clicked(self):
|
|||
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 @@ def copy_search_button_clicked (self):
|
|||
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 @@ def do_advanced_search(self, *args):
|
|||
|
||||
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 @@ def __init__(self):
|
|||
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 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 @@ def show_context_menu(self, point):
|
|||
|
||||
# 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 @@ def get_node_tree(self, sort):
|
|||
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 @@ def setData(self, index, value, role=Qt.EditRole):
|
|||
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 @@ def initialize(self, library_path, db, listener, actions):
|
|||
UpdateMixin.__init__(self, opts)
|
||||
|
||||
####################### Search boxes ########################
|
||||
SavedSearchBoxMixin.__init__(self)
|
||||
SavedSearchBoxMixin.__init__(self, db)
|
||||
SearchBoxMixin.__init__(self)
|
||||
|
||||
####################### Library view ########################
|
||||
|
|
@ -351,7 +351,7 @@ def booklists(self):
|
|||
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 @@ def do_config(self, *args):
|
|||
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 @@ def do_config(self, *args):
|
|||
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 @@ def library_moved(self, newloc):
|
|||
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 @@ def __init__(self, *args):
|
|||
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 @@ def search(self, query, return_matches=False,
|
|||
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.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.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 @@ def __init__(self, library_path, row_factory=False):
|
|||
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 @@ def initialize_dynamic(self):
|
|||
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 @@ def last_modified(self):
|
|||
''' 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 @@ def formats(self, index, index_is_id=False):
|
|||
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 @@ def get_categories(self, sort='name', ids=None, icon_map=None):
|
|||
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 @@ def get_categories(self, sort='name', ids=None, icon_map=None):
|
|||
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 @@ def create_cust_tag_browser_view(table_name, link_table_name):
|
|||
|
||||
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 @@
|
|||
|
||||
|
||||
"""
|
||||
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 @@
|
|||
_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 @@
|
|||
'''
|
||||
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 @@ def commit(self):
|
|||
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 @@
|
|||
|
||||
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 @@
|
|||
|
||||
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 @@ def force_unicode(self, x):
|
|||
|
||||
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 @@ def names(self):
|
|||
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 @@ def __init__(self, locations, test=False):
|
|||
|
||||
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 @@ def __init__(self, locations, test=False):
|
|||
self._tests_failed = bool(failed)
|
||||
|
||||
self._parser = Or
|
||||
#self._parser.setDebug(True)
|
||||
#self.parse('(tolstoy)')
|
||||
self._parser.setDebug(False)
|
||||
|
||||
|
||||
|
|
@ -209,7 +220,7 @@ def evaluate_token(self, argument):
|
|||
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 @@ def run_tests(self):
|
|||
|
||||
|
||||
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 <<<<<<<<<<<<<<<'
|
||||
|
|
|
|||