diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css
index 057c6c9f42..07a95661be 100644
--- a/resources/catalog/stylesheet.css
+++ b/resources/catalog/stylesheet.css
@@ -108,6 +108,13 @@ p.date_read {
text-indent:-6em;
}
+hr.annotations_divider {
+ width:50%;
+ margin-left:1em;
+ margin-top:0em;
+ margin-bottom:0em;
+ }
+
hr.description_divider {
width:90%;
margin-left:5%;
@@ -117,18 +124,20 @@ hr.description_divider {
border-left: solid white 0px;
}
-hr.annotations_divider {
- width:50%;
- margin-left:1em;
- margin-top:0em;
- margin-bottom:0em;
+hr.merged_comments_divider {
+ width:80%;
+ margin-left:10%;
+ border-top: solid white 0px;
+ border-right: solid white 0px;
+ border-bottom: dotted grey 2px;
+ border-left: solid white 0px;
}
td.publisher, td.date {
font-weight:bold;
text-align:center;
}
-td.rating {
+td.rating, td.notes {
text-align: center;
}
td.thumbnail img {
diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py
index a253664a1e..0eba0406a1 100644
--- a/src/calibre/gui2/actions/catalog.py
+++ b/src/calibre/gui2/actions/catalog.py
@@ -57,7 +57,7 @@ class GenerateCatalogAction(InterfaceAction):
if job.result:
# Search terms nulled catalog results
return error_dialog(self.gui, _('No books found'),
- _("No books to catalog\nCheck exclude tags"),
+ _("No books to catalog\nCheck exclusion criteria"),
show=True)
if job.failed:
return self.gui.job_exception(job)
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py
index 1ae4efd014..f5fabca80e 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.py
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py
@@ -17,18 +17,55 @@ class PluginWidget(QWidget,Ui_Form):
TITLE = _('E-book options')
HELP = _('Options specific to')+' EPUB/MOBI '+_('output')
- OPTION_FIELDS = [('exclude_genre','\[.+\]'),
- ('exclude_tags','~,'+_('Catalog')),
- ('generate_titles', True),
- ('generate_series', True),
- ('generate_recently_added', True),
- ('note_tag','*'),
- ('numbers_as_text', False),
- ('read_pattern','+'),
- ('read_source_field_cb','Tag'),
- ('wishlist_tag','Wishlist'),
- ]
+ CheckBoxControls = [
+ 'generate_titles',
+ 'generate_series',
+ 'generate_genres',
+ 'generate_recently_added',
+ 'generate_descriptions',
+ 'include_hr'
+ ]
+ ComboBoxControls = [
+ 'read_source_field',
+ 'exclude_source_field',
+ 'header_note_source_field',
+ 'merge_source_field'
+ ]
+ LineEditControls = [
+ 'exclude_genre',
+ 'exclude_pattern',
+ 'exclude_tags',
+ 'read_pattern',
+ 'wishlist_tag'
+ ]
+ RadioButtonControls = [
+ 'merge_before',
+ 'merge_after'
+ ]
+ SpinBoxControls = [
+ 'thumb_width'
+ ]
+
+ OPTION_FIELDS = zip(CheckBoxControls,
+ [True for i in CheckBoxControls],
+ ['check_box' for i in CheckBoxControls])
+ OPTION_FIELDS += zip(ComboBoxControls,
+ [None for i in ComboBoxControls],
+ ['combo_box' for i in ComboBoxControls])
+ OPTION_FIELDS += zip(RadioButtonControls,
+ [None for i in RadioButtonControls],
+ ['radio_button' for i in RadioButtonControls])
+
+ # LineEditControls
+ OPTION_FIELDS += zip(['exclude_genre'],['\[.+\]'],['line_edit'])
+ OPTION_FIELDS += zip(['exclude_pattern'],[None],['line_edit'])
+ OPTION_FIELDS += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit'])
+ OPTION_FIELDS += zip(['read_pattern'],['+'],['line_edit'])
+ OPTION_FIELDS += zip(['wishlist_tag'],['Wishlist'],['line_edit'])
+
+ # SpinBoxControls
+ OPTION_FIELDS += zip(['thumb_width'],[1.00],['spin_box'])
# Output synced to the connected device?
sync_enabled = True
@@ -42,105 +79,203 @@ class PluginWidget(QWidget,Ui_Form):
def initialize(self, name, db):
self.name = name
-
- # Populate the 'Read book' source fields
- all_custom_fields = db.custom_field_keys()
- custom_fields = {}
- custom_fields['Tag'] = {'field':'tag', 'datatype':u'text'}
- for custom_field in all_custom_fields:
- field_md = db.metadata_for_field(custom_field)
- if field_md['datatype'] in ['bool','composite','datetime','text']:
- custom_fields[field_md['name']] = {'field':custom_field,
- 'datatype':field_md['datatype']}
-
- # Add the sorted eligible fields to the combo box
- for cf in sorted(custom_fields):
- self.read_source_field_cb.addItem(cf)
-
- self.read_source_fields = custom_fields
- self.read_source_field_cb.currentIndexChanged.connect(self.read_source_field_changed)
+ self.db = db
+ self.populateComboBoxes()
# Update dialog fields from stored options
for opt in self.OPTION_FIELDS:
- opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
- if opt[0] in [
- 'generate_recently_added',
- 'generate_series',
- 'generate_titles',
- 'numbers_as_text',
- ]:
- getattr(self, opt[0]).setChecked(opt_value)
+ c_name, c_def, c_type = opt
+ opt_value = gprefs.get(self.name + '_' + c_name, c_def)
+ if c_type in ['check_box']:
+ getattr(self, c_name).setChecked(eval(str(opt_value)))
+ elif c_type in ['combo_box'] and opt_value is not None:
+ # *** Test this code with combo boxes ***
+ #index = self.read_source_field.findText(opt_value)
+ index = getattr(self,c_name).findText(opt_value)
+ if index == -1 and c_name == 'read_source_field':
+ index = self.read_source_field.findText('Tag')
+ #self.read_source_field.setCurrentIndex(index)
+ getattr(self,c_name).setCurrentIndex(index)
+ elif c_type in ['line_edit']:
+ getattr(self, c_name).setText(opt_value)
+ elif c_type in ['radio_button'] and opt_value is not None:
+ getattr(self, c_name).setChecked(opt_value)
+ elif c_type in ['spin_box']:
+ getattr(self, c_name).setValue(float(opt_value))
- # Combo box
- elif opt[0] in ['read_source_field_cb']:
- # Look for last-stored combo box value
- index = self.read_source_field_cb.findText(opt_value)
- if index == -1:
- index = self.read_source_field_cb.findText('Tag')
- self.read_source_field_cb.setCurrentIndex(index)
-
- # Text fields
- else:
- getattr(self, opt[0]).setText(opt_value)
-
- # Init self.read_source_field
- cs = unicode(self.read_source_field_cb.currentText())
+ # Init self.read_source_field_name
+ cs = unicode(self.read_source_field.currentText())
read_source_spec = self.read_source_fields[cs]
- self.read_source_field = read_source_spec['field']
+ self.read_source_field_name = read_source_spec['field']
+
+ # Init self.exclude_source_field_name
+ self.exclude_source_field_name = ''
+ cs = unicode(self.exclude_source_field.currentText())
+ if cs > '':
+ exclude_source_spec = self.exclude_source_fields[cs]
+ self.exclude_source_field_name = exclude_source_spec['field']
+
+ # Init self.merge_source_field_name
+ self.merge_source_field_name = ''
+ cs = unicode(self.merge_source_field.currentText())
+ if cs > '':
+ merge_source_spec = self.merge_source_fields[cs]
+ self.merge_source_field_name = merge_source_spec['field']
+
+ # Init self.header_note_source_field_name
+ self.header_note_source_field_name = ''
+ cs = unicode(self.header_note_source_field.currentText())
+ if cs > '':
+ header_note_source_spec = self.header_note_source_fields[cs]
+ self.header_note_source_field_name = header_note_source_spec['field']
+
+ # Hook changes to thumb_width
+ self.thumb_width.valueChanged.connect(self.thumb_width_changed)
def options(self):
# Save/return the current options
# exclude_genre stores literally
# generate_titles, generate_recently_added, numbers_as_text stores as True/False
# others store as lists
+
opts_dict = {}
+ # Save values to gprefs
for opt in self.OPTION_FIELDS:
- # Save values to gprefs
- if opt[0] in [
- 'generate_recently_added',
- 'generate_series',
- 'generate_titles',
- 'numbers_as_text',
- ]:
- opt_value = getattr(self,opt[0]).isChecked()
+ c_name, c_def, c_type = opt
+ if c_type in ['check_box', 'radio_button']:
+ opt_value = getattr(self, c_name).isChecked()
+ elif c_type in ['combo_box']:
+ opt_value = unicode(getattr(self,c_name).currentText())
+ elif c_type in ['line_edit']:
+ opt_value = unicode(getattr(self, c_name).text())
+ elif c_type in ['spin_box']:
+ opt_value = unicode(getattr(self, c_name).cleanText())
+ gprefs.set(self.name + '_' + c_name, opt_value)
- # Combo box uses .currentText()
- elif opt[0] in ['read_source_field_cb']:
- opt_value = unicode(getattr(self, opt[0]).currentText())
-
- # text fields use .text()
+ # Construct opts object
+ if c_name == 'exclude_tags':
+ # store as list
+ opts_dict[c_name] = opt_value.split(',')
else:
- opt_value = unicode(getattr(self, opt[0]).text())
- gprefs.set(self.name + '_' + opt[0], opt_value)
+ opts_dict[c_name] = opt_value
- # Construct opts
- if opt[0] in [
- 'exclude_genre',
- 'generate_recently_added',
- 'generate_series',
- 'generate_titles',
- 'numbers_as_text',
- ]:
- opts_dict[opt[0]] = opt_value
- else:
- opts_dict[opt[0]] = opt_value.split(',')
+ # Generate markers for hybrids
+ opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name,
+ self.read_pattern.text())
+ opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name,
+ self.exclude_pattern.text())
- # Generate read_book_marker
- opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field, self.read_pattern.text())
+ # Generate specs for merge_comments, header_note_source_field
+ checked = ''
+ if self.merge_before.isChecked():
+ checked = 'before'
+ elif self.merge_after.isChecked():
+ checked = 'after'
+ include_hr = self.include_hr.isChecked()
+ opts_dict['merge_comments'] = "%s:%s:%s" % \
+ (self.merge_source_field_name, checked, include_hr)
+
+ opts_dict['header_note_source_field'] = self.header_note_source_field_name
# Append the output profile
opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']]
+ if False:
+ print "opts_dict"
+ for opt in sorted(opts_dict.keys()):
+ print " %s: %s" % (opt, repr(opts_dict[opt]))
return opts_dict
+ def populateComboBoxes(self):
+ # Custom column types declared in
+ # gui2.preferences.create_custom_column:CreateCustomColumn()
+ # As of 0.7.34:
+ # bool Yes/No
+ # comments Long text, like comments, not shown in tag browser
+ # composite Column built from other columns
+ # datetime Date
+ # enumeration Text, but with a fixed set of permitted values
+ # float Floating point numbers
+ # int Integers
+ # rating Ratings, shown with stars
+ # series Text column for keeping series-like information
+ # text Column shown in the tag browser
+ # *text Comma-separated text, like tags, shown in tag browser
+
+ all_custom_fields = self.db.custom_field_keys()
+ # Populate the 'Read book' hybrid
+ custom_fields = {}
+ custom_fields['Tag'] = {'field':'tag', 'datatype':u'text'}
+ for custom_field in all_custom_fields:
+ field_md = self.db.metadata_for_field(custom_field)
+ if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
+ custom_fields[field_md['name']] = {'field':custom_field,
+ 'datatype':field_md['datatype']}
+ # Add the sorted eligible fields to the combo box
+ for cf in sorted(custom_fields):
+ self.read_source_field.addItem(cf)
+ self.read_source_fields = custom_fields
+ self.read_source_field.currentIndexChanged.connect(self.read_source_field_changed)
+
+
+ # Populate the 'Excluded books' hybrid
+ custom_fields = {}
+ for custom_field in all_custom_fields:
+ field_md = self.db.metadata_for_field(custom_field)
+ if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
+ custom_fields[field_md['name']] = {'field':custom_field,
+ 'datatype':field_md['datatype']}
+ # Blank field first
+ self.exclude_source_field.addItem('')
+ # Add the sorted eligible fields to the combo box
+ for cf in sorted(custom_fields):
+ self.exclude_source_field.addItem(cf)
+ self.exclude_source_fields = custom_fields
+ self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed)
+
+
+ # Populate the 'Header note' combo box
+ custom_fields = {}
+ for custom_field in all_custom_fields:
+ field_md = self.db.metadata_for_field(custom_field)
+ if field_md['datatype'] in ['composite','datetime','enumeration','text']:
+ custom_fields[field_md['name']] = {'field':custom_field,
+ 'datatype':field_md['datatype']}
+ # Blank field first
+ self.header_note_source_field.addItem('')
+ # Add the sorted eligible fields to the combo box
+ for cf in sorted(custom_fields):
+ self.header_note_source_field.addItem(cf)
+ self.header_note_source_fields = custom_fields
+ self.header_note_source_field.currentIndexChanged.connect(self.header_note_source_field_changed)
+
+
+ # Populate the 'Merge with Comments' combo box
+ custom_fields = {}
+ for custom_field in all_custom_fields:
+ field_md = self.db.metadata_for_field(custom_field)
+ if field_md['datatype'] in ['text','comments']:
+ custom_fields[field_md['name']] = {'field':custom_field,
+ 'datatype':field_md['datatype']}
+ # Blank field first
+ self.merge_source_field.addItem('')
+ # Add the sorted eligible fields to the combo box
+ for cf in sorted(custom_fields):
+ self.merge_source_field.addItem(cf)
+ self.merge_source_fields = custom_fields
+ self.merge_source_field.currentIndexChanged.connect(self.merge_source_field_changed)
+ self.merge_before.setEnabled(False)
+ self.merge_after.setEnabled(False)
+ self.include_hr.setEnabled(False)
+
def read_source_field_changed(self,new_index):
'''
Process changes in the read_source_field combo box
Currently using QLineEdit for all field types
Possible to modify to switch QWidget type
'''
- new_source = str(self.read_source_field_cb.currentText())
+ new_source = str(self.read_source_field.currentText())
read_source_spec = self.read_source_fields[str(new_source)]
- self.read_source_field = read_source_spec['field']
+ self.read_source_field_name = read_source_spec['field']
# Change pattern input widget to match the source field datatype
if read_source_spec['datatype'] in ['bool','composite','datetime','text']:
@@ -152,3 +287,62 @@ class PluginWidget(QWidget,Ui_Form):
self.read_pattern = dw
self.read_spec_hl.addWidget(dw)
+ def exclude_source_field_changed(self,new_index):
+ '''
+ Process changes in the exclude_source_field combo box
+ Currently using QLineEdit for all field types
+ Possible to modify to switch QWidget type
+ '''
+ new_source = str(self.exclude_source_field.currentText())
+ self.exclude_source_field_name = new_source
+ if new_source > '':
+ exclude_source_spec = self.exclude_source_fields[str(new_source)]
+ self.exclude_source_field_name = exclude_source_spec['field']
+
+ # Change pattern input widget to match the source field datatype
+ if exclude_source_spec['datatype'] in ['bool','composite','datetime','text']:
+ if not isinstance(self.exclude_pattern, QLineEdit):
+ self.exclude_spec_hl.removeWidget(self.exclude_pattern)
+ dw = QLineEdit(self)
+ dw.setObjectName('exclude_pattern')
+ dw.setToolTip('Exclusion pattern')
+ self.exclude_pattern = dw
+ self.exclude_spec_hl.addWidget(dw)
+ else:
+ self.exclude_pattern.setText('')
+
+ def header_note_source_field_changed(self,new_index):
+ '''
+ Process changes in the header_note_source_field combo box
+ '''
+ new_source = str(self.header_note_source_field.currentText())
+ self.header_note_source_field_name = new_source
+ if new_source > '':
+ header_note_source_spec = self.header_note_source_fields[str(new_source)]
+ self.header_note_source_field_name = header_note_source_spec['field']
+
+ def merge_source_field_changed(self,new_index):
+ '''
+ Process changes in the header_note_source_field combo box
+ '''
+ new_source = str(self.merge_source_field.currentText())
+ self.merge_source_field_name = new_source
+ if new_source > '':
+ merge_source_spec = self.merge_source_fields[str(new_source)]
+ self.merge_source_field_name = merge_source_spec['field']
+ if not self.merge_before.isChecked() and not self.merge_after.isChecked():
+ self.merge_after.setChecked(True)
+ self.merge_before.setEnabled(True)
+ self.merge_after.setEnabled(True)
+ self.include_hr.setEnabled(True)
+
+ else:
+ self.merge_before.setEnabled(False)
+ self.merge_after.setEnabled(False)
+ self.include_hr.setEnabled(False)
+
+ def thumb_width_changed(self,new_value):
+ '''
+ Process changes in the thumb_width spin box
+ '''
+ pass
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
index d72566f581..481763c76d 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
@@ -6,163 +6,653 @@
0
0
- 627
- 549
+ 650
+ 582
+
+
+ 0
+ 0
+
+
Form
-
- -
-
-
- 'Don't include this book' tag:
+
+
-
+
+
+
+ 0
+ 0
+
-
-
- -
-
-
-
-
-
-
- -
-
-
- Additional note tag prefix:
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- Regex pattern describing tags to exclude as genres:
-
-
- Qt::LogText
-
-
- true
-
-
-
- -
-
-
- Regex tips:
-- The default regex - \[.+\] - excludes genre tags of the form [tag], e.g., [Amazon Freebie]
-- A regex pattern of a single dot excludes all genre tags, generating no Genre Section
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
+
- 20
- 40
+ 0
+ 0
-
-
- -
-
-
- Include 'Titles' Section
+
+ Sections to include in generated catalog. A minimal catalog includes 'Books by Author'.
+
+ Included sections (Books by Author included by default)
+
+
+
-
+
+
+ Books by Title
+
+
+
+ -
+
+
+ Books by Series
+
+
+
+ -
+
+
+ Recently Added
+
+
+
+ -
+
+
+ Books by Genre
+
+
+
+ -
+
+
+ Descriptions
+
+
+
+
- -
-
-
- Include 'Recently Added' Section
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+ 0
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Default pattern </p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Courier New,courier';">\[.+\]</span></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">excludes tags of the form [<span style=" font-style:italic;">tag</span>]</p></body></html>
+
+
+ Excluded genres
+
+
+
+ QFormLayout::FieldsStayAtSizeHint
+
+
-
+
+
+ -1
+
+
+ 0
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Tags to exclude
+
+
+ Qt::AutoText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
- -
-
-
- Sort numbers as text
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+ 0
+
+
+
+ Exclude matching books from generated catalog
+
+
+ Excluded books
+
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Tags to exclude
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt;">Comma-separated list of tags to exclude.</span></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt;">Default:</span><span style=" font-family:'Courier New,courier'; font-size:12pt;"> ~,Catalog</span></p></body></html>
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Column/value
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Column containing exclusion criteria
+
+
+ QComboBox::AdjustToMinimumContentsLengthWithIcon
+
+
+ 18
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+ Exclusion pattern
+
+
+
+
+
+
- -
-
-
- Include 'Series' Section
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+ 0
+
+
+
+ Matching books will be displayed with ✓
+
+
+ Read books
+
+
+
-
+
+
+ QLayout::SetDefaultConstraint
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Column/value
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Column containing 'read' status
+
+
+
+
+
+ QComboBox::AdjustToMinimumContentsLengthWithIcon
+
+
+ 18
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+ 'read book' pattern
+
+
+
+
+
+
+
+
+
- -
-
-
- -
-
-
- Wishlist tag:
+
-
+
+
+
+ 0
+ 0
+
-
-
- -
-
-
- QLayout::SetMinimumSize
+
+
+ 0
+ 0
+
-
-
-
-
-
- 0
- 0
-
-
-
- Source column for read book
-
-
-
-
-
-
- -
-
-
- Pattern for read book
-
-
-
-
-
-
-
-
- -
-
-
- Books marked as read:
+
+ Other options
+
+
+ QFormLayout::FieldsStayAtSizeHint
+
+
-
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+
+
+
+ Wishlist tag
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+
+ -
+
+
+ Wishlist items will be displayed with ✕
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Thumbnail width
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Size hint for cover thumbnails included in Descriptions
+
+
+ "
+
+
+ 2
+
+
+ 1.000000000000000
+
+
+ 2.000000000000000
+
+
+ 0.100000000000000
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Header note
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Column containing header note
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Merge with Comments
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Column containing additional content to merge
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Merge before Comments
+
+
+ Before
+
+
+
+ -
+
+
+ Merge after Comments
+
+
+ After
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Separate with horizontal rule
+
+
+ <hr />
+
+
+
+
+
+
diff --git a/src/calibre/gui2/catalog/catalog_tab_template.ui b/src/calibre/gui2/catalog/catalog_tab_template.ui
index 5df881beac..4b24507f80 100644
--- a/src/calibre/gui2/catalog/catalog_tab_template.ui
+++ b/src/calibre/gui2/catalog/catalog_tab_template.ui
@@ -6,8 +6,8 @@
0
0
- 579
- 411
+ 650
+ 575
diff --git a/src/calibre/gui2/dialogs/catalog.ui b/src/calibre/gui2/dialogs/catalog.ui
index e1de9407ea..62ac7cb5af 100644
--- a/src/calibre/gui2/dialogs/catalog.ui
+++ b/src/calibre/gui2/dialogs/catalog.ui
@@ -6,8 +6,8 @@
0
0
- 611
- 514
+ 674
+ 660
@@ -33,6 +33,18 @@
-
+
+
+ 0
+ 0
+
+
+
+
+ 650
+ 575
+
+
0
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index 0b317d6a6e..5eb20159c1 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -550,6 +550,13 @@ class EPUB_MOBI(CatalogPlugin):
"of the conversion process a bug is occurring.\n"
"Default: '%default'None\n"
"Applies to: ePub, MOBI output formats")),
+ Option('--exclude-book-marker',
+ default=':',
+ dest='exclude_book_marker',
+ action = None,
+ help=_("field:pattern specifying custom field/contents indicating book should be excluded.\n"
+ "Default: '%default'\n"
+ "Applies to ePub, MOBI output formats")),
Option('--exclude-genre',
default='\[.+\]',
dest='exclude_genre',
@@ -585,6 +592,23 @@ class EPUB_MOBI(CatalogPlugin):
help=_("Include 'Recently Added' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
+ Option('--header-note-source-field',
+ default='',
+ dest='header_note_source_field',
+ action = None,
+ help=_("Custom field containing note text to insert in Description header.\n"
+ "Default: '%default'\n"
+ "Applies to: ePub, MOBI output formats")),
+ Option('--merge-comments',
+ default='::',
+ dest='merge_comments',
+ action = None,
+ help=_(":[before|after]:[True|False] specifying:\n"
+ " Custom field containing notes to merge with Comments\n"
+ " [before|after] Placement of notes with respect to Comments\n"
+ " [True|False] - A horizontal rule is inserted between notes and Comments\n"
+ "Default: '%default'\n"
+ "Applies to ePub, MOBI output formats")),
Option('--note-tag',
default='*',
dest='note_tag',
@@ -845,6 +869,7 @@ class EPUB_MOBI(CatalogPlugin):
catalog.copyResources()
catalog.buildSources()
'''
+
# A single number creates 'Last x days' only.
# Multiple numbers create 'Last x days', 'x to y days ago' ...
# e.g, [7,15,30,60], [30]
@@ -889,6 +914,7 @@ class EPUB_MOBI(CatalogPlugin):
and self.generateForKindle \
else False
self.__genres = None
+ self.genres = []
self.__genre_tags_dict = None
self.__htmlFileList = []
self.__markerTags = self.getMarkerTags()
@@ -900,13 +926,15 @@ class EPUB_MOBI(CatalogPlugin):
self.__progressString = ''
f, _, p = opts.read_book_marker.partition(':')
self.__read_book_marker = {'field':f, 'pattern':p}
+ f, p, hr = self.opts.merge_comments.split(':')
+ self.__merge_comments = {'field':f, 'position':p, 'hr':hr}
self.__reporter = report_progress
self.__stylesheet = stylesheet
self.__thumbs = None
self.__thumbWidth = 0
self.__thumbHeight = 0
self.__title = opts.catalog_title
- self.__totalSteps = 11.0
+ self.__totalSteps = 8.0
self.__useSeriesPrefixInTitlesSection = False
self.__verbose = opts.verbose
@@ -916,17 +944,36 @@ class EPUB_MOBI(CatalogPlugin):
self.__output_profile = profile
break
- # Confirm/create thumbs archive
+ # Confirm/create thumbs archive.
if not os.path.exists(self.__cache_dir):
self.opts.log.info(" creating new thumb cache '%s'" % self.__cache_dir)
os.makedirs(self.__cache_dir)
if not os.path.exists(self.__archive_path):
- self.opts.log.info(" creating thumbnail archive")
+ self.opts.log.info(' creating thumbnail archive, thumb_width: %1.2f"' %
+ float(self.opts.thumb_width))
zfw = ZipFile(self.__archive_path, mode='w')
zfw.writestr("Catalog Thumbs Archive",'')
+ zfw.comment = "thumb_width: %1.2f" % float(self.opts.thumb_width)
zfw.close()
else:
- self.opts.log.info(" existing thumb cache at '%s'" % self.__archive_path)
+ with closing(ZipFile(self.__archive_path, mode='r')) as zfr:
+ try:
+ cached_thumb_width = float(zfr.comment[len('thumb_width: '):])
+ except:
+ cached_thumb_width = "0.0"
+
+ if float(cached_thumb_width) != float(self.opts.thumb_width):
+ self.opts.log.info(" invalidating cache at '%s'" % self.__archive_path)
+ self.opts.log.info(' thumb_width: %1.2f" => %1.2f"' %
+ (float(cached_thumb_width),float(self.opts.thumb_width)))
+ os.remove(self.__archive_path)
+ zfw = ZipFile(self.__archive_path, mode='w')
+ zfw.writestr("Catalog Thumbs Archive",'')
+ zfw.comment = "thumb_width: %1.2f" % float(self.opts.thumb_width)
+ zfw.close()
+ else:
+ self.opts.log.info(' existing thumb cache at %s, cached_thumb_width: %1.2f"' %
+ (self.__archive_path, float(cached_thumb_width)))
# Tweak build steps based on optional sections: 1 call for HTML, 1 for NCX
if self.opts.generate_titles:
@@ -937,6 +984,9 @@ class EPUB_MOBI(CatalogPlugin):
self.__totalSteps += 2
if self.opts.generate_series:
self.__totalSteps += 2
+ if self.opts.generate_descriptions:
+ # +1 thumbs
+ self.__totalSteps += 3
# Accessors
if True:
@@ -1246,7 +1296,8 @@ class EPUB_MOBI(CatalogPlugin):
return False
self.fetchBooksByAuthor()
self.fetchBookmarks()
- self.generateHTMLDescriptions()
+ if self.opts.generate_descriptions:
+ self.generateHTMLDescriptions()
self.generateHTMLByAuthor()
if self.opts.generate_titles:
self.generateHTMLByTitle()
@@ -1256,10 +1307,10 @@ class EPUB_MOBI(CatalogPlugin):
self.generateHTMLByDateAdded()
if self.generateRecentlyRead:
self.generateHTMLByDateRead()
- self.generateHTMLByTags()
-
- self.generateThumbnails()
-
+ if self.opts.generate_genres:
+ self.generateHTMLByTags()
+ if self.opts.generate_descriptions:
+ self.generateThumbnails()
self.generateOPF()
self.generateNCXHeader()
self.generateNCXByAuthor("Authors")
@@ -1271,8 +1322,11 @@ class EPUB_MOBI(CatalogPlugin):
self.generateNCXByDateAdded("Recently Added")
if self.generateRecentlyRead:
self.generateNCXByDateRead("Recently Read")
- self.generateNCXByGenre("Genres")
- self.generateNCXDescriptions("Descriptions")
+ if self.opts.generate_genres:
+ self.generateNCXByGenre("Genres")
+ if self.opts.generate_descriptions:
+ self.generateNCXDescriptions("Descriptions")
+
self.writeNCX()
return True
@@ -1340,6 +1394,7 @@ class EPUB_MOBI(CatalogPlugin):
#print "fetchBooksByTitle(): opts.search_text: %s" % self.opts.search_text
# Fetch the database as a dictionary
data = self.plugin.search_sort_db(self.db, self.opts)
+ data = self.processExclusions(data)
# Populate this_title{} from data[{},{}]
titles = []
@@ -1388,6 +1443,8 @@ class EPUB_MOBI(CatalogPlugin):
record['comments'] = record['comments'][:ad_offset]
this_title['description'] = self.markdownComments(record['comments'])
+
+ # Create short description
paras = BeautifulSoup(this_title['description']).findAll('p')
tokens = []
for p in paras:
@@ -1399,6 +1456,10 @@ class EPUB_MOBI(CatalogPlugin):
this_title['description'] = None
this_title['short_description'] = None
+ # Merge with custom field/value
+ if self.__merge_comments['field']:
+ this_title['description'] = self.mergeComments(this_title)
+
if record['cover']:
this_title['cover'] = re.sub('&', '&', record['cover'])
@@ -1413,6 +1474,14 @@ class EPUB_MOBI(CatalogPlugin):
formats.append(self.convertHTMLEntities(format))
this_title['formats'] = formats
+ # Add user notes to be displayed in header
+ if self.opts.header_note_source_field:
+ notes = self.__db.get_field(record['id'],
+ self.opts.header_note_source_field,
+ index_is_id=True)
+ if notes:
+ this_title['notes'] = notes
+
titles.append(this_title)
# Re-sort based on title_sort
@@ -1712,7 +1781,8 @@ class EPUB_MOBI(CatalogPlugin):
for tag in title.get('tags', []):
aTag = Tag(soup,'a')
#print "aTag: %s" % "Genre_%s.html" % re.sub("\W","",tag.lower())
- aTag['href'] = "Genre_%s.html" % re.sub("\W","",tag.lower())
+ if self.opts.generate_genres:
+ aTag['href'] = "Genre_%s.html" % re.sub("\W","",tag.lower())
aTag.insert(0,escape(NavigableString(tag)))
emTag = Tag(soup, "em")
emTag.insert(0, aTag)
@@ -1771,7 +1841,6 @@ class EPUB_MOBI(CatalogPlugin):
#ratingLabel = body.find('td',text="Rating").replaceWith("Unrated")
ratingTag.insert(0,NavigableString('
'))
-
# Insert user notes or remove Notes label. Notes > 1 line will push formatting down
if 'notes' in title:
notesTag = body.find(attrs={'class':'notes'})
@@ -1894,7 +1963,8 @@ class EPUB_MOBI(CatalogPlugin):
# Link to book
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(book['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(book['id'])))
aTag.insert(0,escape(book['title']))
pBookTag.insert(ptc, aTag)
ptc += 1
@@ -2067,8 +2137,9 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(book['id'])))
- # Use series, series index if avail else just title, + year of publication
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(book['id'])))
+ # Use series, series index if avail else title, + year of publication
if current_series:
aTag.insert(0,'%s (%s)' % (escape(book['title'][len(book['series'])+1:]),
book['date'].split()[1]))
@@ -2079,7 +2150,6 @@ class EPUB_MOBI(CatalogPlugin):
pBookTag.insert(ptc, aTag)
ptc += 1
-
divTag.insert(dtc, pBookTag)
dtc += 1
@@ -2200,7 +2270,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
if current_series:
aTag.insert(0,escape(new_entry['title'][len(new_entry['series'])+1:]))
else:
@@ -2251,7 +2322,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
aTag.insert(0,escape(new_entry['title']))
pBookTag.insert(ptc, aTag)
ptc += 1
@@ -2411,7 +2483,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
aTag.insert(0,escape(new_entry['title']))
pBookTag.insert(ptc, aTag)
ptc += 1
@@ -2458,7 +2531,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
aTag.insert(0,escape(new_entry['title']))
pBookTag.insert(ptc, aTag)
ptc += 1
@@ -2699,7 +2773,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(book['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(book['id'])))
# Use series, series index if avail else just title
#aTag.insert(0,'%d. %s · %s' % (book['series_index'],escape(book['title']), ' & '.join(book['authors'])))
@@ -2983,18 +3058,20 @@ class EPUB_MOBI(CatalogPlugin):
manifest.insert(mtc, itemTag)
mtc += 1
- # Write the thumbnail images to the manifest
- for thumb in self.thumbs:
- itemTag = Tag(soup, "item")
- itemTag['href'] = "images/%s" % (thumb)
- end = thumb.find('.jpg')
- itemTag['id'] = "%s-image" % thumb[:end]
- itemTag['media-type'] = 'image/jpeg'
- manifest.insert(mtc, itemTag)
- mtc += 1
+ # Write the thumbnail images, descriptions to the manifest
+ sort_descriptions_by = []
+ if self.opts.generate_descriptions:
+ for thumb in self.thumbs:
+ itemTag = Tag(soup, "item")
+ itemTag['href'] = "images/%s" % (thumb)
+ end = thumb.find('.jpg')
+ itemTag['id'] = "%s-image" % thumb[:end]
+ itemTag['media-type'] = 'image/jpeg'
+ manifest.insert(mtc, itemTag)
+ mtc += 1
- # HTML files - add books to manifest and spine
- sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \
+ # HTML files - add descriptions to manifest and spine
+ sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \
else self.booksByTitle
# Add html_files to manifest and spine
@@ -3970,15 +4047,15 @@ class EPUB_MOBI(CatalogPlugin):
from calibre.customize.ui import output_profiles
for x in output_profiles():
if x.short_name == self.opts.output_profile:
- # .9" width aspect ratio: 3:4
- self.thumbWidth = int(x.dpi * 1)
- self.thumbHeight = int(self.thumbWidth * 1.33)
+ # aspect ratio: 3:4
+ self.thumbWidth = x.dpi * float(self.opts.thumb_width)
+ self.thumbHeight = self.thumbWidth * 1.33
if 'kindle' in x.short_name and self.opts.fmt == 'mobi':
# Kindle DPI appears to be off by a factor of 2
- self.thumbWidth = int(self.thumbWidth/2)
- self.thumbHeight = int(self.thumbHeight/2)
+ self.thumbWidth = self.thumbWidth/2
+ self.thumbHeight = self.thumbHeight/2
break
- if False and self.verbose:
+ if True and self.verbose:
self.opts.log(" DPI = %d; thumbnail dimensions: %d x %d" % \
(x.dpi, self.thumbWidth, self.thumbHeight))
@@ -4238,7 +4315,8 @@ class EPUB_MOBI(CatalogPlugin):
# Add the book title
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(book['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(book['id'])))
# Use series, series index if avail else just title
if current_series:
aTag.insert(0,escape(book['title'][len(book['series'])+1:]))
@@ -4460,7 +4538,9 @@ class EPUB_MOBI(CatalogPlugin):
# Leading numbers optionally translated to text equivalent
# Capitalize leading sort word
if i==0:
- if self.opts.numbers_as_text and re.match('[0-9]+',word[0]):
+ # *** Keep this code in case we need to restore numbers_as_text ***
+ if False:
+ #if self.opts.numbers_as_text and re.match('[0-9]+',word[0]):
translated.append(EPUB_MOBI.NumberToText(word).text.capitalize())
else:
if re.match('[0-9]+',word[0]):
@@ -4540,7 +4620,6 @@ class EPUB_MOBI(CatalogPlugin):
''' Return a list of special marker tags to be excluded from genre list '''
markerTags = []
markerTags.extend(self.opts.exclude_tags.split(','))
- markerTags.extend(self.opts.note_tag.split(','))
return markerTags
def letter_or_symbol(self,char):
@@ -4663,13 +4742,63 @@ class EPUB_MOBI(CatalogPlugin):
return result.renderContents(encoding=None)
+ def mergeComments(self, record):
+ '''
+ merge ['description'] with custom field contents to be displayed in Descriptions
+ '''
+ merged = ''
+ if record['description']:
+ addendum = self.__db.get_field(record['id'],
+ self.__merge_comments['field'],
+ index_is_id=True)
+ include_hr = eval(self.__merge_comments['hr'])
+ if self.__merge_comments['position'] == 'before':
+ merged = addendum
+ if include_hr:
+ merged += ''
+ else:
+ merged += '\n'
+ merged += record['description']
+ else:
+ merged = record['description']
+ if include_hr:
+ merged += ''
+ else:
+ merged += '\n'
+ merged += addendum
+ else:
+ # Return the custom field contents
+ merged = self.__db.get_field(record['id'],
+ self.__merge_comments['field'],
+ index_is_id=True)
+
+ return merged
+
+ def processExclusions(self, data_set):
+ '''
+ Remove excluded entries
+ '''
+ field, pat = self.opts.exclude_book_marker.split(':')
+ if pat == '':
+ return data_set
+ filtered_data_set = []
+ for record in data_set:
+ field_contents = self.__db.get_field(record['id'],
+ field,
+ index_is_id=True)
+ if field_contents:
+ if re.search(pat, unicode(field_contents),
+ re.IGNORECASE) is not None:
+ continue
+ filtered_data_set.append(record)
+
+ return filtered_data_set
+
def processSpecialTags(self, tags, this_title, opts):
tag_list = []
for tag in tags:
tag = self.convertHTMLEntities(tag)
- if tag.startswith(opts.note_tag):
- this_title['notes'] = tag[len(self.opts.note_tag):]
- elif re.search(opts.exclude_genre, tag):
+ if re.search(opts.exclude_genre, tag):
continue
elif self.__read_book_marker['field'] == 'tag' and \
tag == self.__read_book_marker['pattern']:
@@ -4767,13 +4896,16 @@ class EPUB_MOBI(CatalogPlugin):
if opts_dict['ids']:
build_log.append(" book count: %d" % len(opts_dict['ids']))
- sections_list = ['Descriptions','Authors']
+ sections_list = ['Authors']
if opts.generate_titles:
sections_list.append('Titles')
if opts.generate_recently_added:
sections_list.append('Recently Added')
- if not opts.exclude_genre.strip() == '.':
+ if opts.generate_genres:
sections_list.append('Genres')
+ if opts.generate_descriptions:
+ sections_list.append('Descriptions')
+
build_log.append(u" Sections: %s" % ', '.join(sections_list))
# Display opts
@@ -4782,11 +4914,12 @@ class EPUB_MOBI(CatalogPlugin):
build_log.append(" opts:")
for key in keys:
if key in ['catalog_title','authorClip','connected_kindle','descriptionClip',
- 'exclude_genre','exclude_tags','note_tag','numbers_as_text',
+ 'exclude_book_marker','exclude_genre','exclude_tags',
+ 'header_note_source_field','merge_comments',
'output_profile','read_book_marker',
'search_text','sort_by','sort_descriptions_by_author','sync',
- 'wishlist_tag']:
- build_log.append(" %s: %s" % (key, opts_dict[key]))
+ 'thumb_width','wishlist_tag']:
+ build_log.append(" %s: %s" % (key, repr(opts_dict[key])))
if opts.verbose:
log('\n'.join(line for line in build_log))
@@ -4801,6 +4934,7 @@ class EPUB_MOBI(CatalogPlugin):
catalog.copyResources()
catalog.calculateThumbnailSize()
catalog_source_built = catalog.buildSources()
+
if opts.verbose:
if catalog_source_built:
log.info(" Completed catalog source generation\n")