merged 07.17 changes

This commit is contained in:
ldolse 2010-09-04 15:20:03 +10:00
commit 572633bac1
85 changed files with 32301 additions and 23268 deletions

View File

@ -4,6 +4,63 @@
# for important features/bug fixes. # for important features/bug fixes.
# Also, each release can have new and improved recipes. # Also, each release can have new and improved recipes.
- version: 0.7.17
date: 2010-09-03
new features:
- title: "Content server: Show custom column data in the book listing"
- title: "Add preference to automatically set a tag when adding books (Preferences->General)"
- title: "Add a tweak to create compound search terms. Show error message in tooltip when user inputs an invalid search query."
- title: "Managing multiple libraries: Allow renaming/deleting libraries from the Choose library menu"
- title: "Searching on series index is now possible. See the User Manual for details."
bug fixes:
- title: "Fix regression in 0.7.16 that broke conversion of HTML files with preprocess turned on"
- title: "MOBI Output: When converting an input document that specifies an inline TOC in the <guide> but not in the <spine>, add it correctly. Fixes #6661 (Conversion to MOBI fails to create TOC)"
tickets: [6661]
- title: "JetBook driver: Only use JetBook naming scheme for txt, pdf and fb2 files."
tickets: [6638]
- title: "Copy to library action now respects merge preferences"
tickets: [6641]
- title: "Fix bug in email sending when using an SSL connection"
- title: "Kobo driver: Fix bug that prevented metadata caching from working correctly"
tickets: [6015]
- title: "Fix regression in 0.7.16 that caused calibre to forget its preferences on each restart for new installs on linux"
- title: "News downloads: Cut off long downloaded from URLs"
tickets: [6649]
new recipes:
- title: "HOY"
author: Fco Javier Nieto
- title: "Milenio"
author: bmsleight
- title: "Winnipeg Free Press"
author: buyo
- title: "Field and stream blog, West Hawaii Today, Marietta Daily Journal"
author: Tony Stegall
- title: "Europa Sur"
author: "Darko Miletic"
improved recipes:
- La Jornada
- Slate
- version: 0.7.16 - version: 0.7.16
date: 2010-08-27 date: 2010-08-27

View File

@ -50,7 +50,7 @@ function render_book(book) {
var comments = $.trim(book.text()).replace(/\n\n/, '<br/>'); var comments = $.trim(book.text()).replace(/\n\n/, '<br/>');
var formats = new Array(); var formats = new Array();
var size = (parseFloat(book.attr('size'))/(1024*1024)).toFixed(1); var size = (parseFloat(book.attr('size'))/(1024*1024)).toFixed(1);
var tags = book.attr('tags').replace(/,/g, ', '); var tags = book.attr('tags')
formats = book.attr("formats").split(","); formats = book.attr("formats").split(",");
if (formats.length > 0) { if (formats.length > 0) {
for (i=0; i < formats.length; i++) { for (i=0; i < formats.length; i++) {
@ -59,7 +59,14 @@ function render_book(book) {
title = title.slice(0, title.length-2); title = title.slice(0, title.length-2);
title += '&nbsp;({0}&nbsp;MB)&nbsp;'.format(size); title += '&nbsp;({0}&nbsp;MB)&nbsp;'.format(size);
} }
if (tags) title += '[{0}]'.format(tags); if (tags) title += 'Tags=[{0}] '.format(tags);
custcols = book.attr("custcols").split(',')
for ( i = 0; i < custcols.length; i++) {
if (custcols[i].length > 0) {
vals = book.attr(custcols[i]).split(':#:', 2);
title += '{0}=[{1}] '.format(vals[0], vals[1]);
}
}
title += '<img style="display:none" alt="" src="get/cover/{0}" /></span>'.format(id); title += '<img style="display:none" alt="" src="get/cover/{0}" /></span>'.format(id);
title += '<div class="comments">{0}</div>'.format(comments) title += '<div class="comments">{0}</div>'.format(comments)
// Render authors cell // Render authors cell

View File

@ -91,3 +91,26 @@ save_template_title_series_sorting = 'library_order'
# auto_connect_to_folder = 'C:\\Users\\someone\\Desktop\\testlib' # auto_connect_to_folder = 'C:\\Users\\someone\\Desktop\\testlib'
# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library' # auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
auto_connect_to_folder = '' auto_connect_to_folder = ''
# Create search terms to apply a query across several built-in search terms.
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
# Example: create the term 'myseries' that when used as myseries:foo would
# search all of the search categories 'series', '#myseries', and '#myseries2':
# grouped_search_terms={'myseries':['series','#myseries', '#myseries2']}
# Example: two search terms 'a' and 'b' both that search 'tags' and '#mytags':
# grouped_search_terms={'a':['tags','#mytags'], 'b':['tags','#mytags']}
# Note: You cannot create a search term that is a duplicate of an existing term.
# Such duplicates will be silently ignored. Also note that search terms ignore
# case. 'MySearch' and 'mysearch' are the same term.
grouped_search_terms = {}
# Set this to True (not 'True') to ensure that tags in 'Tags to add when adding
# a book' are added when copying books to another library
add_new_book_tags_when_importing_books = False
# Set the maximum number of tags to show per book in the content server
max_content_server_tags_shown=5

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

View File

@ -18,7 +18,7 @@ class Clarin(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
no_stylesheets = True no_stylesheets = True
encoding = 'utf8' encoding = 'utf8'
language = 'es_AR' language = 'es'
publication_type = 'newspaper' publication_type = 'newspaper'
INDEX = 'http://www.clarin.com' INDEX = 'http://www.clarin.com'
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg' masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'

View File

@ -20,7 +20,7 @@ class Europasur(BasicNewsRecipe):
delay = 2 delay = 2
no_stylesheets = True no_stylesheets = True
encoding = 'cp1252' encoding = 'cp1252'
language = 'es_ES' language = 'es'
publication_type = 'newspaper' publication_type = 'newspaper'
extra_css = """ body{font-family: Verdana,Arial,Helvetica,sans-serif} extra_css = """ body{font-family: Verdana,Arial,Helvetica,sans-serif}
h2{font-family: Georgia,Times New Roman,Times,serif} h2{font-family: Georgia,Times New Roman,Times,serif}

View File

@ -0,0 +1,64 @@
from calibre.web.feeds.news import BasicNewsRecipe
class FIELDSTREAM(BasicNewsRecipe):
title = 'Field and Stream'
__author__ = 'Starson17 and Tonythebookworm'
description = 'Hunting and Fishing and Gun Talk'
language = 'en'
no_stylesheets = True
publisher = 'Starson17 and Tonythebookworm'
category = 'food recipes, hunting, fishing, guns'
use_embedded_content= False
no_stylesheets = True
oldest_article = 24
remove_javascript = True
remove_empty_feeds = True
masthead_url = 'http://www.fieldandstream.com/sites/all/themes/fs/logo.png'
cover_url = 'http://www.arrowheadflyangler.com/Portals/1/Articles/FieldStream/Field%20and%20Stream%20March%20Fishing%20Edition%20Article%20Cover.jpg'
# recursions = 0
max_articles_per_feed = 10
INDEX = 'http://www.fieldandstream.com'
keep_only_tags = [dict(name='div', attrs={'class':['interior-main']})
]
remove_tags = [dict(name='div', attrs={'id':['comments']})]
def parse_index(self):
feeds = []
for title, url in [
(u"Wild Chef", u"http://www.fieldandstream.com/blogs/wild-chef"),
(u"The Gun Nut", u"http://www.fieldandstream.com/blogs/gun-nut"),
(u"Whitetail 365", u"http://www.fieldandstream.com/blogs/whitetail-365"),
(u"Fly Talk", u"http://www.fieldandstream.com/blogs/flytalk"),
(u"Generation Wild", u"http://www.fieldandstream.com/blogs/generation-wild"),
(u"Conservationist", u"http://www.fieldandstream.com/blogs/conservationist"),
(u"Honest Angler", u"http://www.fieldandstream.com/blogs/honest-angler"),
(u"Mans Best Friend", u"http://www.fieldandstream.com/blogs/mans-best-friend"),
]:
articles = self.make_links(url)
if articles:
feeds.append((title, articles))
return feeds
def make_links(self, url):
title = 'Temp'
current_articles = []
soup = self.index_to_soup(url)
print 'The soup is: ', soup
for item in soup.findAll('h2'):
print 'item is: ', item
link = item.find('a')
print 'the link is: ', link
if link:
url = self.INDEX + link['href']
title = self.tag_to_string(link)
print 'the title is: ', title
print 'the url is: ', url
print 'the title is: ', title
current_articles.append({'title': title, 'url': url, 'description':'', 'date':''}) # append all this
return current_articles

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2010, Francisco Javier Nieto <frjanibo at gmail.com>'
'''
www.hoy.es
'''
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
class Hoy(BasicNewsRecipe):
title = 'HOY'
__author__ = 'Fco Javier Nieto'
description = u'Noticias desde Extremadura'
publisher = 'HOY'
category = 'news, politics, Spain, Extremadura'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
delay = 1
encoding = 'cp1252'
language = 'es'
feeds = [
(u'Portada' , u'http://www.hoy.es/portada.xml' ),
(u'Regional' , u'http://www.hoy.es/rss/feeds/regional.xml' ),
(u'Prov de Badajoz' , u'http://www.hoy.es/rss/feeds/prov_badajoz.xml' ),
(u'Prov de Caceres' , u'http://www.hoy.es/rss/feeds/prov_caceres.xml' ),
(u'Badajoz' , u'http://www.hoy.es/rss/feeds/badajoz.xml' ),
(u'Caceres' , u'http://www.hoy.es/rss/feeds/caceres.xml' ),
(u'Merida' , u'http://www.hoy.es/rss/feeds/merida.xml' ),
(u'Opinion' , u'http://www.hoy.es/rss/feeds/opinion.xml' ),
(u'Nacional' , u'http://www.hoy.es/rss/feeds/nacional.xml' ),
(u'Internacional' , u'http://www.hoy.es/rss/feeds/internacional.xml' ),
(u'Economia' , u'http://www.hoy.es/rss/feeds/economia.xml' ),
(u'Deportes' , u'http://www.hoy.es/rss/feeds/deportes.xml' ),
(u'Sociedad' , u'http://www.hoy.es/rss/feeds/sociedad.xml' ),
(u'Cultura' , u'http://www.hoy.es/rss/feeds/cultura.xml' ),
(u'Television' , u'http://www.hoy.es/rss/feeds/television.xml' ),
(u'contraportada' , u'http://www.hoy.es/rss/feeds/contraportada.xml' )
]
keep_only_tags = [
dict(name='h1', attrs={'class':['headline']}),
dict(name='h2', attrs={'class':['subhead']}),
dict(name='div', attrs={'class':['text']})
]
remove_tags = [
dict(name=['object','link','script'])
,dict(name='div', attrs={'class':['colC_articulo','peu']})
]
remove_tags_after = [dict(name='div', attrs={'class':'text'})]
extra_css = '.headline {font: sans-serif 2em;}\n.subhead,h2{font: sans-serif 1.5em\n'
def preprocess_html(self, soup):
soup.html['dir' ] = self.direction
mcharset = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset=utf-8")])
soup.head.insert(0,mcharset)
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -1,120 +1,64 @@
#!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Rogelio Dominguez <rogelio.dominguez at gmail.com>' __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
www.jornada.unam.mx www.jornada.unam.mx
''' '''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
import re class LaJornada_mx(BasicNewsRecipe):
title = 'La Jornada (Mexico)'
class LaJornada(BasicNewsRecipe): __author__ = 'Darko Miletic'
title = u'La Jornada' description = 'Noticias del diario mexicano La Jornada'
language = 'es' publisher = 'DEMOS, Desarrollo de Medios, S.A. de C.V.'
oldest_article = 1 category = 'news, Mexico'
__author__ = 'rogeliodh' oldest_article = 2
max_articles_per_feed = 100 max_articles_per_feed = 200
remove_tags = [dict(name='div', attrs={'class':['go gui','go gui top','comment-cont',]})]
remove_tags_before = dict(id='article-cont')
remove_tags_after = dict(id='article-cont')
no_stylesheets = True no_stylesheets = True
extra_css = ' .series{ \ encoding = 'utf8'
border-bottom: 1px solid #626366; \ use_embedded_content = False
font-weight: bold; \ language = 'es'
} \ remove_empty_feeds = True
.sumario{ \ cover_url = strftime("http://www.jornada.unam.mx/%Y/%m/%d/planitas/portadita.jpg")
font-weight: bold; \ masthead_url = 'http://www.jornada.unam.mx/v7.0/imagenes/la-jornada-trans.png'
margin-top: 2em; \ extra_css = """
text-align: center \ body{font-family: "Times New Roman",serif }
} \ .cabeza{font-size: xx-large; font-weight: bold }
p.sumario{ \ .credito-articulo{font-size: 1.3em}
text-align: center \ """
} \
.sumarios{font-weight: bold} \
.cabeza{ font-size: 1.5em} \
.pie-foto { \
text-align: justify; \
font-size: 0.8em; \
text-align: justify; \
} \
.pie-foto .credito { \
font-weight: bold; \
display: block \
} \
.credito-autor{ \
margin-top: 1.5em; \
padding-left: 0.6em; \
border-bottom: 1px solid #626366; \
font-variant: small-caps; \
font-weight: bold \
} \
.credito-articulo{ \
margin-top: 1.5em; \
padding-left: 0.6em; \
border-bottom: 1px solid #626366; \
font-variant: small-caps; \
font-weight: bold \
} \
.credito-titulo{text-align: right} \
.hemero { \
text-align: right; \
font-size: 0.9em; \
margin-bottom: 8px; \
} \
.loc { \
font-weight: bold; \
} \
.carton { \
text-align: center; \
} \
.credit { \
font-weight: bold; \
} \
'
preprocess_regexps = [ conversion_options = {
# Remove capitalized initial letter on some articles (editorial) 'comment' : description
(re.compile(r'<div class="inicial">(.*)</div><p class="s-s">', re.DOTALL|re.IGNORECASE), , 'tags' : category
lambda match: match.group(1)), , 'publisher' : publisher
# Cartons section uses a class instead of a div to identify the main content. Change it. , 'language' : language
(re.compile(r'class="carton"', re.DOTALL|re.IGNORECASE), }
lambda match: 'id="article-cont" class="carton"'),
# Remove <link rel="alternate"> as calibre has a bug (to report) keep_only_tags = [
(re.compile(r'<link rel="alternate".*?/>', re.DOTALL|re.IGNORECASE), dict(name='div', attrs={'class':['documentContent','cabeza','sumarios','text']})
lambda match: ''), ,dict(name='div', attrs={'id':'renderComments'})
] ]
remove_tags = [dict(name='div', attrs={'class':'buttonbar'})]
INDEX = 'http://www.jornada.unam.mx/rss/edicion.xml'
feeds = [ feeds = [
(u'Opinion','http://www.jornada.unam.mx/rss/opinion.xml'), (u'Ultimas noticias' , u'http://www.jornada.unam.mx/ultimas/news/RSS' )
(u'Cartones','http://www.jornada.unam.mx/rss/cartones.xml'), ,(u'Opinion' , u'http://www.jornada.unam.mx/rss/opinion.xml' )
(u'Política','http://www.jornada.unam.mx/rss/politica.xml'), ,(u'Politica' , u'http://www.jornada.unam.mx/rss/politica.xml' )
(u'Economía','http://www.jornada.unam.mx/rss/economia.xml'), ,(u'Economia' , u'http://www.jornada.unam.mx/rss/economia.xml' )
(u'Mundo','http://www.jornada.unam.mx/rss/mundo.xml'), ,(u'Mundo' , u'http://www.jornada.unam.mx/rss/mundo.xml' )
(u'Estados','http://www.jornada.unam.mx/rss/estados.xml'), ,(u'Estados' , u'http://www.jornada.unam.mx/rss/estados.xml' )
(u'Capital','http://www.jornada.unam.mx/rss/capital.xml'), ,(u'Capital' , u'http://www.jornada.unam.mx/rss/capital.xml' )
(u'Sociedad','http://www.jornada.unam.mx/rss/sociedad.xml'), ,(u'Sociedad y justicia' , u'http://www.jornada.unam.mx/rss/sociedad.xml' )
(u'Ciencias','http://www.jornada.unam.mx/rss/ciencias.xml'), ,(u'Ciencias' , u'http://www.jornada.unam.mx/rss/ciencias.xml' )
(u'Cultura','http://www.jornada.unam.mx/rss/cultura.xml'), ,(u'Cultura' , u'http://www.jornada.unam.mx/rss/cultura.xml' )
(u'Gastronomia','http://www.jornada.unam.mx/rss/gastronomia.xml'), ,(u'Gastronomia' , u'http://www.jornada.unam.mx/rss/gastronomia.xml' )
(u'Espectáculos','http://www.jornada.unam.mx/rss/espectaculos.xml'), ,(u'Espectaculos' , u'http://www.jornada.unam.mx/rss/espectaculos.xml' )
(u'Deportes','http://www.jornada.unam.mx/rss/deportes.xml'), ,(u'Deportes' , u'http://www.jornada.unam.mx/rss/deportes.xml' )
] ]
def get_cover_url(self): def preprocess_html(self, soup):
''' for item in soup.findAll(style=True):
Cover URL is http://www.jornada.unam.mx/YYYY/MM/DD/portada.pdf del item['style']
''' return soup
cover_url = None
soup = self.index_to_soup(self.INDEX)
soupstone = BeautifulStoneSoup(str(soup))
urlbase = str(soupstone('link')[0])
r= re.compile(r'.*http://www.jornada.unam.mx/([0-9]{4})/([0-9]{2})/([0-9]{2})', re.DOTALL|re.IGNORECASE)
m = r.match(urlbase)
if m:
cover_url = 'http://www.jornada.unam.mx/' + m.groups()[0] + '/' + m.groups()[1] + '/' + m.groups()[2] + '/portada.pdf'
return cover_url

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Brendan Sleight <bms.calibre at barwap.com>'
'''
impreso.milenio.com
'''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
import datetime
class Milenio(BasicNewsRecipe):
title = u'Milenio-diario'
__author__ = 'Bmsleight'
language = 'es'
description = 'Milenio-diario'
oldest_article = 10
max_articles_per_feed = 100
no_stylesheets = False
index = 'http://impreso.milenio.com'
keep_only_tags = [
dict(name='div', attrs={'class':'content'})
]
def parse_index(self):
# "%m/%d/%Y"
# http://impreso.milenio.com/Nacional/2010/09/01/
totalfeeds = []
soup = self.index_to_soup(self.index + "/Nacional/" + datetime.date.today().strftime("%Y/%m/%d"))
maincontent = soup.find('div',attrs={'class':'content'})
mfeed = []
if maincontent:
for itt in maincontent.findAll('a',href=True):
if "/node/" in str(itt['href']):
url = self.index + itt['href']
title = self.tag_to_string(itt)
description = ''
date = strftime(self.timefmt)
mfeed.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append(('Articles', mfeed))
return totalfeeds

View File

@ -102,7 +102,6 @@ class PeriodicalNameHere(BasicNewsRecipe):
todays_section = soup.find(True, attrs={'class':'todaydateline'}) todays_section = soup.find(True, attrs={'class':'todaydateline'})
self.section_dates.append(self.tag_to_string(todays_section,use_alt=False)) self.section_dates.append(self.tag_to_string(todays_section,use_alt=False))
self.section_dates.append(self.tag_to_string(todays_section,use_alt=False))
older_section_dates = soup.findAll(True, attrs={'class':'maindateline'}) older_section_dates = soup.findAll(True, attrs={'class':'maindateline'})
for older_section in older_section_dates : for older_section in older_section_dates :

View File

@ -0,0 +1,30 @@
from calibre.web.feeds.news import BasicNewsRecipe
class WinnipegFreePress(BasicNewsRecipe):
title = u'Winnipeg Free Press'
__author__ = 'buyo'
description = 'News from Winnipeg, Manitoba, Canada'
oldest_article = 1
max_articles_per_feed = 15
category = 'News, Winnipeg, Canada'
cover_url = 'http://media.winnipegfreepress.com/designimages/winnipegfreepress_WFP.gif'
no_stylesheets = True
encoding = 'UTF-8'
remove_javascript = True
use_embedded_content = False
language = 'en_CA'
feeds = [(u'Breaking News', u'http://www.winnipegfreepress.com/rss?path=/breakingnews'),
(u'Local News',u'http://www.winnipegfreepress.com/rss?path=/local'),
(u'Breaking Business News',u'http://www.winnipegfreepress.com/rss?path=/business/finance'),
(u'Business',u'http://www.winnipegfreepress.com/rss?path=/business'),
(u'Editorials',u'http://www.winnipegfreepress.com/rss?path=/opinion/editorials'),
(u'Views from the West',u'http://www.winnipegfreepress.com/rss?path=/opinion/westview'),
(u'Life & Style',u'http://www.winnipegfreepress.com/rss?path=/life'),
(u'Food & Drink',u'http://www.winnipegfreepress.com/rss?path=/life/food')
]
keep_only_tags = [
dict(name='div', attrs={'id':'article_header'}),
dict(name='div', attrs={'class':'article'}),
]

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.7.16' __version__ = '0.7.17'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re
@ -84,6 +84,9 @@ if plugins is None:
# }}} # }}}
# config_dir {{{ # config_dir {{{
CONFIG_DIR_MODE = 0700
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'): if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY']) config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
elif iswindows: elif iswindows:
@ -98,7 +101,11 @@ elif isosx:
else: else:
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config'))) bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre') config_dir = os.path.join(bdir, 'calibre')
if not os.access(config_dir, os.W_OK): try:
os.makedirs(config_dir, mode=CONFIG_DIR_MODE)
except:
pass
if not os.access(config_dir, os.W_OK) or not os.access(config_dir, os.X_OK):
print 'No write acces to', config_dir, 'using a temporary dir instead' print 'No write acces to', config_dir, 'using a temporary dir instead'
import tempfile, atexit import tempfile, atexit
config_dir = tempfile.mkdtemp(prefix='calibre-config-') config_dir = tempfile.mkdtemp(prefix='calibre-config-')

View File

@ -391,6 +391,8 @@ class PreferencesPlugin(Plugin): # {{{
#: The category this plugin should be in #: The category this plugin should be in
category = None category = None
#: The category name displayed to the user for this plugin
gui_category = None
#: The name displayed to the user for this plugin #: The name displayed to the user for this plugin
gui_name = None gui_name = None

View File

@ -679,12 +679,40 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
class LookAndFeel(PreferencesPlugin): class LookAndFeel(PreferencesPlugin):
name = 'Look & Feel' name = 'Look & Feel'
gui_name = _('Look and Feel') gui_name = _('Look and Feel')
category = _('Interface') category = 'Interface'
gui_category = _('Interface')
category_order = 1 category_order = 1
name_order = 1 name_order = 1
config_widget = 'calibre.gui2.preferences.look_feel' config_widget = 'calibre.gui2.preferences.look_feel'
plugins += [LookAndFeel] class Behavior(PreferencesPlugin):
name = 'Behavior'
gui_name = _('Behavior')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 2
config_widget = 'calibre.gui2.preferences.behavior'
class Columns(PreferencesPlugin):
name = 'Custom Columns'
gui_name = _('Add your own columns')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 3
config_widget = 'calibre.gui2.preferences.columns'
class Toolbar(PreferencesPlugin):
name = 'Toolbar'
gui_name = _('Customize the toolbar')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 4
config_widget = 'calibre.gui2.preferences.toolbar'
plugins += [LookAndFeel, Behavior, Columns, Toolbar]
#}}} #}}}

View File

@ -41,7 +41,7 @@ class ANDROID(USBMS):
0x502 : { 0x3203 : [0x0100]}, 0x502 : { 0x3203 : [0x0100]},
# Dell # Dell
0x413c : { 0xb007 : [0x0100]}, 0x413c : { 0xb007 : [0x0100, 0x0224]},
# Eken? # Eken?
0x040d : { 0x0851 : [0x0001]}, 0x040d : { 0x0851 : [0x0001]},

View File

@ -50,6 +50,8 @@ class JETBOOK(USBMS):
def filename_callback(self, fname, mi): def filename_callback(self, fname, mi):
fileext = os.path.splitext(os.path.basename(fname))[1] fileext = os.path.splitext(os.path.basename(fname))[1]
if fileext.lower() not in ('txt', 'pdf', 'fb2'):
return fname
title = mi.title if mi.title else 'Unknown' title = mi.title if mi.title else 'Unknown'
title = title.replace(' ', '_') title = title.replace(' ', '_')
au = mi.format_authors() au = mi.format_authors()

View File

@ -23,7 +23,7 @@ class Book(MetaInformation):
'uuid', 'device_collections', 'uuid', 'device_collections',
] ]
def __init__(self, prefix, lpath, title, authors, mime, date, ContentType, thumbnail_name, other=None): def __init__(self, prefix, lpath, title, authors, mime, date, ContentType, thumbnail_name, size=None, other=None):
MetaInformation.__init__(self, '') MetaInformation.__init__(self, '')
self.device_collections = [] self.device_collections = []
@ -42,10 +42,8 @@ class Book(MetaInformation):
else: else:
self.authors = [authors] self.authors = [authors]
self.mime = mime self.mime = mime
try:
self.size = os.path.getsize(self.path) self.size = size # will be set later if None
except OSError:
self.size = 0
try: try:
if ContentType == '6': if ContentType == '6':
self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f") self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f")

View File

@ -94,19 +94,19 @@ class KOBO(USBMS):
idx = bl_cache.get(lpath, None) idx = bl_cache.get(lpath, None)
if idx is not None: if idx is not None:
bl_cache[lpath] = None
if ImageID is not None: if ImageID is not None:
imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed') imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed')
#print "Image name Normalized: " + imagename #print "Image name Normalized: " + imagename
if imagename is not None: if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename) bl[idx].thumbnail = ImageWrapper(imagename)
bl_cache[lpath] = None
if ContentType != '6': if ContentType != '6':
if self.update_metadata_item(bl[idx]): if self.update_metadata_item(bl[idx]):
# print 'update_metadata_item returned true' # print 'update_metadata_item returned true'
changed = True changed = True
bl[idx].device_collections = playlist_map.get(lpath, []) bl[idx].device_collections = playlist_map.get(lpath, [])
else: else:
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID) book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
# print 'Update booklist' # print 'Update booklist'
if bl.add_book(book, replace_metadata=False): if bl.add_book(book, replace_metadata=False):
changed = True changed = True
@ -316,10 +316,10 @@ class KOBO(USBMS):
lpath = lpath[1:] lpath = lpath[1:]
#print "path: " + lpath #print "path: " + lpath
#book = self.book_class(prefix, lpath, other=info) #book = self.book_class(prefix, lpath, other=info)
lpath = self.normalize_path(prefix + lpath)
book = Book(prefix, lpath, '', '', '', '', '', '', other=info) book = Book(prefix, lpath, '', '', '', '', '', '', other=info)
if book.size is None: if book.size is None:
book.size = os.stat(self.normalize_path(path)).st_size book.size = os.stat(self.normalize_path(path)).st_size
book._new_book = True # Must be before add_book
booklists[blist].add_book(book, replace_metadata=True) booklists[blist].add_book(book, replace_metadata=True)
self.report_progress(1.0, _('Adding books to device metadata listing...')) self.report_progress(1.0, _('Adding books to device metadata listing...'))
@ -380,3 +380,19 @@ class KOBO(USBMS):
return USBMS.get_file(self, path, *args, **kwargs) return USBMS.get_file(self, path, *args, **kwargs)
@classmethod
def book_from_path(cls, prefix, lpath, title, authors, mime, date, ContentType, ImageID):
from calibre.ebooks.metadata import MetaInformation
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
else:
from calibre.ebooks.metadata.meta import metadata_from_filename
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
cls.build_template_regexp())
if mi is None:
mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0],
[_('Unknown')])
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=size, other=mi)
return book

View File

@ -196,8 +196,7 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
try: try:
new_cdata = open(mi.cover, 'rb').read() new_cdata = open(mi.cover, 'rb').read()
except: except:
import traceback pass
traceback.print_exc()
if new_cdata and raster_cover: if new_cdata and raster_cover:
try: try:
cpath = posixpath.join(posixpath.dirname(reader.opf_path), cpath = posixpath.join(posixpath.dirname(reader.opf_path),

View File

@ -62,7 +62,16 @@ class HTMLTOCAdder(object):
def __call__(self, oeb, context): def __call__(self, oeb, context):
if 'toc' in oeb.guide: if 'toc' in oeb.guide:
# Ensure toc pointed to in <guide> is in spine
from calibre.ebooks.oeb.base import urlnormalize
href = urlnormalize(oeb.guide['toc'].href)
if href in oeb.manifest.hrefs:
item = oeb.manifest.hrefs[href]
if oeb.spine.index(item) < 0:
oeb.spine.add(item, linear=False)
return return
else:
oeb.guide.remove('toc')
if not getattr(getattr(oeb, 'toc', False), 'nodes', False): if not getattr(getattr(oeb, 'toc', False), 'nodes', False):
return return
oeb.logger.info('Generating in-line TOC...') oeb.logger.info('Generating in-line TOC...')

View File

@ -5,15 +5,16 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os, shutil
from functools import partial from functools import partial
from PyQt4.Qt import QMenu, Qt from PyQt4.Qt import QMenu, Qt, QInputDialog
from calibre import isbytestring from calibre import isbytestring
from calibre.constants import filesystem_encoding from calibre.constants import filesystem_encoding
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.gui2 import gprefs, warning_dialog, Dispatcher from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
question_dialog
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
class LibraryUsageStats(object): class LibraryUsageStats(object):
@ -66,6 +67,13 @@ class LibraryUsageStats(object):
loc = loc[:-1] loc = loc[:-1]
return loc.split('/')[-1] return loc.split('/')[-1]
def rename(self, location, newloc):
newloc = self.canonicalize_path(newloc)
stats = self.stats.pop(location, None)
if stats is not None:
self.stats[newloc] = stats
self.write_stats()
class ChooseLibraryAction(InterfaceAction): class ChooseLibraryAction(InterfaceAction):
@ -80,7 +88,7 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.stats = LibraryUsageStats() self.stats = LibraryUsageStats()
self.create_action(spec=(_('Switch to library...'), 'lt.png', None, self.create_action(spec=(_('Switch/create library...'), 'lt.png', None,
None), attr='action_choose') None), attr='action_choose')
self.action_choose.triggered.connect(self.choose_library, self.action_choose.triggered.connect(self.choose_library,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
@ -90,7 +98,13 @@ class ChooseLibraryAction(InterfaceAction):
self.quick_menu = QMenu(_('Quick switch')) self.quick_menu = QMenu(_('Quick switch'))
self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu) self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
self.qs_separator = self.choose_menu.addSeparator() self.rename_menu = QMenu(_('Rename library'))
self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu)
self.delete_menu = QMenu(_('Delete library'))
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
self.rename_separator = self.choose_menu.addSeparator()
self.switch_actions = [] self.switch_actions = []
for i in range(5): for i in range(5):
ac = self.create_action(spec=('', None, None, None), ac = self.create_action(spec=('', None, None, None),
@ -123,9 +137,15 @@ class ChooseLibraryAction(InterfaceAction):
ac.setVisible(False) ac.setVisible(False)
self.quick_menu.clear() self.quick_menu.clear()
self.qs_locations = [i[1] for i in locations] self.qs_locations = [i[1] for i in locations]
self.rename_menu.clear()
self.delete_menu.clear()
for name, loc in locations: for name, loc in locations:
self.quick_menu.addAction(name, Dispatcher(partial(self.switch_requested, self.quick_menu.addAction(name, Dispatcher(partial(self.switch_requested,
loc))) loc)))
self.rename_menu.addAction(name, Dispatcher(partial(self.rename_requested,
name, loc)))
self.delete_menu.addAction(name, Dispatcher(partial(self.delete_requested,
name, loc)))
for i, x in enumerate(locations[:len(self.switch_actions)]): for i, x in enumerate(locations[:len(self.switch_actions)]):
name, loc = x name, loc = x
@ -134,12 +154,50 @@ class ChooseLibraryAction(InterfaceAction):
ac.setVisible(True) ac.setVisible(True)
self.quick_menu_action.setVisible(bool(locations)) self.quick_menu_action.setVisible(bool(locations))
self.rename_menu_action.setVisible(bool(locations))
self.delete_menu_action.setVisible(bool(locations))
def location_selected(self, loc): def location_selected(self, loc):
enabled = loc == 'library' enabled = loc == 'library'
self.qaction.setEnabled(enabled) self.qaction.setEnabled(enabled)
def rename_requested(self, name, location):
loc = location.replace('/', os.sep)
base = os.path.dirname(loc)
newname, ok = QInputDialog.getText(self.gui, _('Rename') + ' ' + name,
'<p>'+_('Choose a new name for the library <b>%s</b>. ')%name +
'<p>'+_('Note that the actual library folder will be renamed.'),
text=name)
newname = unicode(newname)
if not ok or not newname or newname == name:
return
newloc = os.path.join(base, newname)
if os.path.exists(newloc):
return error_dialog(self.gui, _('Already exists'),
_('The folder %s already exists. Delete it first.') %
newloc, show=True)
os.rename(loc, newloc)
self.stats.rename(location, newloc)
self.build_menus()
def delete_requested(self, name, location):
loc = location.replace('/', os.sep)
if not question_dialog(self.gui, _('Are you sure?'), '<p>'+
_('All files from %s will be '
'<b>permanently deleted</b>. Are you sure?') % loc,
show_copy_button=False):
return
exists = self.gui.library_view.model().db.exists_at(loc)
if exists:
try:
shutil.rmtree(loc, ignore_errors=True)
except:
pass
self.stats.remove(location)
self.build_menus()
def switch_requested(self, location): def switch_requested(self, location):
if not self.change_library_allowed(): if not self.change_library_allowed():
return return

View File

@ -5,6 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os
from functools import partial from functools import partial
from threading import Thread from threading import Thread
@ -13,6 +14,7 @@ from PyQt4.Qt import QMenu, QToolButton
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.gui2 import error_dialog, Dispatcher from calibre.gui2 import error_dialog, Dispatcher
from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.utils.config import prefs, tweaks
class Worker(Thread): class Worker(Thread):
@ -38,6 +40,13 @@ class Worker(Thread):
self.done() self.done()
def add_formats(self, id, paths, newdb, replace=True):
for path in paths:
fmt = os.path.splitext(path)[-1].replace('.', '').upper()
with open(path, 'rb') as f:
newdb.add_format(id, fmt, f, index_is_id=True,
notify=False, replace=replace)
def doit(self): def doit(self):
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
newdb = LibraryDatabase2(self.loc) newdb = LibraryDatabase2(self.loc)
@ -49,14 +58,21 @@ class Worker(Thread):
else: fmts = fmts.split(',') else: fmts = fmts.split(',')
paths = [self.db.format_abspath(x, fmt, index_is_id=True) for fmt in paths = [self.db.format_abspath(x, fmt, index_is_id=True) for fmt in
fmts] fmts]
newdb.import_book(mi, paths, notify=False, import_hooks=False) added = False
if prefs['add_formats_to_existing']:
identical_book_list = newdb.find_identical_books(mi)
if identical_book_list: # books with same author and nearly same title exist in newdb
added = True
for identical_book in identical_book_list:
self.add_formats(identical_book, paths, newdb, replace=False)
if not added:
newdb.import_book(mi, paths, notify=False, import_hooks=False,
apply_import_tags=tweaks['add_new_book_tags_when_importing_books'])
co = self.db.conversion_options(x, 'PIPE') co = self.db.conversion_options(x, 'PIPE')
if co is not None: if co is not None:
newdb.set_conversion_options(x, 'PIPE', co) newdb.set_conversion_options(x, 'PIPE', co)
class CopyToLibraryAction(InterfaceAction): class CopyToLibraryAction(InterfaceAction):
name = 'Copy To Library' name = 'Copy To Library'

View File

@ -21,7 +21,8 @@ class SimilarBooksAction(InterfaceAction):
m = QMenu(self.gui) m = QMenu(self.gui)
for text, icon, target, shortcut in [ for text, icon, target, shortcut in [
(_('Books by same author'), 'user_profile.svg', 'authors', _('Alt+A')), (_('Books by same author'), 'user_profile.svg', 'authors', _('Alt+A')),
(_('Books in this series'), 'books_in_series.svg', 'series', _('Alt+S')), (_('Books in this series'), 'books_in_series.svg', 'series',
_('Alt+Shift+S')),
(_('Books by this publisher'), 'publisher.png', 'publisher', _('Alt+P')), (_('Books by this publisher'), 'publisher.png', 'publisher', _('Alt+P')),
(_('Books with the same tags'), 'tags.svg', 'tag', _('Alt+T')),]: (_('Books with the same tags'), 'tags.svg', 'tag', _('Alt+T')),]:
ac = self.create_action(spec=(text, icon, None, shortcut), ac = self.create_action(spec=(text, icon, None, shortcut),

View File

@ -1,7 +1,7 @@
''' '''
UI for adding books to the database and saving books to disk UI for adding books to the database and saving books to disk
''' '''
import os, shutil, time, re import os, shutil, time
from Queue import Queue, Empty from Queue import Queue, Empty
from threading import Thread from threading import Thread
@ -94,14 +94,6 @@ class DBAdder(Thread): # {{{
self.daemon = True self.daemon = True
self.input_queue = Queue() self.input_queue = Queue()
self.output_queue = Queue() self.output_queue = Queue()
self.fuzzy_title_patterns = [(re.compile(pat), repl) for pat, repl in
[
(r'[\[\](){}<>\'";,:#]', ''),
(r'^(the|a|an) ', ''),
(r'[-._]', ' '),
(r'\s+', ' ')
]
]
self.merged_books = set([]) self.merged_books = set([])
def run(self): def run(self):
@ -138,33 +130,6 @@ class DBAdder(Thread): # {{{
fmts[-1] = fmt fmts[-1] = fmt
return fmts return fmts
def fuzzy_title(self, title):
title = title.strip().lower()
for pat, repl in self.fuzzy_title_patterns:
title = pat.sub(repl, title)
return title
def find_identical_books(self, mi):
identical_book_ids = set([])
if mi.authors:
try:
query = u' and '.join([u'author:"=%s"'%(a.replace('"', '')) for a in
mi.authors])
except ValueError:
return identical_book_ids
try:
book_ids = self.db.data.parse(query)
except:
import traceback
traceback.print_exc()
return identical_book_ids
for book_id in book_ids:
fbook_title = self.db.title(book_id, index_is_id=True)
fbook_title = self.fuzzy_title(fbook_title)
mbook_title = self.fuzzy_title(mi.title)
if fbook_title == mbook_title:
identical_book_ids.add(book_id)
return identical_book_ids
def add(self, id, opf, cover, name): def add(self, id, opf, cover, name):
formats = self.ids.pop(id) formats = self.ids.pop(id)
@ -191,7 +156,7 @@ class DBAdder(Thread): # {{{
orig_formats = formats orig_formats = formats
formats = [f for f in formats if not f.lower().endswith('.opf')] formats = [f for f in formats if not f.lower().endswith('.opf')]
if prefs['add_formats_to_existing']: if prefs['add_formats_to_existing']:
identical_book_list = self.find_identical_books(mi) identical_book_list = self.db.find_identical_books(mi)
if identical_book_list: # books with same author and nearly same title exist in db if identical_book_list: # books with same author and nearly same title exist in db
self.merged_books.add(mi.title) self.merged_books.add(mi.title)

View File

@ -457,6 +457,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.priority.setCurrentIndex(p) self.priority.setCurrentIndex(p)
self.priority.setVisible(iswindows) self.priority.setVisible(iswindows)
self.priority_label.setVisible(iswindows) self.priority_label.setVisible(iswindows)
self.new_book_tags.setText(', '.join(prefs['new_book_tags']))
self._plugin_model = PluginModel() self._plugin_model = PluginModel()
self.plugin_view.setModel(self._plugin_model) self.plugin_view.setModel(self._plugin_model)
self.plugin_view.setStyleSheet( self.plugin_view.setStyleSheet(
@ -906,6 +907,9 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['disable_tray_notification'] = not self.systray_notifications.isChecked() config['disable_tray_notification'] = not self.systray_notifications.isChecked()
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()] p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
prefs['worker_process_priority'] = p prefs['worker_process_priority'] = p
nbt = [x.strip() for x in
unicode(self.new_book_tags.text()).strip().split(',')]
prefs['new_book_tags'] = [x for x in nbt if x]
prefs['output_format'] = unicode(self.output_format.currentText()).upper() prefs['output_format'] = unicode(self.output_format.currentText()).upper()
config['cover_flow_queue_length'] = self.cover_browse.value() config['cover_flow_queue_length'] = self.cover_browse.value()
prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString()) prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString())

View File

@ -136,7 +136,7 @@
</item> </item>
<item> <item>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>Default network &amp;timeout:</string> <string>Default network &amp;timeout:</string>
@ -146,7 +146,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="2" column="1">
<widget class="QSpinBox" name="timeout"> <widget class="QSpinBox" name="timeout">
<property name="toolTip"> <property name="toolTip">
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string> <string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string>
@ -165,10 +165,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="3" column="1">
<widget class="QComboBox" name="language"/> <widget class="QComboBox" name="language"/>
</item> </item>
<item row="2" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_7"> <widget class="QLabel" name="label_7">
<property name="text"> <property name="text">
<string>Choose &amp;language (requires restart):</string> <string>Choose &amp;language (requires restart):</string>
@ -178,7 +178,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="4" column="1">
<widget class="QComboBox" name="priority"> <widget class="QComboBox" name="priority">
<item> <item>
<property name="text"> <property name="text">
@ -197,7 +197,7 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="priority_label"> <widget class="QLabel" name="priority_label">
<property name="text"> <property name="text">
<string>Job &amp;priority:</string> <string>Job &amp;priority:</string>
@ -207,7 +207,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_23"> <widget class="QLabel" name="label_23">
<property name="text"> <property name="text">
<string>Preferred &amp;output format:</string> <string>Preferred &amp;output format:</string>
@ -217,9 +217,26 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="1" column="1">
<widget class="QComboBox" name="output_format"/> <widget class="QComboBox" name="output_format"/>
</item> </item>
<item row="0" column="0">
<widget class="QLabel" name="label_230">
<property name="text">
<string>Tags to apply when adding a book:</string>
</property>
<property name="buddy">
<cstring>new_book_tags</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="new_book_tags">
<property name="toolTip">
<string>A comma-separated list of tags that will be applied to books added to the library</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>

View File

@ -105,6 +105,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
return self.simple_error('', _('No lookup name was provided')) return self.simple_error('', _('No lookup name was provided'))
if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col: if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col:
return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter')) return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter'))
if col.endswith('_index'):
return self.simple_error('', _('Lookup names cannot end with _index, because these names are reserved for the index of a series column.'))
col_heading = unicode(self.column_heading_box.text()) col_heading = unicode(self.column_heading_box.text())
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype'] col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
if col_type == '*text': if col_type == '*text':

View File

@ -236,8 +236,8 @@ class BooksModel(QAbstractTableModel): # {{{
def search(self, text, reset=True): def search(self, text, reset=True):
try: try:
self.db.search(text) self.db.search(text)
except ParseException: except ParseException as e:
self.searched.emit(False) self.searched.emit(e.msg)
return return
self.last_search = text self.last_search = text
if reset: if reset:

View File

@ -229,6 +229,8 @@ class BooksView(QTableView): # {{{
def cleanup_sort_history(self, sort_history): def cleanup_sort_history(self, sort_history):
history = [] history = []
for col, order in sort_history: for col, order in sort_history:
if col == 'date':
col = 'timestamp'
if col in self.column_map and (not history or history[0][0] != col): if col in self.column_map and (not history or history[0][0] != col):
history.append([col, order]) history.append([col, order])
return history return history

View File

@ -17,6 +17,9 @@ class ConfigWidgetInterface(object):
def genesis(self, gui): def genesis(self, gui):
raise NotImplementedError() raise NotImplementedError()
def initialize(self):
raise NotImplementedError()
def restore_defaults(self): def restore_defaults(self):
pass pass
@ -26,39 +29,39 @@ class ConfigWidgetInterface(object):
class Setting(object): class Setting(object):
def __init__(self, name, config_obj, widget, gui_name=None, def __init__(self, name, config_obj, widget, gui_name=None,
empty_string_is_None=True, choices=None): empty_string_is_None=True, choices=None, restart_required=False):
self.name, self.gui_name = name, gui_name self.name, self.gui_name = name, gui_name
self.empty_string_is_None = empty_string_is_None self.empty_string_is_None = empty_string_is_None
self.restart_required = restart_required
self.choices = choices self.choices = choices
if gui_name is None: if gui_name is None:
self.gui_name = 'opt_'+name self.gui_name = 'opt_'+name
self.config_obj = config_obj self.config_obj = config_obj
self.gui_obj = getattr(widget, self.gui_name) self.gui_obj = getattr(widget, self.gui_name)
self.widget = widget
if isinstance(self.gui_obj, QCheckBox): if isinstance(self.gui_obj, QCheckBox):
self.datatype = 'bool' self.datatype = 'bool'
self.gui_obj.stateChanged.connect(lambda x: self.gui_obj.stateChanged.connect(self.changed)
widget.changed_signal.emit())
elif isinstance(self.gui_obj, QAbstractSpinBox): elif isinstance(self.gui_obj, QAbstractSpinBox):
self.datatype = 'number' self.datatype = 'number'
self.gui_obj.valueChanged.connect(lambda x: self.gui_obj.valueChanged.connect(self.changed)
widget.changed_signal.emit())
elif isinstance(self.gui_obj, QLineEdit): elif isinstance(self.gui_obj, QLineEdit):
self.datatype = 'string' self.datatype = 'string'
self.gui_obj.textChanged.connect(lambda x: self.gui_obj.textChanged.connect(self.changed)
widget.changed_signal.emit())
elif isinstance(self.gui_obj, QComboBox): elif isinstance(self.gui_obj, QComboBox):
self.datatype = 'choice' self.datatype = 'choice'
self.gui_obj.editTextChanged.connect(lambda x: self.gui_obj.editTextChanged.connect(self.changed)
widget.changed_signal.emit()) self.gui_obj.currentIndexChanged.connect(self.changed)
self.gui_obj.currentIndexChanged.connect(lambda x:
widget.changed_signal.emit())
else: else:
raise ValueError('Unknown data type') raise ValueError('Unknown data type')
def changed(self, *args):
self.widget.changed_signal.emit()
def initialize(self): def initialize(self):
self.gui_obj.blockSignals(True) self.gui_obj.blockSignals(True)
if self.datatype == 'choices': if self.datatype == 'choice':
self.gui_obj.clear() self.gui_obj.clear()
for x in self.choices: for x in self.choices:
if isinstance(x, basestring): if isinstance(x, basestring):
@ -66,9 +69,15 @@ class Setting(object):
self.gui_obj.addItem(x[0], QVariant(x[1])) self.gui_obj.addItem(x[0], QVariant(x[1]))
self.set_gui_val(self.get_config_val(default=False)) self.set_gui_val(self.get_config_val(default=False))
self.gui_obj.blockSignals(False) self.gui_obj.blockSignals(False)
self.initial_value = self.get_gui_val()
def commit(self): def commit(self):
val = self.get_gui_val()
oldval = self.get_config_val()
changed = val != oldval
if changed:
self.set_config_val(self.get_gui_val()) self.set_config_val(self.get_gui_val())
return changed and self.restart_required
def restore_defaults(self): def restore_defaults(self):
self.set_gui_val(self.get_config_val(default=True)) self.set_gui_val(self.get_config_val(default=True))
@ -90,7 +99,7 @@ class Setting(object):
self.gui_obj.setValue(val) self.gui_obj.setValue(val)
elif self.datatype == 'string': elif self.datatype == 'string':
self.gui_obj.setText(val if val else '') self.gui_obj.setText(val if val else '')
elif self.datatype == 'choices': elif self.datatype == 'choice':
idx = self.gui_obj.findData(QVariant(val)) idx = self.gui_obj.findData(QVariant(val))
if idx == -1: if idx == -1:
idx = 0 idx = 0
@ -100,17 +109,32 @@ class Setting(object):
if self.datatype == 'bool': if self.datatype == 'bool':
val = bool(self.gui_obj.isChecked()) val = bool(self.gui_obj.isChecked())
elif self.datatype == 'number': elif self.datatype == 'number':
val = self.gui_obj.value(val) val = self.gui_obj.value()
elif self.datatype == 'string': elif self.datatype == 'string':
val = unicode(self.gui_name.text()).strip() val = unicode(self.gui_name.text()).strip()
if self.empty_string_is_None and not val: if self.empty_string_is_None and not val:
val = None val = None
elif self.datatype == 'choices': elif self.datatype == 'choice':
idx = self.gui_obj.currentIndex() idx = self.gui_obj.currentIndex()
if idx < 0: idx = 0 if idx < 0: idx = 0
val = unicode(self.gui_obj.itemData(idx).toString()) val = unicode(self.gui_obj.itemData(idx).toString())
return val return val
class CommaSeparatedList(Setting):
def set_gui_val(self, val):
x = ''
if val:
x = u', '.join(val)
self.gui_obj.setText(x)
def get_gui_val(self):
val = unicode(self.gui_obj.text()).strip()
ans = []
if val:
ans = [x.strip() for x in val.split(',')]
ans = [x for x in ans if x]
return ans
class ConfigWidgetBase(QWidget, ConfigWidgetInterface): class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
@ -122,10 +146,11 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
self.setupUi(self) self.setupUi(self)
self.settings = {} self.settings = {}
def register(self, name, config_obj, gui_name=None, choices=None, setting=Setting): def register(self, name, config_obj, gui_name=None, choices=None,
restart_required=False, setting=Setting):
setting = setting(name, config_obj, self, gui_name=gui_name, setting = setting(name, config_obj, self, gui_name=gui_name,
choices=choices) choices=choices, restart_required=restart_required)
self.register_setting(setting) return self.register_setting(setting)
def register_setting(self, setting): def register_setting(self, setting):
self.settings[setting.name] = setting self.settings[setting.name] = setting
@ -135,9 +160,13 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
for setting in self.settings.values(): for setting in self.settings.values():
setting.initialize() setting.initialize()
def commit(self): def commit(self, *args):
restart_required = False
for setting in self.settings.values(): for setting in self.settings.values():
setting.commit() rr = setting.commit()
if rr:
restart_required = True
return restart_required
def restore_defaults(self, *args): def restore_defaults(self, *args):
for setting in self.settings.values(): for setting in self.settings.values():
@ -158,6 +187,7 @@ def test_widget(category, name, gui=None): # {{{
pl = get_plugin(category, name) pl = get_plugin(category, name)
d = QDialog() d = QDialog()
d.resize(750, 550) d.resize(750, 550)
d.setWindowTitle(category + " - " + name)
bb = QDialogButtonBox(d) bb = QDialogButtonBox(d)
bb.setStandardButtons(bb.Apply|bb.Cancel|bb.RestoreDefaults) bb.setStandardButtons(bb.Apply|bb.Cancel|bb.RestoreDefaults)
bb.accepted.connect(d.accept) bb.accepted.connect(d.accept)
@ -165,11 +195,13 @@ def test_widget(category, name, gui=None): # {{{
w = pl.create_widget(d) w = pl.create_widget(d)
bb.button(bb.RestoreDefaults).clicked.connect(w.restore_defaults) bb.button(bb.RestoreDefaults).clicked.connect(w.restore_defaults)
bb.button(bb.Apply).setEnabled(False) bb.button(bb.Apply).setEnabled(False)
w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnable(True)) bb.button(bb.Apply).clicked.connect(d.accept)
w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnabled(True))
l = QVBoxLayout() l = QVBoxLayout()
d.setLayout(l) d.setLayout(l)
l.addWidget(w) l.addWidget(w)
l.addWidget(bb) l.addWidget(bb)
mygui = gui is None
if gui is None: if gui is None:
from calibre.gui2.ui import Main from calibre.gui2.ui import Main
from calibre.gui2.main import option_parser from calibre.gui2.main import option_parser
@ -181,7 +213,14 @@ def test_widget(category, name, gui=None): # {{{
gui = Main(opts) gui = Main(opts)
gui.initialize(db.library_path, db, None, actions, show_gui=False) gui.initialize(db.library_path, db, None, actions, show_gui=False)
w.genesis(gui) w.genesis(gui)
w.initialize()
restart_required = False
if d.exec_() == QDialog.Accepted: if d.exec_() == QDialog.Accepted:
w.commit() restart_required = w.commit()
if restart_required:
from calibre.gui2 import warning_dialog
warning_dialog(gui, 'Restart required', 'Restart required', show=True)
if mygui:
gui.shutdown()
# }}} # }}}

View File

@ -0,0 +1,169 @@
#!/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 re
from PyQt4.Qt import Qt, QVariant, QListWidgetItem
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
CommaSeparatedList
from calibre.gui2.preferences.behavior_ui import Ui_Form
from calibre.gui2 import config, info_dialog, dynamic
from calibre.utils.config import prefs
from calibre.customize.ui import available_output_formats, all_input_formats
from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported
from calibre.constants import iswindows
class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
db = gui.library_view.model().db
r = self.register
r('worker_process_priority', prefs, choices=
[(_('Low'), 'low'), (_('Normal'), 'normal'), (_('High'), 'high')])
r('network_timeout', prefs)
r('overwrite_author_title_metadata', config)
r('get_social_metadata', config)
r('new_version_notification', config)
r('upload_news_to_device', config)
r('delete_news_from_library_on_upload', config)
output_formats = list(sorted(available_output_formats()))
output_formats.remove('oeb')
choices = [(x.upper(), x) for x in output_formats]
r('output_format', prefs, choices=choices)
restrictions = sorted(saved_searches().names(),
cmp=lambda x,y: cmp(x.lower(), y.lower()))
choices = [('', '')] + [(x, x) for x in restrictions]
r('gui_restriction', db.prefs, choices=choices)
r('new_book_tags', prefs, setting=CommaSeparatedList)
self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs)
self.input_up_button.clicked.connect(self.up_input)
self.input_down_button.clicked.connect(self.down_input)
for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'):
signal = getattr(self.opt_internally_viewed_formats, 'item'+signal)
signal.connect(self.internally_viewed_formats_changed)
self.settings['worker_process_priority'].gui_obj.setVisible(iswindows)
self.priority_label.setVisible(iswindows)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.init_input_order()
self.init_internally_viewed_formats()
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
self.init_input_order(defaults=True)
self.init_internally_viewed_formats(defaults=True)
self.changed_signal.emit()
def commit(self):
input_map = prefs['input_format_order']
input_cols = [unicode(self.opt_input_order.item(i).data(Qt.UserRole).toString()) for
i in range(self.opt_input_order.count())]
if input_map != input_cols:
prefs['input_format_order'] = input_cols
fmts = self.current_internally_viewed_formats
old = config['internally_viewed_formats']
if fmts != old:
config['internally_viewed_formats'] = fmts
return ConfigWidgetBase.commit(self)
# Internally viewed formats {{{
def internally_viewed_formats_changed(self, *args):
fmts = self.current_internally_viewed_formats
old = config['internally_viewed_formats']
if fmts != old:
self.changed_signal.emit()
def init_internally_viewed_formats(self, defaults=False):
if defaults:
fmts = config.defaults['internally_viewed_formats']
else:
fmts = config['internally_viewed_formats']
viewer = self.opt_internally_viewed_formats
viewer.blockSignals(True)
exts = set([])
for ext in BOOK_EXTENSIONS:
ext = ext.lower()
ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext)
if ext == 'lrf' or is_supported('book.'+ext):
exts.add(ext)
viewer.clear()
for ext in sorted(exts):
viewer.addItem(ext.upper())
item = viewer.item(viewer.count()-1)
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked if
ext.upper() in fmts else Qt.Unchecked)
viewer.blockSignals(False)
@property
def current_internally_viewed_formats(self):
fmts = []
viewer = self.opt_internally_viewed_formats
for i in range(viewer.count()):
if viewer.item(i).checkState() == Qt.Checked:
fmts.append(unicode(viewer.item(i).text()))
return fmts
# }}}
# Input format order {{{
def init_input_order(self, defaults=False):
if defaults:
input_map = prefs.defaults['input_format_order']
else:
input_map = prefs['input_format_order']
all_formats = set()
self.opt_input_order.clear()
for fmt in all_input_formats().union(set(['ZIP', 'RAR'])):
all_formats.add(fmt.upper())
for format in input_map + list(all_formats.difference(input_map)):
item = QListWidgetItem(format, self.opt_input_order)
item.setData(Qt.UserRole, QVariant(format))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable)
def up_input(self, *args):
idx = self.opt_input_order.currentRow()
if idx > 0:
self.opt_input_order.insertItem(idx-1, self.opt_input_order.takeItem(idx))
self.opt_input_order.setCurrentRow(idx-1)
self.changed_signal.emit()
def down_input(self, *args):
idx = self.opt_input_order.currentRow()
if idx < self.opt_input_order.count()-1:
self.opt_input_order.insertItem(idx+1, self.opt_input_order.takeItem(idx))
self.opt_input_order.setCurrentRow(idx+1)
self.changed_signal.emit()
# }}}
def reset_confirmation_dialogs(self, *args):
for key in dynamic.keys():
if key.endswith('_again') and dynamic[key] is False:
dynamic[key] = True
info_dialog(self, _('Done'),
_('Confirmation dialogs have all been reset'), show=True)
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Behavior')

View File

@ -29,21 +29,21 @@
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="2"> <item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="new_version_notification"> <widget class="QCheckBox" name="opt_new_version_notification">
<property name="text"> <property name="text">
<string>Show notification when &amp;new version is available</string> <string>Show notification when &amp;new version is available</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0" colspan="2"> <item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news"> <widget class="QCheckBox" name="opt_upload_news_to_device">
<property name="text"> <property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string> <string>Automatically send downloaded &amp;news to ebook reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0" colspan="2"> <item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news"> <widget class="QCheckBox" name="opt_delete_news_from_library_on_upload">
<property name="text"> <property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string> <string>&amp;Delete news from library when it is automatically sent to reader</string>
</property> </property>
@ -57,12 +57,12 @@
<string>Default network &amp;timeout:</string> <string>Default network &amp;timeout:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>timeout</cstring> <cstring>opt_network_timeout</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QSpinBox" name="timeout"> <widget class="QSpinBox" name="opt_network_timeout">
<property name="toolTip"> <property name="toolTip">
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string> <string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string>
</property> </property>
@ -81,7 +81,7 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="priority"> <widget class="QComboBox" name="opt_worker_process_priority">
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property> </property>
@ -111,7 +111,7 @@
<string>Job &amp;priority:</string> <string>Job &amp;priority:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>priority</cstring> <cstring>opt_worker_process_priority</cstring>
</property> </property>
</widget> </widget>
</item> </item>
@ -121,12 +121,12 @@
<string>Preferred &amp;output format:</string> <string>Preferred &amp;output format:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>output_format</cstring> <cstring>opt_output_format</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QComboBox" name="output_format"> <widget class="QComboBox" name="opt_output_format">
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property> </property>
@ -164,6 +164,20 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1">
<widget class="QLineEdit" name="opt_new_book_tags">
<property name="toolTip">
<string>A comma-separated list of tags that will be applied to books added to the library</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_230">
<property name="text">
<string>Tags to apply when adding a book:</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item row="6" column="0" colspan="2"> <item row="6" column="0" colspan="2">
@ -182,7 +196,7 @@
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_10"> <layout class="QHBoxLayout" name="horizontalLayout_10">
<item> <item>
<widget class="QListWidget" name="input_order"> <widget class="QListWidget" name="opt_input_order">
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -194,7 +208,7 @@
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_10"> <layout class="QVBoxLayout" name="verticalLayout_10">
<item> <item>
<widget class="QToolButton" name="input_up"> <widget class="QToolButton" name="input_up_button">
<property name="text"> <property name="text">
<string>...</string> <string>...</string>
</property> </property>
@ -218,7 +232,7 @@
</spacer> </spacer>
</item> </item>
<item> <item>
<widget class="QToolButton" name="input_down"> <widget class="QToolButton" name="input_down_button">
<property name="text"> <property name="text">
<string>...</string> <string>...</string>
</property> </property>
@ -242,7 +256,7 @@
</property> </property>
<layout class="QGridLayout" name="gridLayout_4"> <layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QListWidget" name="viewer"> <widget class="QListWidget" name="opt_internally_viewed_formats">
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>

View File

@ -0,0 +1,172 @@
#!/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 copy, sys
from PyQt4.Qt import Qt, QVariant, QListWidgetItem
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.columns_ui import Ui_Form
from calibre.gui2.preferences.create_custom_column import CreateCustomColumn
from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS
class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
db = self.gui.library_view.model().db
self.custcols = copy.deepcopy(db.field_metadata.get_custom_field_metadata())
self.column_up.clicked.connect(self.up_column)
self.column_down.clicked.connect(self.down_column)
self.del_custcol_button.clicked.connect(self.del_custcol)
self.add_custcol_button.clicked.connect(self.add_custcol)
self.add_col_button.clicked.connect(self.add_custcol)
self.edit_custcol_button.clicked.connect(self.edit_custcol)
for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'):
signal = getattr(self.opt_columns, 'item'+signal)
signal.connect(self.columns_changed)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.init_columns()
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
self.init_columns(defaults=True)
self.changed_signal.emit()
def commit(self):
rr = ConfigWidgetBase.commit(self)
return self.apply_custom_column_changes() or rr
def columns_changed(self, *args):
self.changed_signal.emit()
def columns_state(self, defaults=False):
if defaults:
return self.gui.library_view.get_default_state()
return self.gui.library_view.get_state()
def init_columns(self, defaults=False):
# Set up columns
self.opt_columns.blockSignals(True)
model = self.gui.library_view.model()
colmap = list(model.column_map)
state = self.columns_state(defaults)
hidden_cols = state['hidden_columns']
positions = state['column_positions']
colmap.sort(cmp=lambda x,y: cmp(positions[x], positions[y]))
self.opt_columns.clear()
for col in colmap:
item = QListWidgetItem(model.headers[col], self.opt_columns)
item.setData(Qt.UserRole, QVariant(col))
flags = Qt.ItemIsEnabled|Qt.ItemIsSelectable
if col != 'ondevice':
flags |= Qt.ItemIsUserCheckable
item.setFlags(flags)
if col != 'ondevice':
item.setCheckState(Qt.Unchecked if col in hidden_cols else
Qt.Checked)
self.opt_columns.blockSignals(False)
def up_column(self):
idx = self.opt_columns.currentRow()
if idx > 0:
self.opt_columns.insertItem(idx-1, self.opt_columns.takeItem(idx))
self.opt_columns.setCurrentRow(idx-1)
self.changed_signal.emit()
def down_column(self):
idx = self.opt_columns.currentRow()
if idx < self.opt_columns.count()-1:
self.opt_columns.insertItem(idx+1, self.opt_columns.takeItem(idx))
self.opt_columns.setCurrentRow(idx+1)
self.changed_signal.emit()
def del_custcol(self):
idx = self.opt_columns.currentRow()
if idx < 0:
return error_dialog(self, '', _('You must select a column to delete it'),
show=True)
col = unicode(self.opt_columns.item(idx).data(Qt.UserRole).toString())
if col not in self.custcols:
return error_dialog(self, '',
_('The selected column is not a custom column'), show=True)
if not question_dialog(self, _('Are you sure?'),
_('Do you really want to delete column %s and all its data?') %
self.custcols[col]['name'], show_copy_button=False):
return
self.opt_columns.item(idx).setCheckState(False)
self.opt_columns.takeItem(idx)
self.custcols[col]['*deleteme'] = True
self.changed_signal.emit()
def add_custcol(self):
model = self.gui.library_view.model()
CreateCustomColumn(self, False, model.orig_headers, ALL_COLUMNS)
self.changed_signal.emit()
def edit_custcol(self):
model = self.gui.library_view.model()
CreateCustomColumn(self, True, model.orig_headers, ALL_COLUMNS)
self.changed_signal.emit()
def apply_custom_column_changes(self):
model = self.gui.library_view.model()
db = model.db
config_cols = [unicode(self.opt_columns.item(i).data(Qt.UserRole).toString())\
for i in range(self.opt_columns.count())]
if not config_cols:
config_cols = ['title']
removed_cols = set(model.column_map) - set(config_cols)
hidden_cols = set([unicode(self.opt_columns.item(i).data(Qt.UserRole).toString())\
for i in range(self.opt_columns.count()) \
if self.opt_columns.item(i).checkState()==Qt.Unchecked])
hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols
hidden_cols = list(hidden_cols.intersection(set(model.column_map)))
if 'ondevice' in hidden_cols:
hidden_cols.remove('ondevice')
def col_pos(x, y):
xidx = config_cols.index(x) if x in config_cols else sys.maxint
yidx = config_cols.index(y) if y in config_cols else sys.maxint
return cmp(xidx, yidx)
positions = {}
for i, col in enumerate((sorted(model.column_map, cmp=col_pos))):
positions[col] = i
state = {'hidden_columns': hidden_cols, 'column_positions':positions}
self.gui.library_view.apply_state(state)
self.gui.library_view.save_state()
must_restart = False
for c in self.custcols:
if self.custcols[c]['colnum'] is None:
db.create_custom_column(
label=self.custcols[c]['label'],
name=self.custcols[c]['name'],
datatype=self.custcols[c]['datatype'],
is_multiple=self.custcols[c]['is_multiple'],
display = self.custcols[c]['display'])
must_restart = True
elif '*deleteme' in self.custcols[c]:
db.delete_custom_column(label=self.custcols[c]['label'])
must_restart = True
elif '*edited' in self.custcols[c]:
cc = self.custcols[c]
db.set_custom_column_metadata(cc['colnum'], name=cc['name'],
label=cc['label'],
display = self.custcols[c]['display'])
if '*must_restart' in self.custcols[c]:
must_restart = True
return must_restart
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Custom Columns')

View File

@ -25,7 +25,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QListWidget" name="columns"> <widget class="QListWidget" name="opt_columns">
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -155,7 +155,7 @@
</layout> </layout>
</item> </item>
<item row="2" column="0" colspan="2"> <item row="2" column="0" colspan="2">
<widget class="QPushButton" name="pushButton"> <widget class="QPushButton" name="add_col_button">
<property name="text"> <property name="text">
<string>Add &amp;custom column</string> <string>Add &amp;custom column</string>
</property> </property>

View File

@ -0,0 +1,174 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
'''Dialog to create a new custom column'''
import re
from functools import partial
from PyQt4.QtCore import SIGNAL
from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant
from calibre.gui2.preferences.create_custom_column_ui import Ui_QCreateCustomColumn
from calibre.gui2 import error_dialog
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
column_types = {
0:{'datatype':'text',
'text':_('Text, column shown in the tag browser'),
'is_multiple':False},
1:{'datatype':'*text',
'text':_('Comma separated text, like tags, shown in the tag browser'),
'is_multiple':True},
2:{'datatype':'comments',
'text':_('Long text, like comments, not shown in the tag browser'),
'is_multiple':False},
3:{'datatype':'series',
'text':_('Text column for keeping series-like information'),
'is_multiple':False},
4:{'datatype':'datetime',
'text':_('Date'), 'is_multiple':False},
5:{'datatype':'float',
'text':_('Floating point numbers'), 'is_multiple':False},
6:{'datatype':'int',
'text':_('Integers'), 'is_multiple':False},
7:{'datatype':'rating',
'text':_('Ratings, shown with stars'),
'is_multiple':False},
8:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False},
}
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)
self.connect(self.button_box, SIGNAL("rejected()"), self.reject)
self.parent = parent
self.editing_col = editing
self.standard_colheads = standard_colheads
self.standard_colnames = standard_colnames
for t in self.column_types:
self.column_type_box.addItem(self.column_types[t]['text'])
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
if not self.editing_col:
self.datatype_changed()
self.exec_()
return
idx = parent.opt_columns.currentRow()
if idx < 0:
self.simple_error(_('No column selected'),
_('No column has been selected'))
return
col = unicode(parent.opt_columns.item(idx).data(Qt.UserRole).toString())
if col not in parent.custcols:
self.simple_error('', _('Selected column is not a user-defined column'))
return
c = parent.custcols[col]
self.column_name_box.setText(c['label'])
self.column_heading_box.setText(c['name'])
ct = c['datatype'] if not c['is_multiple'] else '*text'
self.orig_column_number = c['colnum']
self.orig_column_name = col
column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types))
self.column_type_box.setCurrentIndex(column_numbers[ct])
self.column_type_box.setEnabled(False)
if ct == 'datetime':
if c['display'].get('date_format', None):
self.date_format_box.setText(c['display'].get('date_format', ''))
self.datatype_changed()
self.exec_()
def datatype_changed(self, *args):
try:
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
except:
col_type = None
df_visible = col_type == 'datetime'
for x in ('box', 'default_label', 'label'):
getattr(self, 'date_format_'+x).setVisible(df_visible)
def accept(self):
col = unicode(self.column_name_box.text())
if not col:
return self.simple_error('', _('No lookup name was provided'))
if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col:
return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter'))
if col.endswith('_index'):
return self.simple_error('', _('Lookup names cannot end with _index, because these names are reserved for the index of a series column.'))
col_heading = unicode(self.column_heading_box.text())
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
if col_type == '*text':
col_type='text'
is_multiple = True
else:
is_multiple = False
if not col_heading:
return self.simple_error('', _('No column heading was provided'))
bad_col = False
if col in self.parent.custcols:
if not self.editing_col or self.parent.custcols[col]['colnum'] != self.orig_column_number:
bad_col = True
if bad_col:
return self.simple_error('', _('The lookup name %s is already used')%col)
bad_head = False
for t in self.parent.custcols:
if self.parent.custcols[t]['name'] == col_heading:
if not self.editing_col or self.parent.custcols[t]['colnum'] != self.orig_column_number:
bad_head = True
for t in self.standard_colheads:
if self.standard_colheads[t] == col_heading:
bad_head = True
if bad_head:
return self.simple_error('', _('The heading %s is already used')%col_heading)
date_format = {}
if col_type == 'datetime':
if self.date_format_box.text():
date_format = {'date_format':unicode(self.date_format_box.text())}
else:
date_format = {'date_format': None}
db = self.parent.gui.library_view.model().db
key = db.field_metadata.custom_field_prefix+col
if not self.editing_col:
db.field_metadata
self.parent.custcols[key] = {
'label':col,
'name':col_heading,
'datatype':col_type,
'editable':True,
'display':date_format,
'normalized':None,
'colnum':None,
'is_multiple':is_multiple,
}
item = QListWidgetItem(col_heading, self.parent.opt_columns)
item.setData(Qt.UserRole, QVariant(key))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked)
else:
idx = self.parent.opt_columns.currentRow()
item = self.parent.opt_columns.item(idx)
item.setData(Qt.UserRole, QVariant(key))
item.setText(col_heading)
self.parent.custcols[self.orig_column_name]['label'] = col
self.parent.custcols[self.orig_column_name]['name'] = col_heading
self.parent.custcols[self.orig_column_name]['display'].update(date_format)
self.parent.custcols[self.orig_column_name]['*edited'] = True
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
QDialog.accept(self)
def reject(self):
QDialog.reject(self)

View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QCreateCustomColumn</class>
<widget class="QDialog" name="QCreateCustomColumn">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>528</width>
<height>199</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Create or edit custom columns</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="margin">
<number>5</number>
</property>
<item row="2" column="0">
<layout class="QGridLayout" name="gridLayout">
<property name="margin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Lookup name</string>
</property>
<property name="buddy">
<cstring>column_name_box</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Column &amp;heading</string>
</property>
<property name="buddy">
<cstring>column_heading_box</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="column_name_box">
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Used for searching the column. Must contain only digits and lower case letters.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="column_heading_box">
<property name="toolTip">
<string>Column heading in the library view and category name in the tag browser</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Column &amp;type</string>
</property>
<property name="buddy">
<cstring>column_type_box</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="column_type_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>What kind of information will be kept in the column.</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="date_format_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;p&gt;Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.&lt;/p&gt;
&lt;p&gt;For example:
&lt;ul&gt;
&lt;li&gt; ddd, d MMM yyyy gives Mon, 5 Jan 2010&lt;li&gt;
&lt;li&gt;dd MMMM yy gives 05 January 10&lt;/li&gt;
&lt;/ul&gt; </string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="date_format_default_label">
<property name="toolTip">
<string>Use MMM yyyy for month + year, yyyy for year only</string>
</property>
<property name="text">
<string>Default: dd MMM yyyy.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="date_format_label">
<property name="text">
<string>Format for &amp;dates</string>
</property>
<property name="buddy">
<cstring>date_format_box</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Create or edit custom columns</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>column_name_box</tabstop>
<tabstop>column_heading_box</tabstop>
<tabstop>column_type_box</tabstop>
<tabstop>date_format_box</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -20,7 +20,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r = self.register r = self.register
r('gui_layout', config, choices= r('gui_layout', config, restart_required=True, choices=
[(_('Wide'), 'wide'), (_('Narrow'), 'narrow')]) [(_('Wide'), 'wide'), (_('Narrow'), 'narrow')])
r('cover_flow_queue_length', config) r('cover_flow_queue_length', config)
@ -32,19 +32,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if l != lang] if l != lang]
if lang != 'en': if lang != 'en':
items.append(('en', get_language('en'))) items.append(('en', get_language('en')))
items.sort(cmp=lambda x, y: cmp(x[1], y[1])) items.sort(cmp=lambda x, y: cmp(x[1].lower(), y[1].lower()))
choices = [(y, x) for x, y in items] choices = [(y, x) for x, y in items]
# Default language is the autodetected one # Default language is the autodetected one
choices = [get_language(lang), lang] + choices choices = [(get_language(lang), lang)] + choices
r('language', prefs, choices=choices) r('language', prefs, choices=choices, restart_required=True)
r('show_avg_rating', config) r('show_avg_rating', config)
r('disable_animations', config) r('disable_animations', config)
r('systray_icon', config) r('systray_icon', config, restart_required=True)
r('show_splash_screen', gprefs) r('show_splash_screen', gprefs)
r('disable_tray_notification', config) r('disable_tray_notification', config)
r('use_roman_numerals_for_series_number', config) r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config) r('separate_cover_flow', config, restart_required=True)
r('search_as_you_type', config) r('search_as_you_type', config)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),

View File

@ -0,0 +1,293 @@
#!/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'
from functools import partial
from PyQt4.Qt import QAbstractListModel, Qt, QIcon, \
QVariant, QItemSelectionModel
from calibre.gui2.preferences.toolbar_ui import Ui_Form
from calibre.gui2 import gprefs, NONE, warning_dialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
class FakeAction(object):
def __init__(self, name, icon, tooltip=None,
dont_add_to=frozenset([]), dont_remove_from=frozenset([])):
self.name = name
self.action_spec = (name, icon, tooltip, None)
self.dont_remove_from = dont_remove_from
self.dont_add_to = dont_add_to
class BaseModel(QAbstractListModel):
def name_to_action(self, name, gui):
if name == 'Donate':
return FakeAction(name, 'donate.svg',
dont_add_to=frozenset(['context-menu',
'context-menu-device']))
if name == 'Location Manager':
return FakeAction(name, None,
_('Switch between library and device views'),
dont_remove_from=set(['toolbar-device']))
if name is None:
return FakeAction('--- '+_('Separator')+' ---', None)
return gui.iactions[name]
def rowCount(self, parent):
return len(self._data)
def data(self, index, role):
row = index.row()
action = self._data[row].action_spec
if role == Qt.DisplayRole:
text = action[0]
text = text.replace('&', '')
if text == _('%d books'):
text = _('Choose library')
return QVariant(text)
if role == Qt.DecorationRole:
ic = action[1]
if ic is None:
ic = 'blank.svg'
return QVariant(QIcon(I(ic)))
if role == Qt.ToolTipRole and action[2] is not None:
return QVariant(action[2])
return NONE
def names(self, indexes):
rows = [i.row() for i in indexes]
ans = []
for i in rows:
n = self._data[i].name
if n.startswith('---'):
n = None
ans.append(n)
return ans
class AllModel(BaseModel):
def __init__(self, key, gui):
BaseModel.__init__(self)
self.gprefs_name = 'action-layout-'+key
current = gprefs[self.gprefs_name]
self.gui = gui
self.key = key
self._data = self.get_all_actions(current)
def get_all_actions(self, current):
all = list(self.gui.iactions.keys()) + ['Donate']
all = [x for x in all if x not in current] + [None]
all = [self.name_to_action(x, self.gui) for x in all]
all = [x for x in all if self.key not in x.dont_add_to]
all.sort()
return all
def add(self, names):
actions = []
for name in names:
if name is None or name.startswith('---'): continue
actions.append(self.name_to_action(name, self.gui))
self._data.extend(actions)
self._data.sort()
self.reset()
def remove(self, indices, allowed):
rows = [i.row() for i in indices]
remove = set([])
for row in rows:
ac = self._data[row]
if ac.name.startswith('---'): continue
if ac.name in allowed:
remove.add(row)
ndata = []
for i, ac in enumerate(self._data):
if i not in remove:
ndata.append(ac)
self._data = ndata
self.reset()
def restore_defaults(self):
current = gprefs.defaults[self.gprefs_name]
self._data = self.get_all_actions(current)
self.reset()
class CurrentModel(BaseModel):
def __init__(self, key, gui):
BaseModel.__init__(self)
self.gprefs_name = 'action-layout-'+key
current = gprefs[self.gprefs_name]
self._data = [self.name_to_action(x, gui) for x in current]
self.key = key
self.gui = gui
def move(self, idx, delta):
row = idx.row()
if row < 0 or row >= len(self._data):
return
nrow = row + delta
if nrow < 0 or nrow >= len(self._data):
return
t = self._data[row]
self._data[row] = self._data[nrow]
self._data[nrow] = t
ni = self.index(nrow)
self.dataChanged.emit(idx, idx)
self.dataChanged.emit(ni, ni)
return ni
def add(self, names):
actions = []
reject = set([])
for name in names:
ac = self.name_to_action(name, self.gui)
if self.key in ac.dont_add_to:
reject.add(ac)
else:
actions.append(ac)
self._data.extend(actions)
self.reset()
return reject
def remove(self, indices):
rows = [i.row() for i in indices]
remove, rejected = set([]), set([])
for row in rows:
ac = self._data[row]
if self.key in ac.dont_remove_from:
rejected.add(ac)
continue
remove.add(row)
ndata = []
for i, ac in enumerate(self._data):
if i not in remove:
ndata.append(ac)
self._data = ndata
self.reset()
return rejected
def commit(self):
old = gprefs[self.gprefs_name]
new = []
for x in self._data:
n = x.name
if n.startswith('---'):
n = None
new.append(n)
new = tuple(new)
if new != old:
defaults = gprefs.defaults[self.gprefs_name]
if defaults == new:
del gprefs[self.gprefs_name]
else:
gprefs[self.gprefs_name] = new
def restore_defaults(self):
current = gprefs.defaults[self.gprefs_name]
self._data = [self.name_to_action(x, self.gui) for x in current]
self.reset()
class ConfigWidget(ConfigWidgetBase, Ui_Form):
LOCATIONS = [
('toolbar', _('The main toolbar')),
('toolbar-device', _('The main toolbar when a device is connected')),
('context-menu', _('The context menu for the books in the '
'calibre library')),
('context-menu-device', _('The context menu for the books on '
'the device'))
]
def genesis(self, gui):
self.models = {}
for key, text in self.LOCATIONS:
self.what.addItem(text, key)
all_model = AllModel(key, gui)
current_model = CurrentModel(key, gui)
self.models[key] = (all_model, current_model)
self.what.setCurrentIndex(0)
self.what.currentIndexChanged[int].connect(self.what_changed)
self.what_changed(0)
self.add_action_button.clicked.connect(self.add_action)
self.remove_action_button.clicked.connect(self.remove_action)
self.action_up_button.clicked.connect(partial(self.move, -1))
self.action_down_button.clicked.connect(partial(self.move, 1))
def what_changed(self, idx):
key = unicode(self.what.itemData(idx).toString())
self.all_actions.setModel(self.models[key][0])
self.current_actions.setModel(self.models[key][1])
def add_action(self, *args):
x = self.all_actions.selectionModel().selectedIndexes()
names = self.all_actions.model().names(x)
if names:
not_added = self.current_actions.model().add(names)
ns = set([x.name for x in not_added])
added = set(names) - ns
self.all_actions.model().remove(x, added)
if not_added:
warning_dialog(self, _('Cannot add'),
_('Cannot add the actions %s to this location') %
','.join([a.action_spec[0] for a in not_added]),
show=True)
if added:
ca = self.current_actions
idx = ca.model().index(ca.model().rowCount(None)-1)
ca.scrollTo(idx)
self.changed_signal.emit()
def remove_action(self, *args):
x = self.current_actions.selectionModel().selectedIndexes()
names = self.current_actions.model().names(x)
if names:
not_removed = self.current_actions.model().remove(x)
ns = set([x.name for x in not_removed])
removed = set(names) - ns
self.all_actions.model().add(removed)
if not_removed:
warning_dialog(self, _('Cannot remove'),
_('Cannot remove the actions %s from this location') %
','.join([a.action_spec[0] for a in not_removed]),
show=True)
else:
self.changed_signal.emit()
def move(self, delta, *args):
ci = self.current_actions.currentIndex()
m = self.current_actions.model()
if ci.isValid():
ni = m.move(ci, delta)
if ni is not None:
self.current_actions.setCurrentIndex(ni)
self.current_actions.selectionModel().select(ni,
QItemSelectionModel.ClearAndSelect)
self.changed_signal.emit()
def commit(self):
for am, cm in self.models.values():
cm.commit()
return False
def restore_defaults(self):
for am, cm in self.models.values():
cm.restore_defaults()
am.restore_defaults()
self.changed_signal.emit()
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Toolbar')

View File

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>831</width>
<height>553</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Customize the actions in:</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="3">
<widget class="QComboBox" name="what">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>20</number>
</property>
</widget>
</item>
<item row="1" column="0" rowspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>A&amp;vailable actions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListView" name="all_actions">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="spacing">
<number>10</number>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="2" rowspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Current actions</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListView" name="current_actions">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="spacing">
<number>10</number>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QToolButton" name="action_up_button">
<property name="toolTip">
<string>Move selected action up</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.svg</normaloff>:/images/arrow-up.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="action_down_button">
<property name="toolTip">
<string>Move selected action down</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-down.svg</normaloff>:/images/arrow-down.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="1" column="1" rowspan="2">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QToolButton" name="add_action_button">
<property name="toolTip">
<string>Add selected actions to toolbar</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/forward.svg</normaloff>:/images/forward.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="remove_action_button">
<property name="toolTip">
<string>Remove selected actions from toolbar</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/back.svg</normaloff>:/images/back.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -90,6 +90,7 @@ class SearchBox2(QComboBox):
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.setMinimumContentsLength(25) self.setMinimumContentsLength(25)
self._in_a_search = False self._in_a_search = False
self.tool_tip_text = self.toolTip()
def initialize(self, opt_name, colorize=False, help_text=_('Search')): def initialize(self, opt_name, colorize=False, help_text=_('Search')):
self.as_you_type = config['search_as_you_type'] self.as_you_type = config['search_as_you_type']
@ -100,6 +101,7 @@ class SearchBox2(QComboBox):
self.clear_to_help() self.clear_to_help()
def normalize_state(self): def normalize_state(self):
self.setToolTip(self.tool_tip_text)
if self.help_state: if self.help_state:
self.setEditText('') self.setEditText('')
self.line_edit.setStyleSheet( self.line_edit.setStyleSheet(
@ -112,6 +114,7 @@ class SearchBox2(QComboBox):
self.normal_background) self.normal_background)
def clear_to_help(self): def clear_to_help(self):
self.setToolTip(self.tool_tip_text)
if self.help_state: if self.help_state:
return return
self.help_state = True self.help_state = True
@ -131,6 +134,9 @@ class SearchBox2(QComboBox):
self.clear_to_help() self.clear_to_help()
def search_done(self, ok): def search_done(self, ok):
if isinstance(ok, basestring):
self.setToolTip(ok)
ok = False
if not unicode(self.currentText()).strip(): if not unicode(self.currentText()).strip():
return self.clear_to_help() return self.clear_to_help()
self._in_a_search = ok self._in_a_search = ok

View File

@ -233,7 +233,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
######################### Search Restriction ########################## ######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self) SearchRestrictionMixin.__init__(self)
self.apply_named_search_restriction(db.prefs.get('gui_restriction', '')) self.apply_named_search_restriction(db.prefs['gui_restriction'])
########################### Cover Flow ################################ ########################### Cover Flow ################################
@ -378,7 +378,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.set_window_title() self.set_window_title()
self.apply_named_search_restriction('') # reset restriction to null self.apply_named_search_restriction('') # reset restriction to null
self.saved_searches_changed() # reload the search restrictions combo box self.saved_searches_changed() # reload the search restrictions combo box
self.apply_named_search_restriction(db.prefs.get('gui_restriction', '')) self.apply_named_search_restriction(db.prefs['gui_restriction'])
def set_window_title(self): def set_window_title(self):
self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name()) self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())

View File

@ -319,12 +319,18 @@ class ResultCache(SearchQueryParser):
matches.add(item[0]) matches.add(item[0])
return matches return matches
def get_matches(self, location, query): def get_matches(self, location, query, allow_recursion=True):
matches = set([]) matches = set([])
if query and query.strip(): if query and query.strip():
# get metadata key associated with the search term. Eliminates # get metadata key associated with the search term. Eliminates
# dealing with plurals and other aliases # dealing with plurals and other aliases
location = self.field_metadata.search_term_to_key(location.lower().strip()) location = self.field_metadata.search_term_to_key(location.lower().strip())
if isinstance(location, list):
if allow_recursion:
for loc in location:
matches |= self.get_matches(loc, query, allow_recursion=False)
return matches
raise ParseException(query, len(query), 'Recursive query group detected', self)
# take care of dates special case # take care of dates special case
if location in self.field_metadata and \ if location in self.field_metadata and \

View File

@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
''' '''
The database used to store ebook metadata The database used to store ebook metadata
''' '''
import os, sys, shutil, cStringIO, glob, time, functools, traceback import os, sys, shutil, cStringIO, glob, time, functools, traceback, re
from itertools import repeat from itertools import repeat
from math import floor from math import floor
@ -145,6 +145,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def initialize_dynamic(self): def initialize_dynamic(self):
self.prefs = DBPrefs(self) self.prefs = DBPrefs(self)
defs = self.prefs.defaults
defs['gui_restriction'] = defs['cs_restriction'] = ''
# Migrate saved search and user categories to db preference scheme # Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default): def migrate_preference(key, default):
@ -264,7 +266,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# account for the series index column. Field_metadata knows that # account for the series index column. Field_metadata knows that
# the series index is one larger than the series. If you change # the series index is one larger than the series. If you change
# it here, be sure to change it there as well. # it here, be sure to change it there as well.
self.FIELD_MAP[str(col)+'_s_index'] = base = base+1 self.FIELD_MAP[str(col)+'_index'] = base = base+1
self.field_metadata.set_field_record_index(
self.custom_column_num_map[col]['label']+'_index',
base,
prefer_custom=True)
self.FIELD_MAP['cover'] = base+1 self.FIELD_MAP['cover'] = base+1
self.field_metadata.set_field_record_index('cover', base+1, prefer_custom=False) self.field_metadata.set_field_record_index('cover', base+1, prefer_custom=False)
@ -293,6 +299,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if len(saved_searches().names()): if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches')) tb_cats.add_search_category(label='search', name=_('Searches'))
gst = tweaks['grouped_search_terms']
for t in gst:
try:
self.field_metadata._add_search_terms_to_map(gst[t], [t])
except ValueError:
traceback.print_exc()
self.book_on_device_func = None self.book_on_device_func = None
self.data = ResultCache(self.FIELD_MAP, self.field_metadata) self.data = ResultCache(self.FIELD_MAP, self.field_metadata)
self.search = self.data.search self.search = self.data.search
@ -546,6 +559,43 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return bool(self.conn.get('SELECT id FROM books where title=?', (title,), all=False)) return bool(self.conn.get('SELECT id FROM books where title=?', (title,), all=False))
return False return False
def find_identical_books(self, mi):
fuzzy_title_patterns = [(re.compile(pat), repl) for pat, repl in
[
(r'[\[\](){}<>\'";,:#]', ''),
(r'^(the|a|an) ', ''),
(r'[-._]', ' '),
(r'\s+', ' ')
]
]
def fuzzy_title(title):
title = title.strip().lower()
for pat, repl in fuzzy_title_patterns:
title = pat.sub(repl, title)
return title
identical_book_ids = set([])
if mi.authors:
try:
query = u' and '.join([u'author:"=%s"'%(a.replace('"', '')) for a in
mi.authors])
except ValueError:
return identical_book_ids
try:
book_ids = self.data.parse(query)
except:
import traceback
traceback.print_exc()
return identical_book_ids
for book_id in book_ids:
fbook_title = self.title(book_id, index_is_id=True)
fbook_title = fuzzy_title(fbook_title)
mbook_title = fuzzy_title(mi.title)
if fbook_title == mbook_title:
identical_book_ids.add(book_id)
return identical_book_ids
def has_cover(self, index, index_is_id=False): def has_cover(self, index, index_is_id=False):
id = index if index_is_id else self.id(index) id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg') path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
@ -1677,7 +1727,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
path = path_or_stream path = path_or_stream
return run_plugins_on_import(path, format) return run_plugins_on_import(path, format)
def _add_newbook_tag(self, mi):
tags = prefs['new_book_tags']
if tags:
for tag in [t.strip() for t in tags]:
if tag:
if mi.tags is None:
mi.tags = [tag]
else:
mi.tags.append(tag)
def create_book_entry(self, mi, cover=None, add_duplicates=True): def create_book_entry(self, mi, cover=None, add_duplicates=True):
self._add_newbook_tag(mi)
if not add_duplicates and self.has_book(mi): if not add_duplicates and self.has_book(mi):
return None return None
series_index = 1.0 if mi.series_index is None else mi.series_index series_index = 1.0 if mi.series_index is None else mi.series_index
@ -1716,6 +1777,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
ids = [] ids = []
for path in paths: for path in paths:
mi = metadata.next() mi = metadata.next()
self._add_newbook_tag(mi)
format = formats.next() format = formats.next()
if not add_duplicates and self.has_book(mi): if not add_duplicates and self.has_book(mi):
duplicates.append((path, format, mi)) duplicates.append((path, format, mi))
@ -1754,8 +1816,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return (paths, formats, metadata), len(ids) return (paths, formats, metadata), len(ids)
return None, len(ids) return None, len(ids)
def import_book(self, mi, formats, notify=True, import_hooks=True): def import_book(self, mi, formats, notify=True, import_hooks=True,
apply_import_tags=True):
series_index = 1.0 if mi.series_index is None else mi.series_index series_index = 1.0 if mi.series_index is None else mi.series_index
if apply_import_tags:
self._add_newbook_tag(mi)
if not mi.title: if not mi.title:
mi.title = _('Unknown') mi.title = _('Unknown')
if not mi.authors: if not mi.authors:

View File

@ -36,7 +36,7 @@ class FieldMetadata(dict):
treated as a single term. If not None, it contains a string, and the field treated as a single term. If not None, it contains a string, and the field
is assumed to contain a list of terms separated by that string is assumed to contain a list of terms separated by that string
kind == standard: is a db field. kind == field: is a db field.
kind == category: standard tag category that isn't a field. see news. kind == category: standard tag category that isn't a field. see news.
kind == user: user-defined tag category. kind == user: user-defined tag category.
kind == search: saved-searches category. kind == search: saved-searches category.
@ -239,7 +239,7 @@ class FieldMetadata(dict):
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
'name':None, 'name':None,
'search_terms':[], 'search_terms':['series_index'],
'is_custom':False, 'is_custom':False,
'is_category':False}), 'is_category':False}),
('sort', {'table':None, ('sort', {'table':None,
@ -395,6 +395,18 @@ class FieldMetadata(dict):
'is_editable': is_editable,} 'is_editable': is_editable,}
self._add_search_terms_to_map(key, [key]) self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label] = key self.custom_label_to_key_map[label] = key
if datatype == 'series':
key += '_index'
self._tb_cats[key] = {'table':None, 'column':None,
'datatype':'float', 'is_multiple':False,
'kind':'field', 'name':'',
'search_terms':[key], 'label':label+'_index',
'colnum':None, 'display':{},
'is_custom':False, 'is_category':False,
'link_column':None, 'category_sort':None,
'is_editable': False,}
self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label+'_index'] = key
def remove_custom_fields(self): def remove_custom_fields(self):
for key in self.get_custom_fields(): for key in self.get_custom_fields():
@ -463,9 +475,7 @@ class FieldMetadata(dict):
# ]) # ])
def get_search_terms(self): def get_search_terms(self):
s_keys = [] s_keys = sorted(self._search_term_map.keys())
for v in self._tb_cats.itervalues():
map((lambda x:s_keys.append(x)), v['search_terms'])
for v in self.search_items: for v in self.search_items:
s_keys.append(v) s_keys.append(v)
# if set(s_keys) != self.DEFAULT_LOCATIONS: # if set(s_keys) != self.DEFAULT_LOCATIONS:
@ -476,6 +486,9 @@ class FieldMetadata(dict):
def _add_search_terms_to_map(self, key, terms): def _add_search_terms_to_map(self, key, terms):
if terms is not None: if terms is not None:
for t in terms: for t in terms:
t = t.lower()
if t in self._search_term_map:
raise ValueError('Attempt to add duplicate search term "%s"'%t)
self._search_term_map[t] = key self._search_term_map[t] = key
def search_term_to_key(self, term): def search_term_to_key(self, term):

View File

@ -13,11 +13,11 @@ from lxml import html
from lxml.html.builder import HTML, HEAD, TITLE, LINK, DIV, IMG, BODY, \ from lxml.html.builder import HTML, HEAD, TITLE, LINK, DIV, IMG, BODY, \
OPTION, SELECT, INPUT, FORM, SPAN, TABLE, TR, TD, A, HR OPTION, SELECT, INPUT, FORM, SPAN, TABLE, TR, TD, A, HR
from calibre.library.server.utils import strftime from calibre.library.server.utils import strftime, format_tag_string
from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import fmt_sidx
from calibre.constants import __appname__ from calibre.constants import __appname__
from calibre import human_readable from calibre import human_readable
from calibre.utils.date import utcfromtimestamp from calibre.utils.date import utcfromtimestamp, format_date
def CLASS(*args, **kwargs): # class is a reserved word in Python def CLASS(*args, **kwargs): # class is a reserved word in Python
kwargs['class'] = ' '.join(args) kwargs['class'] = ' '.join(args)
@ -85,7 +85,7 @@ def build_navigation(start, num, total, url_base): # {{{
# }}} # }}}
def build_index(books, num, search, sort, order, start, total, url_base): def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
logo = DIV(IMG(src='/static/calibre.png', alt=__appname__), id='logo') logo = DIV(IMG(src='/static/calibre.png', alt=__appname__), id='logo')
search_box = build_search_box(num, search, sort, order) search_box = build_search_box(num, search, sort, order)
@ -123,10 +123,16 @@ def build_index(books, num, search, sort, order, start, total, url_base):
series = u'[%s - %s]'%(book['series'], book['series_index']) \ series = u'[%s - %s]'%(book['series'], book['series_index']) \
if book['series'] else '' if book['series'] else ''
tags = u'[%s]'%book['tags'] if book['tags'] else '' tags = u'Tags=[%s]'%book['tags'] if book['tags'] else ''
text = u'\u202f%s %s by %s - %s - %s %s' % (book['title'], series, ctext = ''
book['authors'], book['size'], book['timestamp'], tags) for key in CKEYS:
val = book.get(key, None)
if val:
ctext += '%s=[%s] '%tuple(val.split(':#:'))
text = u'\u202f%s %s by %s - %s - %s %s %s' % (book['title'], series,
book['authors'], book['size'], book['timestamp'], tags, ctext)
if last is None: if last is None:
data.text = text data.text = text
@ -150,7 +156,7 @@ def build_index(books, num, search, sort, order, start, total, url_base):
class MobileServer(object): class MobileServer(object):
'A view optimized for browsers in mobile devices' 'A view optimized for browsers in mobile devices'
MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian|HD2)') MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian|HD2|Kindle)')
def add_routes(self, connect): def add_routes(self, connect):
connect('mobile', '/mobile', self.mobile) connect('mobile', '/mobile', self.mobile)
@ -189,6 +195,10 @@ class MobileServer(object):
if sort is not None: if sort is not None:
self.sort(items, sort, (order.lower().strip() == 'ascending')) self.sort(items, sort, (order.lower().strip() == 'ascending'))
CFM = self.db.field_metadata
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
books = [] books = []
for record in items[(start-1):(start-1)+num]: for record in items[(start-1):(start-1)+num]:
book = {'formats':record[FM['formats']], 'size':record[FM['size']]} book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
@ -203,12 +213,37 @@ class MobileServer(object):
book['authors'] = authors book['authors'] = authors
book['series_index'] = fmt_sidx(float(record[FM['series_index']])) book['series_index'] = fmt_sidx(float(record[FM['series_index']]))
book['series'] = record[FM['series']] book['series'] = record[FM['series']]
book['tags'] = record[FM['tags']] book['tags'] = format_tag_string(record[FM['tags']], ',')
book['title'] = record[FM['title']] book['title'] = record[FM['title']]
for x in ('timestamp', 'pubdate'): for x in ('timestamp', 'pubdate'):
book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]]) book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
book['id'] = record[FM['id']] book['id'] = record[FM['id']]
books.append(book) books.append(book)
for key in CKEYS:
def concat(name, val):
return '%s:#:%s'%(name, unicode(val))
val = record[CFM[key]['rec_index']]
if val:
datatype = CFM[key]['datatype']
if datatype in ['comments']:
continue
name = CFM[key]['name']
if datatype == 'text' and CFM[key]['is_multiple']:
book[key] = concat(name, format_tag_string(val, '|'))
elif datatype == 'series':
book[key] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
book[key] = concat(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy')))
elif datatype == 'bool':
if val:
book[key] = concat(name, __builtin__._('Yes'))
else:
book[key] = concat(name, __builtin__._('No'))
else:
book[key] = concat(name, val)
updated = self.db.last_modified() updated = self.db.last_modified()
cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8' cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8'
@ -218,7 +253,7 @@ class MobileServer(object):
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num) url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
return html.tostring(build_index(books, num, search, sort, order, return html.tostring(build_index(books, num, search, sort, order,
start, len(ids), url_base), start, len(ids), url_base, CKEYS),
encoding='utf-8', include_meta_content_type=True, encoding='utf-8', include_meta_content_type=True,
pretty_print=True) pretty_print=True)

View File

@ -19,6 +19,7 @@ from calibre.ebooks.metadata import fmt_sidx
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre import guess_type from calibre import guess_type
from calibre.utils.ordered_dict import OrderedDict from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.date import format_date
BASE_HREFS = { BASE_HREFS = {
0 : '/stanza', 0 : '/stanza',
@ -130,7 +131,7 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated):
link link
) )
def ACQUISITION_ENTRY(item, version, FM, updated): def ACQUISITION_ENTRY(item, version, FM, updated, CFM, CKEYS):
title = item[FM['title']] title = item[FM['title']]
if not title: if not title:
title = _('Unknown') title = _('Unknown')
@ -153,6 +154,21 @@ def ACQUISITION_ENTRY(item, version, FM, updated):
extra.append(_('SERIES: %s [%s]<br />')%\ extra.append(_('SERIES: %s [%s]<br />')%\
(series, (series,
fmt_sidx(float(item[FM['series_index']])))) fmt_sidx(float(item[FM['series_index']]))))
for key in CKEYS:
val = item[CFM[key]['rec_index']]
if val is not None:
name = CFM[key]['name']
datatype = CFM[key]['datatype']
if datatype == 'text' and CFM[key]['is_multiple']:
extra.append('%s: %s<br />'%(name, ', '.join(val.split('|'))))
elif datatype == 'series':
extra.append('%s: %s [%s]<br />'%(name, val,
fmt_sidx(item[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
extra.append('%s: %s<br />'%(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy'))))
else:
extra.append('%s: %s <br />' % (CFM[key]['name'], val))
comments = item[FM['comments']] comments = item[FM['comments']]
if comments: if comments:
comments = comments_to_html(comments) comments = comments_to_html(comments)
@ -260,10 +276,14 @@ class NavFeed(Feed):
class AcquisitionFeed(NavFeed): class AcquisitionFeed(NavFeed):
def __init__(self, updated, id_, items, offsets, page_url, up_url, version, def __init__(self, updated, id_, items, offsets, page_url, up_url, version,
FM): FM, CFM):
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url) NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
for item in items: for item in items:
self.root.append(ACQUISITION_ENTRY(item, version, FM, updated)) self.root.append(ACQUISITION_ENTRY(item, version, FM, updated,
CFM, CKEYS))
class CategoryFeed(NavFeed): class CategoryFeed(NavFeed):
@ -360,7 +380,7 @@ class OPDSServer(object):
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
cherrypy.response.headers['Content-Type'] = 'application/atom+xml;profile=opds-catalog' cherrypy.response.headers['Content-Type'] = 'application/atom+xml;profile=opds-catalog'
return str(AcquisitionFeed(updated, id_, items, offsets, return str(AcquisitionFeed(updated, id_, items, offsets,
page_url, up_url, version, self.db.FIELD_MAP)) page_url, up_url, version, self.db.FIELD_MAP, self.db.field_metadata))
def opds_search(self, query=None, version=0, offset=0): def opds_search(self, query=None, version=0, offset=0):
try: try:
@ -568,7 +588,10 @@ class OPDSServer(object):
(_('Newest'), _('Date'), 'Onewest'), (_('Newest'), _('Date'), 'Onewest'),
(_('Title'), _('Title'), 'Otitle'), (_('Title'), _('Title'), 'Otitle'),
] ]
for category in categories: def getter(x):
return category_meta[x]['name'].lower()
for category in sorted(categories,
cmp=lambda x,y: cmp(getter(x), getter(y))):
if len(categories[category]) == 0: if len(categories[category]) == 0:
continue continue
if category == 'formats': if category == 'formats':

View File

@ -11,6 +11,7 @@ import cherrypy
from calibre import strftime as _strftime, prints from calibre import strftime as _strftime, prints
from calibre.utils.date import now as nowf from calibre.utils.date import now as nowf
from calibre.utils.config import tweaks
def expose(func): def expose(func):
@ -43,4 +44,14 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
except: except:
return _strftime(fmt, nowf().timetuple()) return _strftime(fmt, nowf().timetuple())
def format_tag_string(tags, sep):
MAX = tweaks['max_content_server_tags_shown']
if tags:
tlist = [t.strip() for t in tags.split(sep)]
else:
tlist = []
tlist.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
if len(tlist) > MAX:
tlist = tlist[:MAX]+['...']
return u'%s'%(', '.join(tlist)) if tlist else ''

View File

@ -11,10 +11,11 @@ import cherrypy
from lxml.builder import ElementMaker from lxml.builder import ElementMaker
from lxml import etree from lxml import etree
from calibre.library.server.utils import strftime from calibre.library.server.utils import strftime, format_tag_string
from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import fmt_sidx
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre import isbytestring from calibre import isbytestring
from calibre.utils.date import format_date
E = ElementMaker() E = ElementMaker()
@ -83,9 +84,44 @@ class XMLServer(object):
for x in ('isbn', 'formats', 'series', 'tags', 'publisher', for x in ('isbn', 'formats', 'series', 'tags', 'publisher',
'comments'): 'comments'):
y = record[FM[x]] y = record[FM[x]]
if x == 'tags':
y = format_tag_string(y, ',')
kwargs[x] = serialize(y) if y else '' kwargs[x] = serialize(y) if y else ''
c = kwargs.pop('comments') c = kwargs.pop('comments')
CFM = self.db.field_metadata
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
custcols = []
for key in CKEYS:
def concat(name, val):
return '%s:#:%s'%(name, unicode(val))
val = record[CFM[key]['rec_index']]
if val:
datatype = CFM[key]['datatype']
if datatype in ['comments']:
continue
k = str('CF_'+key[1:])
name = CFM[key]['name']
custcols.append(k)
if datatype == 'text' and CFM[key]['is_multiple']:
kwargs[k] = concat(name, format_tag_string(val,'|'))
elif datatype == 'series':
kwargs[k] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
kwargs[k] = concat(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy')))
elif datatype == 'bool':
if val:
kwargs[k] = concat(name, __builtin__._('Yes'))
else:
kwargs[k] = concat(name, __builtin__._('No'))
else:
kwargs[k] = concat(name, val)
kwargs['custcols'] = ','.join(custcols)
books.append(E.book(c, **kwargs)) books.append(E.book(c, **kwargs))
updated = self.db.last_modified() updated = self.db.last_modified()

View File

@ -223,7 +223,7 @@ the server has IP address 63.45.128.5, in the browser, you would type::
http://63.45.128.5:8080 http://63.45.128.5:8080
Some devices, like the Kindle, do not allow you to access port 8080 (the default port on which the content Some devices, like the Kindle (1/2/DX), do not allow you to access port 8080 (the default port on which the content
server runs. In that case, change the port in the |app| Preferences to 80. (On some operating systems, server runs. In that case, change the port in the |app| Preferences to 80. (On some operating systems,
you may not be able to run the server on a port number less than 1024 because of security settings. In you may not be able to run the server on a port number less than 1024 because of security settings. In
this case the simplest solution is to adjust your router to forward requests on port 80 to port 8080). this case the simplest solution is to adjust your router to forward requests on port 80 to port 8080).

View File

@ -166,7 +166,7 @@ Search & Sort
The Search & Sort section allows you to perform several powerful actions on your book collections. The Search & Sort section allows you to perform several powerful actions on your book collections.
* You can sort them by title, author, date, rating etc. by clicking on the column titles. * You can sort them by title, author, date, rating etc. by clicking on the column titles. You can also sub-sort (i.e. sort on multiple columns). For example, if you click on the title column and then the author column, the book will be sorted by author and then all the entries for the same author will be sorted by title.
* You can search for a particular book or set of books using the search bar. More on that below. * You can search for a particular book or set of books using the search bar. More on that below.
@ -212,9 +212,10 @@ metadata.
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
clicking the button |sbi|. clicking the button |sbi|.
Available fields for searching are: ``tag, title, author, publisher, series, rating, cover, comments, format, Available fields for searching are: ``tag, title, author, publisher, series, series_index, rating, cover,
isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the ``ondevice`` field comments, format, isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the
becomes available. To find the search name for a custom column, hover your mouse over the column header. ``ondevice`` field becomes available. To find the search name for a custom column, hover your mouse over the
column header.
The syntax for searching for dates is:: The syntax for searching for dates is::
@ -223,9 +224,8 @@ The syntax for searching for dates is::
pubdate:=2009 Will find all books published in 2009 pubdate:=2009 Will find all books published in 2009
If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy
locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009. locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009. Some
special date strings are available. The string ``today`` translates to today's date, whatever it is. The
Some special date strings are available. The string ``today`` translates to today's date, whatever it is. The
strings `yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare strings `yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare
to a date some number of days ago, for example: date:>10daysago, date:<=45daysago. to a date some number of days ago, for example: date:>10daysago, date:<=45daysago.
@ -234,9 +234,15 @@ You can search for books that have a format of a certain size like this::
size:>1.1M Will find books with a format larger than 1.1MB size:>1.1M Will find books with a format larger than 1.1MB
size:<=1K Will find books with a format smaller than 1KB size:<=1K Will find books with a format smaller than 1KB
Dates and numeric fields support the operators ``=`` (equals), ``>`` (greater than), ``>=`` (greater than or Dates and numeric fields support the relational operators ``=`` (equals), ``>`` (greater than), ``>=``
equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to). Rating fields are (greater than or equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to).
considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3 or higher. Rating fields are considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3
or higher.
Series indices are searchable. For the standard series, the search name is 'series_index'. For
custom series columns, use the column search name followed by _index. For example, to search the indices for a
custom series column named ``#my_series``, you would use the search name ``#my_series_index``.
Series indices are numbers, so you can use the relational operators described above.
The special field ``search`` is used for saved searches. So if you save a search with the name The special field ``search`` is used for saved searches. So if you save a search with the name
"My spouse's books" you can enter ``search:"My spouse's books"`` in the search bar to reuse the saved "My spouse's books" you can enter ``search:"My spouse's books"`` in the search bar to reuse the saved
@ -310,6 +316,70 @@ Jobs
The Jobs panel shows you the number of currently running jobs. Jobs are tasks that run in a separate process, they include converting ebooks and talking to your reader device. You can click on the jobs panel to access the list of jobs. Once a job has completed, by double-clicking it in the list, you can see a detailed log from that job. This is useful to debug jobs that may not have completed successfully. The Jobs panel shows you the number of currently running jobs. Jobs are tasks that run in a separate process, they include converting ebooks and talking to your reader device. You can click on the jobs panel to access the list of jobs. Once a job has completed, by double-clicking it in the list, you can see a detailed log from that job. This is useful to debug jobs that may not have completed successfully.
Keyboard Shortcuts
---------------------
Calibre has several keyboard shortcuts to save you time and mouse movement. These shortcuts are active in the book list view (when you're not editing the details of a particular book), and most of them affect the title you have selected. The |app| e-book viewer has its own shortcuts, which can be customised by clicking the Preferences button in the viewer.
.. note::
Note: The Calibre keyboard shortcuts do not require a modifier key (Command, Option, Control etc.), unless specifically noted. You only need to press the letter key, e.g. E to edit.
.. list-table:: Keyboard Shortcuts
:widths: 10 100
:header-rows: 1
* - Keyboard Shortcut
- Action
* - :kbd:`A`
- Add Books
* - :kbd:`C`
- Convert selected Books
* - :kbd:`D`
- Send to device
* - :kbd:`Del`
- Remove selected Books
* - :kbd:`E`
- Edit metadata of selected books
* - :kbd:`I`
- Show book details
* - :kbd:`M`
- Merge selected records
* - :kbd:`O`
- Open containing folder
* - :kbd:`S`
- Save to Disk
* - :kbd:`V`
- View
* - :kbd:`Alt+V/Cmd+V in OS X`
- View specific format
* - :kbd:`Alt+Shift+J`
- Toggle jobs list
* - :kbd:`Alt+Shift+B`
- Toggle Cover Browser
* - :kbd:`Alt+Shift+T`
- Toggle Tag Browser
* - :kbd:`Alt+A`
- Show books by the Same author as the current book
* - :kbd:`Alt+T`
- Show books with the same tags as current book
* - :kbd:`Alt+P`
- Show books by the same publisher as current book
* - :kbd:`Alt+Shift+S`
- Show books in the same series as current book
* - :kbd:`/, Ctrl+F`
- Focus the search bar
* - :kbd:`Ctrl+D`
- Download metadata and shortcuts
* - :kbd:`Ctrl+R`
- Restart calibre
* - :kbd:`Ctrl+Q`
- Quit calibre

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,14 +13,12 @@ from optparse import OptionParser as _OptionParser
from optparse import IndentedHelpFormatter from optparse import IndentedHelpFormatter
from collections import defaultdict from collections import defaultdict
from calibre.constants import terminal_controller, config_dir, \ from calibre.constants import terminal_controller, config_dir, CONFIG_DIR_MODE, \
__appname__, __version__, __author__ __appname__, __version__, __author__
from calibre.utils.lock import LockError, ExclusiveFile from calibre.utils.lock import LockError, ExclusiveFile
plugin_dir = os.path.join(config_dir, 'plugins') plugin_dir = os.path.join(config_dir, 'plugins')
CONFIG_DIR_MODE = 0700
def make_config_dir(): def make_config_dir():
if not os.path.exists(plugin_dir): if not os.path.exists(plugin_dir):
os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE) os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE)
@ -719,6 +717,7 @@ def _prefs():
c.add_opt('add_formats_to_existing', default=False, c.add_opt('add_formats_to_existing', default=False,
help=_('Add new formats to existing book records')) help=_('Add new formats to existing book records'))
c.add_opt('installation_uuid', default=None, help='Installation UUID') c.add_opt('installation_uuid', default=None, help='Installation UUID')
c.add_opt('new_book_tags', default=[], help=_('Tags to apply to books added to the library'))
# these are here instead of the gui preferences because calibredb and # these are here instead of the gui preferences because calibredb and
# calibre server can execute searches # calibre server can execute searches

View File

@ -81,12 +81,7 @@ def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30,
for x in to: for x in to:
return sendmail_direct(from_, x, msg, timeout, localhost, verbose) return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
import smtplib import smtplib
class SMTP_SSL(smtplib.SMTP_SSL): # Workaround for bug in smtplib.py cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
def _get_socket(self, host, port, timeout):
smtplib.SMTP_SSL._get_socket(self, host, port, timeout)
return self.sock
cls = smtplib.SMTP if encryption == 'TLS' else SMTP_SSL
timeout = None # Non-blocking sockets sometimes don't work timeout = None # Non-blocking sockets sometimes don't work
port = int(port) port = int(port)
s = cls(timeout=timeout, local_hostname=localhost) s = cls(timeout=timeout, local_hostname=localhost)

View File

@ -193,9 +193,11 @@ class NavBarTemplate(Template):
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_70', navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_70',
style='text-align:'+align)) style='text-align:'+align))
if bottom: if bottom:
if not url.startswith('file://'):
navbar.append(HR()) navbar.append(HR())
text = 'This article was downloaded by ' text = 'This article was downloaded by '
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left') p = PT(text, STRONG(__appname__), A(url, href=url),
style='text-align:left; max-width: 100%; overflow: hidden;')
p[0].tail = ' from ' p[0].tail = ' from '
navbar.append(p) navbar.append(p)
navbar.append(BR()) navbar.append(BR())