mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
0.7.17 release
This commit is contained in:
commit
525bfdb913
@ -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
|
||||
|
||||
|
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
|
||||
|
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
|
@ -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
|
||||
|
@ -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,31 @@ 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'
|
||||
|
||||
plugins += [LookAndFeel, Behavior, Columns]
|
||||
|
||||
#}}}
|
||||
|
||||
|
@ -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...')
|
||||
|
@ -14,7 +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
|
||||
from calibre.utils.config import prefs, tweaks
|
||||
|
||||
class Worker(Thread):
|
||||
|
||||
@ -66,7 +66,8 @@ class Worker(Thread):
|
||||
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)
|
||||
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)
|
||||
|
@ -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>
|
||||
|
@ -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:
|
||||
|
@ -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'),
|
||||
|
@ -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 \
|
||||
|
@ -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):
|
||||
@ -297,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
|
||||
@ -1718,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
|
||||
@ -1757,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))
|
||||
@ -1795,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:
|
||||
|
@ -475,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:
|
||||
@ -488,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)
|
||||
|
||||
|
@ -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).
|
||||
|
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
@ -717,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
|
||||
|
Loading…
x
Reference in New Issue
Block a user