0.7.17 release

This commit is contained in:
GRiker 2010-09-03 17:11:38 -07:00
commit 525bfdb913
62 changed files with 31337 additions and 23066 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.7.16' __version__ = '0.7.17'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re

View File

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

View File

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

View File

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

View File

@ -62,7 +62,16 @@ class HTMLTOCAdder(object):
def __call__(self, oeb, context): def __call__(self, oeb, context):
if 'toc' in oeb.guide: if 'toc' in oeb.guide:
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): if not getattr(getattr(oeb, 'toc', False), 'nodes', False):
return return
oeb.logger.info('Generating in-line TOC...') oeb.logger.info('Generating in-line TOC...')

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import QMenu, QToolButton
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.gui2 import error_dialog, Dispatcher from calibre.gui2 import error_dialog, Dispatcher
from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.utils.config import prefs from calibre.utils.config import prefs, tweaks
class Worker(Thread): class Worker(Thread):
@ -66,7 +66,8 @@ class Worker(Thread):
for identical_book in identical_book_list: for identical_book in identical_book_list:
self.add_formats(identical_book, paths, newdb, replace=False) self.add_formats(identical_book, paths, newdb, replace=False)
if not added: if not added:
newdb.import_book(mi, paths, notify=False, import_hooks=False) newdb.import_book(mi, paths, notify=False, import_hooks=False,
apply_import_tags=tweaks['add_new_book_tags_when_importing_books'])
co = self.db.conversion_options(x, 'PIPE') co = self.db.conversion_options(x, 'PIPE')
if co is not None: if co is not None:
newdb.set_conversion_options(x, 'PIPE', co) newdb.set_conversion_options(x, 'PIPE', co)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -145,6 +145,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def initialize_dynamic(self): def initialize_dynamic(self):
self.prefs = DBPrefs(self) self.prefs = DBPrefs(self)
defs = self.prefs.defaults
defs['gui_restriction'] = defs['cs_restriction'] = ''
# Migrate saved search and user categories to db preference scheme # Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default): def migrate_preference(key, default):
@ -297,6 +299,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if len(saved_searches().names()): if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches')) tb_cats.add_search_category(label='search', name=_('Searches'))
gst = tweaks['grouped_search_terms']
for t in gst:
try:
self.field_metadata._add_search_terms_to_map(gst[t], [t])
except ValueError:
traceback.print_exc()
self.book_on_device_func = None self.book_on_device_func = None
self.data = ResultCache(self.FIELD_MAP, self.field_metadata) self.data = ResultCache(self.FIELD_MAP, self.field_metadata)
self.search = self.data.search self.search = self.data.search
@ -1718,7 +1727,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
path = path_or_stream path = path_or_stream
return run_plugins_on_import(path, format) return run_plugins_on_import(path, format)
def _add_newbook_tag(self, mi):
tags = prefs['new_book_tags']
if tags:
for tag in [t.strip() for t in tags]:
if tag:
if mi.tags is None:
mi.tags = [tag]
else:
mi.tags.append(tag)
def create_book_entry(self, mi, cover=None, add_duplicates=True): def create_book_entry(self, mi, cover=None, add_duplicates=True):
self._add_newbook_tag(mi)
if not add_duplicates and self.has_book(mi): if not add_duplicates and self.has_book(mi):
return None return None
series_index = 1.0 if mi.series_index is None else mi.series_index series_index = 1.0 if mi.series_index is None else mi.series_index
@ -1757,6 +1777,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
ids = [] ids = []
for path in paths: for path in paths:
mi = metadata.next() mi = metadata.next()
self._add_newbook_tag(mi)
format = formats.next() format = formats.next()
if not add_duplicates and self.has_book(mi): if not add_duplicates and self.has_book(mi):
duplicates.append((path, format, mi)) duplicates.append((path, format, mi))
@ -1795,8 +1816,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return (paths, formats, metadata), len(ids) return (paths, formats, metadata), len(ids)
return None, len(ids) return None, len(ids)
def import_book(self, mi, formats, notify=True, import_hooks=True): def import_book(self, mi, formats, notify=True, import_hooks=True,
apply_import_tags=True):
series_index = 1.0 if mi.series_index is None else mi.series_index series_index = 1.0 if mi.series_index is None else mi.series_index
if apply_import_tags:
self._add_newbook_tag(mi)
if not mi.title: if not mi.title:
mi.title = _('Unknown') mi.title = _('Unknown')
if not mi.authors: if not mi.authors:

View File

@ -475,9 +475,7 @@ class FieldMetadata(dict):
# ]) # ])
def get_search_terms(self): def get_search_terms(self):
s_keys = [] s_keys = sorted(self._search_term_map.keys())
for v in self._tb_cats.itervalues():
map((lambda x:s_keys.append(x)), v['search_terms'])
for v in self.search_items: for v in self.search_items:
s_keys.append(v) s_keys.append(v)
# if set(s_keys) != self.DEFAULT_LOCATIONS: # if set(s_keys) != self.DEFAULT_LOCATIONS:
@ -488,6 +486,9 @@ class FieldMetadata(dict):
def _add_search_terms_to_map(self, key, terms): def _add_search_terms_to_map(self, key, terms):
if terms is not None: if terms is not None:
for t in terms: for t in terms:
t = t.lower()
if t in self._search_term_map:
raise ValueError('Attempt to add duplicate search term "%s"'%t)
self._search_term_map[t] = key self._search_term_map[t] = key
def search_term_to_key(self, term): def search_term_to_key(self, term):

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -717,6 +717,7 @@ def _prefs():
c.add_opt('add_formats_to_existing', default=False, c.add_opt('add_formats_to_existing', default=False,
help=_('Add new formats to existing book records')) help=_('Add new formats to existing book records'))
c.add_opt('installation_uuid', default=None, help='Installation UUID') c.add_opt('installation_uuid', default=None, help='Installation UUID')
c.add_opt('new_book_tags', default=[], help=_('Tags to apply to books added to the library'))
# these are here instead of the gui preferences because calibredb and # these are here instead of the gui preferences because calibredb and
# calibre server can execute searches # calibre server can execute searches