mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merged 07.17 changes
This commit is contained in:
commit
572633bac1
@ -4,6 +4,63 @@
|
||||
# for important features/bug fixes.
|
||||
# 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
|
||||
date: 2010-08-27
|
||||
|
||||
|
@ -50,7 +50,7 @@ function render_book(book) {
|
||||
var comments = $.trim(book.text()).replace(/\n\n/, '<br/>');
|
||||
var formats = new Array();
|
||||
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(",");
|
||||
if (formats.length > 0) {
|
||||
for (i=0; i < formats.length; i++) {
|
||||
@ -59,7 +59,14 @@ function render_book(book) {
|
||||
title = title.slice(0, title.length-2);
|
||||
title += ' ({0} MB) '.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 += '<div class="comments">{0}</div>'.format(comments)
|
||||
// Render authors cell
|
||||
@ -290,7 +297,7 @@ function layout() {
|
||||
}
|
||||
|
||||
$(function() {
|
||||
// document is ready
|
||||
// document is ready
|
||||
create_table_headers();
|
||||
|
||||
// Setup widgets
|
||||
|
@ -90,4 +90,27 @@ save_template_title_series_sorting = 'library_order'
|
||||
# Examples:
|
||||
# auto_connect_to_folder = 'C:\\Users\\someone\\Desktop\\testlib'
|
||||
# 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
|
||||
|
||||
|
BIN
resources/images/news/fstream.png
Normal file
BIN
resources/images/news/fstream.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 636 B |
BIN
resources/images/news/la_jornada.png
Normal file
BIN
resources/images/news/la_jornada.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 943 B |
@ -18,7 +18,7 @@ class Clarin(BasicNewsRecipe):
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
encoding = 'utf8'
|
||||
language = 'es_AR'
|
||||
language = 'es'
|
||||
publication_type = 'newspaper'
|
||||
INDEX = 'http://www.clarin.com'
|
||||
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'
|
||||
|
@ -20,7 +20,7 @@ class Europasur(BasicNewsRecipe):
|
||||
delay = 2
|
||||
no_stylesheets = True
|
||||
encoding = 'cp1252'
|
||||
language = 'es_ES'
|
||||
language = 'es'
|
||||
publication_type = 'newspaper'
|
||||
extra_css = """ body{font-family: Verdana,Arial,Helvetica,sans-serif}
|
||||
h2{font-family: Georgia,Times New Roman,Times,serif}
|
||||
|
64
resources/recipes/fstream.recipe
Normal file
64
resources/recipes/fstream.recipe
Normal 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
|
||||
|
||||
|
||||
|
||||
|
69
resources/recipes/hoy.recipe
Normal file
69
resources/recipes/hoy.recipe
Normal 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
|
||||
|
@ -1,120 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__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
|
||||
'''
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
|
||||
|
||||
import re
|
||||
class LaJornada_mx(BasicNewsRecipe):
|
||||
title = 'La Jornada (Mexico)'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Noticias del diario mexicano La Jornada'
|
||||
publisher = 'DEMOS, Desarrollo de Medios, S.A. de C.V.'
|
||||
category = 'news, Mexico'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'utf8'
|
||||
use_embedded_content = False
|
||||
language = 'es'
|
||||
remove_empty_feeds = True
|
||||
cover_url = strftime("http://www.jornada.unam.mx/%Y/%m/%d/planitas/portadita.jpg")
|
||||
masthead_url = 'http://www.jornada.unam.mx/v7.0/imagenes/la-jornada-trans.png'
|
||||
extra_css = """
|
||||
body{font-family: "Times New Roman",serif }
|
||||
.cabeza{font-size: xx-large; font-weight: bold }
|
||||
.credito-articulo{font-size: 1.3em}
|
||||
"""
|
||||
|
||||
class LaJornada(BasicNewsRecipe):
|
||||
title = u'La Jornada'
|
||||
language = 'es'
|
||||
oldest_article = 1
|
||||
__author__ = 'rogeliodh'
|
||||
max_articles_per_feed = 100
|
||||
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
|
||||
extra_css = ' .series{ \
|
||||
border-bottom: 1px solid #626366; \
|
||||
font-weight: bold; \
|
||||
} \
|
||||
.sumario{ \
|
||||
font-weight: bold; \
|
||||
margin-top: 2em; \
|
||||
text-align: center \
|
||||
} \
|
||||
p.sumario{ \
|
||||
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; \
|
||||
} \
|
||||
'
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
preprocess_regexps = [
|
||||
# Remove capitalized initial letter on some articles (editorial)
|
||||
(re.compile(r'<div class="inicial">(.*)</div><p class="s-s">', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: match.group(1)),
|
||||
# Cartons section uses a class instead of a div to identify the main content. Change it.
|
||||
(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)
|
||||
(re.compile(r'<link rel="alternate".*?/>', re.DOTALL|re.IGNORECASE),
|
||||
lambda match: ''),
|
||||
]
|
||||
|
||||
INDEX = 'http://www.jornada.unam.mx/rss/edicion.xml'
|
||||
feeds = [
|
||||
(u'Opinion','http://www.jornada.unam.mx/rss/opinion.xml'),
|
||||
(u'Cartones','http://www.jornada.unam.mx/rss/cartones.xml'),
|
||||
(u'Política','http://www.jornada.unam.mx/rss/politica.xml'),
|
||||
(u'Economía','http://www.jornada.unam.mx/rss/economia.xml'),
|
||||
(u'Mundo','http://www.jornada.unam.mx/rss/mundo.xml'),
|
||||
(u'Estados','http://www.jornada.unam.mx/rss/estados.xml'),
|
||||
(u'Capital','http://www.jornada.unam.mx/rss/capital.xml'),
|
||||
(u'Sociedad','http://www.jornada.unam.mx/rss/sociedad.xml'),
|
||||
(u'Ciencias','http://www.jornada.unam.mx/rss/ciencias.xml'),
|
||||
(u'Cultura','http://www.jornada.unam.mx/rss/cultura.xml'),
|
||||
(u'Gastronomia','http://www.jornada.unam.mx/rss/gastronomia.xml'),
|
||||
(u'Espectáculos','http://www.jornada.unam.mx/rss/espectaculos.xml'),
|
||||
(u'Deportes','http://www.jornada.unam.mx/rss/deportes.xml'),
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':['documentContent','cabeza','sumarios','text']})
|
||||
,dict(name='div', attrs={'id':'renderComments'})
|
||||
]
|
||||
remove_tags = [dict(name='div', attrs={'class':'buttonbar'})]
|
||||
|
||||
def get_cover_url(self):
|
||||
'''
|
||||
Cover URL is http://www.jornada.unam.mx/YYYY/MM/DD/portada.pdf
|
||||
'''
|
||||
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'
|
||||
feeds = [
|
||||
(u'Ultimas noticias' , u'http://www.jornada.unam.mx/ultimas/news/RSS' )
|
||||
,(u'Opinion' , u'http://www.jornada.unam.mx/rss/opinion.xml' )
|
||||
,(u'Politica' , u'http://www.jornada.unam.mx/rss/politica.xml' )
|
||||
,(u'Economia' , u'http://www.jornada.unam.mx/rss/economia.xml' )
|
||||
,(u'Mundo' , u'http://www.jornada.unam.mx/rss/mundo.xml' )
|
||||
,(u'Estados' , u'http://www.jornada.unam.mx/rss/estados.xml' )
|
||||
,(u'Capital' , u'http://www.jornada.unam.mx/rss/capital.xml' )
|
||||
,(u'Sociedad y justicia' , u'http://www.jornada.unam.mx/rss/sociedad.xml' )
|
||||
,(u'Ciencias' , u'http://www.jornada.unam.mx/rss/ciencias.xml' )
|
||||
,(u'Cultura' , u'http://www.jornada.unam.mx/rss/cultura.xml' )
|
||||
,(u'Gastronomia' , u'http://www.jornada.unam.mx/rss/gastronomia.xml' )
|
||||
,(u'Espectaculos' , u'http://www.jornada.unam.mx/rss/espectaculos.xml' )
|
||||
,(u'Deportes' , u'http://www.jornada.unam.mx/rss/deportes.xml' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return soup
|
||||
|
||||
return cover_url
|
||||
|
47
resources/recipes/milenio.recipe
Normal file
47
resources/recipes/milenio.recipe
Normal 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
|
@ -102,7 +102,6 @@ class PeriodicalNameHere(BasicNewsRecipe):
|
||||
|
||||
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))
|
||||
|
||||
older_section_dates = soup.findAll(True, attrs={'class':'maindateline'})
|
||||
for older_section in older_section_dates :
|
||||
|
30
resources/recipes/winnipeg_free_press.recipe
Normal file
30
resources/recipes/winnipeg_free_press.recipe
Normal 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'}),
|
||||
]
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.16'
|
||||
__version__ = '0.7.17'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
@ -84,6 +84,9 @@ if plugins is None:
|
||||
# }}}
|
||||
|
||||
# config_dir {{{
|
||||
|
||||
CONFIG_DIR_MODE = 0700
|
||||
|
||||
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
|
||||
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
|
||||
elif iswindows:
|
||||
@ -98,7 +101,11 @@ elif isosx:
|
||||
else:
|
||||
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
|
||||
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'
|
||||
import tempfile, atexit
|
||||
config_dir = tempfile.mkdtemp(prefix='calibre-config-')
|
||||
|
@ -391,6 +391,8 @@ class PreferencesPlugin(Plugin): # {{{
|
||||
#: The category this plugin should be in
|
||||
category = None
|
||||
|
||||
#: The category name displayed to the user for this plugin
|
||||
gui_category = None
|
||||
#: The name displayed to the user for this plugin
|
||||
gui_name = None
|
||||
|
||||
|
@ -679,12 +679,40 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
class LookAndFeel(PreferencesPlugin):
|
||||
name = 'Look & Feel'
|
||||
gui_name = _('Look and Feel')
|
||||
category = _('Interface')
|
||||
category = 'Interface'
|
||||
gui_category = _('Interface')
|
||||
category_order = 1
|
||||
name_order = 1
|
||||
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]
|
||||
|
||||
#}}}
|
||||
|
||||
|
@ -41,7 +41,7 @@ class ANDROID(USBMS):
|
||||
0x502 : { 0x3203 : [0x0100]},
|
||||
|
||||
# Dell
|
||||
0x413c : { 0xb007 : [0x0100]},
|
||||
0x413c : { 0xb007 : [0x0100, 0x0224]},
|
||||
|
||||
# Eken?
|
||||
0x040d : { 0x0851 : [0x0001]},
|
||||
|
@ -50,6 +50,8 @@ class JETBOOK(USBMS):
|
||||
|
||||
def filename_callback(self, fname, mi):
|
||||
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 = title.replace(' ', '_')
|
||||
au = mi.format_authors()
|
||||
|
@ -23,7 +23,7 @@ class Book(MetaInformation):
|
||||
'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, '')
|
||||
self.device_collections = []
|
||||
@ -42,10 +42,8 @@ class Book(MetaInformation):
|
||||
else:
|
||||
self.authors = [authors]
|
||||
self.mime = mime
|
||||
try:
|
||||
self.size = os.path.getsize(self.path)
|
||||
except OSError:
|
||||
self.size = 0
|
||||
|
||||
self.size = size # will be set later if None
|
||||
try:
|
||||
if ContentType == '6':
|
||||
self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f")
|
||||
|
@ -94,19 +94,19 @@ class KOBO(USBMS):
|
||||
|
||||
idx = bl_cache.get(lpath, None)
|
||||
if idx is not None:
|
||||
bl_cache[lpath] = None
|
||||
if ImageID is not None:
|
||||
imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed')
|
||||
#print "Image name Normalized: " + imagename
|
||||
if imagename is not None:
|
||||
bl[idx].thumbnail = ImageWrapper(imagename)
|
||||
bl_cache[lpath] = None
|
||||
if ContentType != '6':
|
||||
if self.update_metadata_item(bl[idx]):
|
||||
# print 'update_metadata_item returned true'
|
||||
changed = True
|
||||
bl[idx].device_collections = playlist_map.get(lpath, [])
|
||||
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'
|
||||
if bl.add_book(book, replace_metadata=False):
|
||||
changed = True
|
||||
@ -316,10 +316,10 @@ class KOBO(USBMS):
|
||||
lpath = lpath[1:]
|
||||
#print "path: " + lpath
|
||||
#book = self.book_class(prefix, lpath, other=info)
|
||||
lpath = self.normalize_path(prefix + lpath)
|
||||
book = Book(prefix, lpath, '', '', '', '', '', '', other=info)
|
||||
if book.size is None:
|
||||
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)
|
||||
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)
|
||||
|
||||
@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
|
||||
|
@ -196,8 +196,7 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
|
||||
try:
|
||||
new_cdata = open(mi.cover, 'rb').read()
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
pass
|
||||
if new_cdata and raster_cover:
|
||||
try:
|
||||
cpath = posixpath.join(posixpath.dirname(reader.opf_path),
|
||||
|
@ -62,7 +62,16 @@ class HTMLTOCAdder(object):
|
||||
|
||||
def __call__(self, oeb, context):
|
||||
if 'toc' in oeb.guide:
|
||||
return
|
||||
# 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
|
||||
else:
|
||||
oeb.guide.remove('toc')
|
||||
if not getattr(getattr(oeb, 'toc', False), 'nodes', False):
|
||||
return
|
||||
oeb.logger.info('Generating in-line TOC...')
|
||||
|
@ -5,15 +5,16 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
import os, shutil
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QMenu, Qt
|
||||
from PyQt4.Qt import QMenu, Qt, QInputDialog
|
||||
|
||||
from calibre import isbytestring
|
||||
from calibre.constants import filesystem_encoding
|
||||
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
|
||||
|
||||
class LibraryUsageStats(object):
|
||||
@ -66,6 +67,13 @@ class LibraryUsageStats(object):
|
||||
loc = loc[:-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):
|
||||
|
||||
@ -80,7 +88,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
type=Qt.QueuedConnection)
|
||||
|
||||
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')
|
||||
self.action_choose.triggered.connect(self.choose_library,
|
||||
type=Qt.QueuedConnection)
|
||||
@ -90,7 +98,13 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
|
||||
self.quick_menu = QMenu(_('Quick switch'))
|
||||
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 = []
|
||||
for i in range(5):
|
||||
ac = self.create_action(spec=('', None, None, None),
|
||||
@ -123,9 +137,15 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
ac.setVisible(False)
|
||||
self.quick_menu.clear()
|
||||
self.qs_locations = [i[1] for i in locations]
|
||||
self.rename_menu.clear()
|
||||
self.delete_menu.clear()
|
||||
for name, loc in locations:
|
||||
self.quick_menu.addAction(name, Dispatcher(partial(self.switch_requested,
|
||||
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)]):
|
||||
name, loc = x
|
||||
@ -134,12 +154,50 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
ac.setVisible(True)
|
||||
|
||||
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):
|
||||
enabled = loc == 'library'
|
||||
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):
|
||||
if not self.change_library_allowed():
|
||||
return
|
||||
|
@ -5,6 +5,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
from functools import partial
|
||||
from threading import Thread
|
||||
|
||||
@ -13,6 +14,7 @@ from PyQt4.Qt import QMenu, QToolButton
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.gui2 import error_dialog, Dispatcher
|
||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||
from calibre.utils.config import prefs, tweaks
|
||||
|
||||
class Worker(Thread):
|
||||
|
||||
@ -38,6 +40,13 @@ class Worker(Thread):
|
||||
|
||||
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):
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
newdb = LibraryDatabase2(self.loc)
|
||||
@ -49,12 +58,19 @@ class Worker(Thread):
|
||||
else: fmts = fmts.split(',')
|
||||
paths = [self.db.format_abspath(x, fmt, index_is_id=True) for fmt in
|
||||
fmts]
|
||||
newdb.import_book(mi, paths, notify=False, import_hooks=False)
|
||||
co = self.db.conversion_options(x, 'PIPE')
|
||||
if co is not None:
|
||||
newdb.set_conversion_options(x, 'PIPE', co)
|
||||
|
||||
|
||||
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')
|
||||
if co is not None:
|
||||
newdb.set_conversion_options(x, 'PIPE', co)
|
||||
|
||||
|
||||
class CopyToLibraryAction(InterfaceAction):
|
||||
|
@ -21,7 +21,8 @@ class SimilarBooksAction(InterfaceAction):
|
||||
m = QMenu(self.gui)
|
||||
for text, icon, target, shortcut in [
|
||||
(_('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 with the same tags'), 'tags.svg', 'tag', _('Alt+T')),]:
|
||||
ac = self.create_action(spec=(text, icon, None, shortcut),
|
||||
|
@ -1,7 +1,7 @@
|
||||
'''
|
||||
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 threading import Thread
|
||||
|
||||
@ -94,14 +94,6 @@ class DBAdder(Thread): # {{{
|
||||
self.daemon = True
|
||||
self.input_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([])
|
||||
|
||||
def run(self):
|
||||
@ -138,33 +130,6 @@ class DBAdder(Thread): # {{{
|
||||
fmts[-1] = fmt
|
||||
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):
|
||||
formats = self.ids.pop(id)
|
||||
@ -191,7 +156,7 @@ class DBAdder(Thread): # {{{
|
||||
orig_formats = formats
|
||||
formats = [f for f in formats if not f.lower().endswith('.opf')]
|
||||
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
|
||||
self.merged_books.add(mi.title)
|
||||
|
@ -457,6 +457,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.priority.setCurrentIndex(p)
|
||||
self.priority.setVisible(iswindows)
|
||||
self.priority_label.setVisible(iswindows)
|
||||
self.new_book_tags.setText(', '.join(prefs['new_book_tags']))
|
||||
self._plugin_model = PluginModel()
|
||||
self.plugin_view.setModel(self._plugin_model)
|
||||
self.plugin_view.setStyleSheet(
|
||||
@ -906,6 +907,9 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
config['disable_tray_notification'] = not self.systray_notifications.isChecked()
|
||||
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
|
||||
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()
|
||||
config['cover_flow_queue_length'] = self.cover_browse.value()
|
||||
prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString())
|
||||
|
@ -136,7 +136,7 @@
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Default network &timeout:</string>
|
||||
@ -146,7 +146,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="timeout">
|
||||
<property name="toolTip">
|
||||
<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>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="language"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Choose &language (requires restart):</string>
|
||||
@ -178,7 +178,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="priority">
|
||||
<item>
|
||||
<property name="text">
|
||||
@ -197,7 +197,7 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="priority_label">
|
||||
<property name="text">
|
||||
<string>Job &priority:</string>
|
||||
@ -207,7 +207,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
<string>Preferred &output format:</string>
|
||||
@ -217,9 +217,26 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="output_format"/>
|
||||
</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>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -105,6 +105,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
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':
|
||||
|
@ -236,8 +236,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
def search(self, text, reset=True):
|
||||
try:
|
||||
self.db.search(text)
|
||||
except ParseException:
|
||||
self.searched.emit(False)
|
||||
except ParseException as e:
|
||||
self.searched.emit(e.msg)
|
||||
return
|
||||
self.last_search = text
|
||||
if reset:
|
||||
|
@ -229,6 +229,8 @@ class BooksView(QTableView): # {{{
|
||||
def cleanup_sort_history(self, sort_history):
|
||||
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):
|
||||
history.append([col, order])
|
||||
return history
|
||||
|
@ -17,6 +17,9 @@ class ConfigWidgetInterface(object):
|
||||
def genesis(self, gui):
|
||||
raise NotImplementedError()
|
||||
|
||||
def initialize(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def restore_defaults(self):
|
||||
pass
|
||||
|
||||
@ -26,39 +29,39 @@ class ConfigWidgetInterface(object):
|
||||
class Setting(object):
|
||||
|
||||
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.empty_string_is_None = empty_string_is_None
|
||||
self.restart_required = restart_required
|
||||
self.choices = choices
|
||||
if gui_name is None:
|
||||
self.gui_name = 'opt_'+name
|
||||
self.config_obj = config_obj
|
||||
self.gui_obj = getattr(widget, self.gui_name)
|
||||
self.widget = widget
|
||||
|
||||
if isinstance(self.gui_obj, QCheckBox):
|
||||
self.datatype = 'bool'
|
||||
self.gui_obj.stateChanged.connect(lambda x:
|
||||
widget.changed_signal.emit())
|
||||
self.gui_obj.stateChanged.connect(self.changed)
|
||||
elif isinstance(self.gui_obj, QAbstractSpinBox):
|
||||
self.datatype = 'number'
|
||||
self.gui_obj.valueChanged.connect(lambda x:
|
||||
widget.changed_signal.emit())
|
||||
self.gui_obj.valueChanged.connect(self.changed)
|
||||
elif isinstance(self.gui_obj, QLineEdit):
|
||||
self.datatype = 'string'
|
||||
self.gui_obj.textChanged.connect(lambda x:
|
||||
widget.changed_signal.emit())
|
||||
self.gui_obj.textChanged.connect(self.changed)
|
||||
elif isinstance(self.gui_obj, QComboBox):
|
||||
self.datatype = 'choice'
|
||||
self.gui_obj.editTextChanged.connect(lambda x:
|
||||
widget.changed_signal.emit())
|
||||
self.gui_obj.currentIndexChanged.connect(lambda x:
|
||||
widget.changed_signal.emit())
|
||||
self.gui_obj.editTextChanged.connect(self.changed)
|
||||
self.gui_obj.currentIndexChanged.connect(self.changed)
|
||||
else:
|
||||
raise ValueError('Unknown data type')
|
||||
|
||||
def changed(self, *args):
|
||||
self.widget.changed_signal.emit()
|
||||
|
||||
def initialize(self):
|
||||
self.gui_obj.blockSignals(True)
|
||||
if self.datatype == 'choices':
|
||||
if self.datatype == 'choice':
|
||||
self.gui_obj.clear()
|
||||
for x in self.choices:
|
||||
if isinstance(x, basestring):
|
||||
@ -66,9 +69,15 @@ class Setting(object):
|
||||
self.gui_obj.addItem(x[0], QVariant(x[1]))
|
||||
self.set_gui_val(self.get_config_val(default=False))
|
||||
self.gui_obj.blockSignals(False)
|
||||
self.initial_value = self.get_gui_val()
|
||||
|
||||
def commit(self):
|
||||
self.set_config_val(self.get_gui_val())
|
||||
val = self.get_gui_val()
|
||||
oldval = self.get_config_val()
|
||||
changed = val != oldval
|
||||
if changed:
|
||||
self.set_config_val(self.get_gui_val())
|
||||
return changed and self.restart_required
|
||||
|
||||
def restore_defaults(self):
|
||||
self.set_gui_val(self.get_config_val(default=True))
|
||||
@ -90,7 +99,7 @@ class Setting(object):
|
||||
self.gui_obj.setValue(val)
|
||||
elif self.datatype == 'string':
|
||||
self.gui_obj.setText(val if val else '')
|
||||
elif self.datatype == 'choices':
|
||||
elif self.datatype == 'choice':
|
||||
idx = self.gui_obj.findData(QVariant(val))
|
||||
if idx == -1:
|
||||
idx = 0
|
||||
@ -100,17 +109,32 @@ class Setting(object):
|
||||
if self.datatype == 'bool':
|
||||
val = bool(self.gui_obj.isChecked())
|
||||
elif self.datatype == 'number':
|
||||
val = self.gui_obj.value(val)
|
||||
val = self.gui_obj.value()
|
||||
elif self.datatype == 'string':
|
||||
val = unicode(self.gui_name.text()).strip()
|
||||
if self.empty_string_is_None and not val:
|
||||
val = None
|
||||
elif self.datatype == 'choices':
|
||||
elif self.datatype == 'choice':
|
||||
idx = self.gui_obj.currentIndex()
|
||||
if idx < 0: idx = 0
|
||||
val = unicode(self.gui_obj.itemData(idx).toString())
|
||||
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):
|
||||
|
||||
@ -122,10 +146,11 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
|
||||
self.setupUi(self)
|
||||
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,
|
||||
choices=choices)
|
||||
self.register_setting(setting)
|
||||
choices=choices, restart_required=restart_required)
|
||||
return self.register_setting(setting)
|
||||
|
||||
def register_setting(self, setting):
|
||||
self.settings[setting.name] = setting
|
||||
@ -135,9 +160,13 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
|
||||
for setting in self.settings.values():
|
||||
setting.initialize()
|
||||
|
||||
def commit(self):
|
||||
def commit(self, *args):
|
||||
restart_required = False
|
||||
for setting in self.settings.values():
|
||||
setting.commit()
|
||||
rr = setting.commit()
|
||||
if rr:
|
||||
restart_required = True
|
||||
return restart_required
|
||||
|
||||
def restore_defaults(self, *args):
|
||||
for setting in self.settings.values():
|
||||
@ -158,6 +187,7 @@ def test_widget(category, name, gui=None): # {{{
|
||||
pl = get_plugin(category, name)
|
||||
d = QDialog()
|
||||
d.resize(750, 550)
|
||||
d.setWindowTitle(category + " - " + name)
|
||||
bb = QDialogButtonBox(d)
|
||||
bb.setStandardButtons(bb.Apply|bb.Cancel|bb.RestoreDefaults)
|
||||
bb.accepted.connect(d.accept)
|
||||
@ -165,11 +195,13 @@ def test_widget(category, name, gui=None): # {{{
|
||||
w = pl.create_widget(d)
|
||||
bb.button(bb.RestoreDefaults).clicked.connect(w.restore_defaults)
|
||||
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()
|
||||
d.setLayout(l)
|
||||
l.addWidget(w)
|
||||
l.addWidget(bb)
|
||||
mygui = gui is None
|
||||
if gui is None:
|
||||
from calibre.gui2.ui import Main
|
||||
from calibre.gui2.main import option_parser
|
||||
@ -181,7 +213,14 @@ def test_widget(category, name, gui=None): # {{{
|
||||
gui = Main(opts)
|
||||
gui.initialize(db.library_path, db, None, actions, show_gui=False)
|
||||
w.genesis(gui)
|
||||
w.initialize()
|
||||
restart_required = False
|
||||
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()
|
||||
# }}}
|
||||
|
||||
|
169
src/calibre/gui2/preferences/behavior.py
Normal file
169
src/calibre/gui2/preferences/behavior.py
Normal 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')
|
||||
|
@ -29,21 +29,21 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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">
|
||||
<string>Show notification when &new version is available</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<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">
|
||||
<string>Automatically send downloaded &news to ebook reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<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">
|
||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||
</property>
|
||||
@ -57,12 +57,12 @@
|
||||
<string>Default network &timeout:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>timeout</cstring>
|
||||
<cstring>opt_network_timeout</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="timeout">
|
||||
<widget class="QSpinBox" name="opt_network_timeout">
|
||||
<property name="toolTip">
|
||||
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string>
|
||||
</property>
|
||||
@ -81,7 +81,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="priority">
|
||||
<widget class="QComboBox" name="opt_worker_process_priority">
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||
</property>
|
||||
@ -111,7 +111,7 @@
|
||||
<string>Job &priority:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>priority</cstring>
|
||||
<cstring>opt_worker_process_priority</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -121,12 +121,12 @@
|
||||
<string>Preferred &output format:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>output_format</cstring>
|
||||
<cstring>opt_output_format</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="output_format">
|
||||
<widget class="QComboBox" name="opt_output_format">
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||
</property>
|
||||
@ -164,6 +164,20 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
@ -182,7 +196,7 @@
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<item>
|
||||
<widget class="QListWidget" name="input_order">
|
||||
<widget class="QListWidget" name="opt_input_order">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -194,7 +208,7 @@
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_10">
|
||||
<item>
|
||||
<widget class="QToolButton" name="input_up">
|
||||
<widget class="QToolButton" name="input_up_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
@ -218,7 +232,7 @@
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="input_down">
|
||||
<widget class="QToolButton" name="input_down_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
@ -242,7 +256,7 @@
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0">
|
||||
<widget class="QListWidget" name="viewer">
|
||||
<widget class="QListWidget" name="opt_internally_viewed_formats">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
|
172
src/calibre/gui2/preferences/columns.py
Normal file
172
src/calibre/gui2/preferences/columns.py
Normal 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')
|
||||
|
@ -25,7 +25,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QListWidget" name="columns">
|
||||
<widget class="QListWidget" name="opt_columns">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -155,7 +155,7 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="pushButton">
|
||||
<widget class="QPushButton" name="add_col_button">
|
||||
<property name="text">
|
||||
<string>Add &custom column</string>
|
||||
</property>
|
174
src/calibre/gui2/preferences/create_custom_column.py
Normal file
174
src/calibre/gui2/preferences/create_custom_column.py
Normal 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)
|
191
src/calibre/gui2/preferences/create_custom_column.ui
Normal file
191
src/calibre/gui2/preferences/create_custom_column.ui
Normal 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>&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 &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 &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><p>Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.</p>
|
||||
<p>For example:
|
||||
<ul>
|
||||
<li> ddd, d MMM yyyy gives Mon, 5 Jan 2010<li>
|
||||
<li>dd MMMM yy gives 05 January 10</li>
|
||||
</ul> </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 &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>
|
@ -20,7 +20,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
r = self.register
|
||||
|
||||
r('gui_layout', config, choices=
|
||||
r('gui_layout', config, restart_required=True, choices=
|
||||
[(_('Wide'), 'wide'), (_('Narrow'), 'narrow')])
|
||||
|
||||
r('cover_flow_queue_length', config)
|
||||
@ -32,19 +32,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
if l != lang]
|
||||
if lang != '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]
|
||||
# Default language is the autodetected one
|
||||
choices = [get_language(lang), lang] + choices
|
||||
r('language', prefs, choices=choices)
|
||||
choices = [(get_language(lang), lang)] + choices
|
||||
r('language', prefs, choices=choices, restart_required=True)
|
||||
|
||||
r('show_avg_rating', config)
|
||||
r('disable_animations', config)
|
||||
r('systray_icon', config)
|
||||
r('systray_icon', config, restart_required=True)
|
||||
r('show_splash_screen', gprefs)
|
||||
r('disable_tray_notification', 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)
|
||||
|
||||
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
|
||||
|
293
src/calibre/gui2/preferences/toolbar.py
Normal file
293
src/calibre/gui2/preferences/toolbar.py
Normal 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')
|
||||
|
216
src/calibre/gui2/preferences/toolbar.ui
Normal file
216
src/calibre/gui2/preferences/toolbar.ui
Normal 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&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>&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>
|
@ -90,6 +90,7 @@ class SearchBox2(QComboBox):
|
||||
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
||||
self.setMinimumContentsLength(25)
|
||||
self._in_a_search = False
|
||||
self.tool_tip_text = self.toolTip()
|
||||
|
||||
def initialize(self, opt_name, colorize=False, help_text=_('Search')):
|
||||
self.as_you_type = config['search_as_you_type']
|
||||
@ -100,6 +101,7 @@ class SearchBox2(QComboBox):
|
||||
self.clear_to_help()
|
||||
|
||||
def normalize_state(self):
|
||||
self.setToolTip(self.tool_tip_text)
|
||||
if self.help_state:
|
||||
self.setEditText('')
|
||||
self.line_edit.setStyleSheet(
|
||||
@ -112,6 +114,7 @@ class SearchBox2(QComboBox):
|
||||
self.normal_background)
|
||||
|
||||
def clear_to_help(self):
|
||||
self.setToolTip(self.tool_tip_text)
|
||||
if self.help_state:
|
||||
return
|
||||
self.help_state = True
|
||||
@ -131,6 +134,9 @@ class SearchBox2(QComboBox):
|
||||
self.clear_to_help()
|
||||
|
||||
def search_done(self, ok):
|
||||
if isinstance(ok, basestring):
|
||||
self.setToolTip(ok)
|
||||
ok = False
|
||||
if not unicode(self.currentText()).strip():
|
||||
return self.clear_to_help()
|
||||
self._in_a_search = ok
|
||||
|
@ -233,7 +233,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
|
||||
######################### Search Restriction ##########################
|
||||
SearchRestrictionMixin.__init__(self)
|
||||
self.apply_named_search_restriction(db.prefs.get('gui_restriction', ''))
|
||||
self.apply_named_search_restriction(db.prefs['gui_restriction'])
|
||||
|
||||
########################### Cover Flow ################################
|
||||
|
||||
@ -378,7 +378,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
self.set_window_title()
|
||||
self.apply_named_search_restriction('') # reset restriction to null
|
||||
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):
|
||||
self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())
|
||||
|
@ -319,12 +319,18 @@ class ResultCache(SearchQueryParser):
|
||||
matches.add(item[0])
|
||||
return matches
|
||||
|
||||
def get_matches(self, location, query):
|
||||
def get_matches(self, location, query, allow_recursion=True):
|
||||
matches = set([])
|
||||
if query and query.strip():
|
||||
# get metadata key associated with the search term. Eliminates
|
||||
# dealing with plurals and other aliases
|
||||
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
|
||||
if location in self.field_metadata and \
|
||||
|
@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
|
||||
'''
|
||||
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 math import floor
|
||||
|
||||
@ -145,6 +145,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
|
||||
def initialize_dynamic(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
|
||||
def migrate_preference(key, default):
|
||||
@ -264,7 +266,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
# account for the series index column. Field_metadata knows that
|
||||
# the series index is one larger than the series. If you change
|
||||
# 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_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()):
|
||||
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.data = ResultCache(self.FIELD_MAP, self.field_metadata)
|
||||
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 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):
|
||||
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')
|
||||
@ -1677,7 +1727,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
path = path_or_stream
|
||||
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):
|
||||
self._add_newbook_tag(mi)
|
||||
if not add_duplicates and self.has_book(mi):
|
||||
return None
|
||||
series_index = 1.0 if mi.series_index is None else mi.series_index
|
||||
@ -1716,6 +1777,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
ids = []
|
||||
for path in paths:
|
||||
mi = metadata.next()
|
||||
self._add_newbook_tag(mi)
|
||||
format = formats.next()
|
||||
if not add_duplicates and self.has_book(mi):
|
||||
duplicates.append((path, format, mi))
|
||||
@ -1754,8 +1816,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
return (paths, formats, metadata), 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
|
||||
if apply_import_tags:
|
||||
self._add_newbook_tag(mi)
|
||||
if not mi.title:
|
||||
mi.title = _('Unknown')
|
||||
if not mi.authors:
|
||||
|
@ -36,7 +36,7 @@ class FieldMetadata(dict):
|
||||
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
|
||||
|
||||
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 == user: user-defined tag category.
|
||||
kind == search: saved-searches category.
|
||||
@ -239,7 +239,7 @@ class FieldMetadata(dict):
|
||||
'is_multiple':None,
|
||||
'kind':'field',
|
||||
'name':None,
|
||||
'search_terms':[],
|
||||
'search_terms':['series_index'],
|
||||
'is_custom':False,
|
||||
'is_category':False}),
|
||||
('sort', {'table':None,
|
||||
@ -395,6 +395,18 @@ class FieldMetadata(dict):
|
||||
'is_editable': is_editable,}
|
||||
self._add_search_terms_to_map(key, [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):
|
||||
for key in self.get_custom_fields():
|
||||
@ -463,9 +475,7 @@ class FieldMetadata(dict):
|
||||
# ])
|
||||
|
||||
def get_search_terms(self):
|
||||
s_keys = []
|
||||
for v in self._tb_cats.itervalues():
|
||||
map((lambda x:s_keys.append(x)), v['search_terms'])
|
||||
s_keys = sorted(self._search_term_map.keys())
|
||||
for v in self.search_items:
|
||||
s_keys.append(v)
|
||||
# if set(s_keys) != self.DEFAULT_LOCATIONS:
|
||||
@ -476,6 +486,9 @@ class FieldMetadata(dict):
|
||||
def _add_search_terms_to_map(self, key, terms):
|
||||
if terms is not None:
|
||||
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
|
||||
|
||||
def search_term_to_key(self, term):
|
||||
|
@ -13,11 +13,11 @@ from lxml import html
|
||||
from lxml.html.builder import HTML, HEAD, TITLE, LINK, DIV, IMG, BODY, \
|
||||
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.constants import __appname__
|
||||
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
|
||||
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')
|
||||
|
||||
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']) \
|
||||
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,
|
||||
book['authors'], book['size'], book['timestamp'], tags)
|
||||
ctext = ''
|
||||
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:
|
||||
data.text = text
|
||||
@ -150,7 +156,7 @@ def build_index(books, num, search, sort, order, start, total, url_base):
|
||||
class MobileServer(object):
|
||||
'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):
|
||||
connect('mobile', '/mobile', self.mobile)
|
||||
@ -189,6 +195,10 @@ class MobileServer(object):
|
||||
if sort is not None:
|
||||
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 = []
|
||||
for record in items[(start-1):(start-1)+num]:
|
||||
book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
|
||||
@ -203,12 +213,37 @@ class MobileServer(object):
|
||||
book['authors'] = authors
|
||||
book['series_index'] = fmt_sidx(float(record[FM['series_index']]))
|
||||
book['series'] = record[FM['series']]
|
||||
book['tags'] = record[FM['tags']]
|
||||
book['tags'] = format_tag_string(record[FM['tags']], ',')
|
||||
book['title'] = record[FM['title']]
|
||||
for x in ('timestamp', 'pubdate'):
|
||||
book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
|
||||
book['id'] = record[FM['id']]
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
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,
|
||||
pretty_print=True)
|
||||
|
||||
|
@ -19,6 +19,7 @@ from calibre.ebooks.metadata import fmt_sidx
|
||||
from calibre.library.comments import comments_to_html
|
||||
from calibre import guess_type
|
||||
from calibre.utils.ordered_dict import OrderedDict
|
||||
from calibre.utils.date import format_date
|
||||
|
||||
BASE_HREFS = {
|
||||
0 : '/stanza',
|
||||
@ -130,7 +131,7 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated):
|
||||
link
|
||||
)
|
||||
|
||||
def ACQUISITION_ENTRY(item, version, FM, updated):
|
||||
def ACQUISITION_ENTRY(item, version, FM, updated, CFM, CKEYS):
|
||||
title = item[FM['title']]
|
||||
if not title:
|
||||
title = _('Unknown')
|
||||
@ -153,6 +154,21 @@ def ACQUISITION_ENTRY(item, version, FM, updated):
|
||||
extra.append(_('SERIES: %s [%s]<br />')%\
|
||||
(series,
|
||||
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']]
|
||||
if comments:
|
||||
comments = comments_to_html(comments)
|
||||
@ -260,10 +276,14 @@ class NavFeed(Feed):
|
||||
class AcquisitionFeed(NavFeed):
|
||||
|
||||
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)
|
||||
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:
|
||||
self.root.append(ACQUISITION_ENTRY(item, version, FM, updated))
|
||||
self.root.append(ACQUISITION_ENTRY(item, version, FM, updated,
|
||||
CFM, CKEYS))
|
||||
|
||||
class CategoryFeed(NavFeed):
|
||||
|
||||
@ -360,7 +380,7 @@ class OPDSServer(object):
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
cherrypy.response.headers['Content-Type'] = 'application/atom+xml;profile=opds-catalog'
|
||||
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):
|
||||
try:
|
||||
@ -568,7 +588,10 @@ class OPDSServer(object):
|
||||
(_('Newest'), _('Date'), 'Onewest'),
|
||||
(_('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:
|
||||
continue
|
||||
if category == 'formats':
|
||||
|
@ -11,6 +11,7 @@ import cherrypy
|
||||
|
||||
from calibre import strftime as _strftime, prints
|
||||
from calibre.utils.date import now as nowf
|
||||
from calibre.utils.config import tweaks
|
||||
|
||||
|
||||
def expose(func):
|
||||
@ -43,4 +44,14 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
|
||||
except:
|
||||
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 ''
|
||||
|
||||
|
@ -11,10 +11,11 @@ import cherrypy
|
||||
from lxml.builder import ElementMaker
|
||||
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.constants import preferred_encoding
|
||||
from calibre import isbytestring
|
||||
from calibre.utils.date import format_date
|
||||
|
||||
E = ElementMaker()
|
||||
|
||||
@ -83,9 +84,44 @@ class XMLServer(object):
|
||||
for x in ('isbn', 'formats', 'series', 'tags', 'publisher',
|
||||
'comments'):
|
||||
y = record[FM[x]]
|
||||
if x == 'tags':
|
||||
y = format_tag_string(y, ',')
|
||||
kwargs[x] = serialize(y) if y else ''
|
||||
|
||||
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))
|
||||
|
||||
updated = self.db.last_modified()
|
||||
|
@ -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
|
||||
|
||||
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,
|
||||
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).
|
||||
|
@ -166,7 +166,7 @@ Search & Sort
|
||||
|
||||
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.
|
||||
|
||||
@ -212,9 +212,10 @@ metadata.
|
||||
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
|
||||
clicking the button |sbi|.
|
||||
|
||||
Available fields for searching are: ``tag, title, author, publisher, series, rating, cover, comments, format,
|
||||
isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the ``ondevice`` field
|
||||
becomes available. To find the search name for a custom column, hover your mouse over the column header.
|
||||
Available fields for searching are: ``tag, title, author, publisher, series, series_index, rating, cover,
|
||||
comments, format, isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the
|
||||
``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::
|
||||
|
||||
@ -223,9 +224,8 @@ The syntax for searching for dates is::
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
@ -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:<=1K Will find books with a format smaller than 1KB
|
||||
|
||||
Dates and numeric fields support the operators ``=`` (equals), ``>`` (greater than), ``>=`` (greater than or
|
||||
equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to). Rating fields are
|
||||
considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3 or higher.
|
||||
Dates and numeric fields support the relational operators ``=`` (equals), ``>`` (greater than), ``>=``
|
||||
(greater than or equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to).
|
||||
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
|
||||
"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.
|
||||
|
||||
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
@ -13,14 +13,12 @@ from optparse import OptionParser as _OptionParser
|
||||
from optparse import IndentedHelpFormatter
|
||||
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__
|
||||
from calibre.utils.lock import LockError, ExclusiveFile
|
||||
|
||||
plugin_dir = os.path.join(config_dir, 'plugins')
|
||||
|
||||
CONFIG_DIR_MODE = 0700
|
||||
|
||||
def make_config_dir():
|
||||
if not os.path.exists(plugin_dir):
|
||||
os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE)
|
||||
@ -719,6 +717,7 @@ def _prefs():
|
||||
c.add_opt('add_formats_to_existing', default=False,
|
||||
help=_('Add new formats to existing book records'))
|
||||
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
|
||||
# calibre server can execute searches
|
||||
|
@ -81,12 +81,7 @@ def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30,
|
||||
for x in to:
|
||||
return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
|
||||
import smtplib
|
||||
class SMTP_SSL(smtplib.SMTP_SSL): # Workaround for bug in smtplib.py
|
||||
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
|
||||
cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
|
||||
timeout = None # Non-blocking sockets sometimes don't work
|
||||
port = int(port)
|
||||
s = cls(timeout=timeout, local_hostname=localhost)
|
||||
|
@ -193,12 +193,14 @@ class NavBarTemplate(Template):
|
||||
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_70',
|
||||
style='text-align:'+align))
|
||||
if bottom:
|
||||
navbar.append(HR())
|
||||
text = 'This article was downloaded by '
|
||||
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
|
||||
p[0].tail = ' from '
|
||||
navbar.append(p)
|
||||
navbar.append(BR())
|
||||
if not url.startswith('file://'):
|
||||
navbar.append(HR())
|
||||
text = 'This article was downloaded by '
|
||||
p = PT(text, STRONG(__appname__), A(url, href=url),
|
||||
style='text-align:left; max-width: 100%; overflow: hidden;')
|
||||
p[0].tail = ' from '
|
||||
navbar.append(p)
|
||||
navbar.append(BR())
|
||||
navbar.append(BR())
|
||||
else:
|
||||
next = 'feed_%d'%(feed+1) if art == number_of_articles_in_feed - 1 \
|
||||
|
Loading…
x
Reference in New Issue
Block a user