From 230d92dcb562705fb18fb9578431074e07697fab Mon Sep 17 00:00:00 2001 From: GRiker Date: Thu, 23 Dec 2010 12:49:53 -0700 Subject: [PATCH] GwR catalog UI revisions --- resources/catalog/stylesheet.css | 21 +- src/calibre/gui2/actions/catalog.py | 2 +- src/calibre/gui2/catalog/catalog_epub_mobi.py | 356 +++++++-- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 756 +++++++++++++++--- .../gui2/catalog/catalog_tab_template.ui | 4 +- src/calibre/gui2/dialogs/catalog.ui | 16 +- src/calibre/library/catalog.py | 234 ++++-- 7 files changed, 1114 insertions(+), 275 deletions(-) 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")