diff --git a/resources/jacket/stylesheet.css b/resources/jacket/stylesheet.css index 5f4f012d01..c45f8fe977 100644 --- a/resources/jacket/stylesheet.css +++ b/resources/jacket/stylesheet.css @@ -36,22 +36,37 @@ /* ** Title */ -.cbj_title { +table.cbj_header td.cbj_title { font-size: x-large; + font-style: italic; + text-align: center; +} + +/* +** Series +*/ +table.cbj_header td.cbj_series { + font-size: medium; text-align: center; } /* ** Author */ -.cbj_author { +table.cbj_header td.cbj_author { font-size: medium; text-align: center; - margin-bottom: 1ex; } /* -** Table containing Series, Publication Year, Rating and Tags +** Publisher/published +*/ +table.cbj_header td.cbj_pubdata { + text-align: center; +} + +/* +** Table containing Rating and Tags */ table.cbj_header { width: 100%; @@ -62,9 +77,8 @@ table.cbj_header { */ table.cbj_header td.cbj_label { font-family: sans-serif; - font-weight: bold; text-align: right; - width: 40%; + width: 33%; } /* @@ -73,9 +87,23 @@ table.cbj_header td.cbj_label { table.cbj_header td.cbj_content { font-family: sans-serif; text-align: left; - width:60%; + width:67%; } +/* +** Metadata divider +*/ +hr.metadata_divider { + width:90%; + margin-left:5%; + border-top: solid white 0px; + border-right: solid white 0px; + border-bottom: solid black 1px; + border-left: solid white 0px; + } + + + /* ** To skip a banner item (Series|Published|Rating|Tags), ** edit the appropriate CSS rule below. diff --git a/resources/jacket/template.xhtml b/resources/jacket/template.xhtml index 8447b1d6b3..056ac0aad3 100644 --- a/resources/jacket/template.xhtml +++ b/resources/jacket/template.xhtml @@ -6,17 +6,24 @@
-
{title}
-
{author}
- - - + + + + + - - - + + + + + + + + + + diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py index e744a14389..84f2dd5d6a 100644 --- a/src/calibre/ebooks/oeb/transforms/jacket.py +++ b/src/calibre/ebooks/oeb/transforms/jacket.py @@ -93,7 +93,7 @@ class Jacket(object): # Render Jacket {{{ -def get_rating(rating, rchar): +def get_rating(rating, rchar, e_rchar): ans = '' try: num = float(rating)/2 @@ -104,12 +104,12 @@ def get_rating(rating, rchar): if num < 1: return ans - ans = rchar * int(num) + ans = ("%s%s") % (rchar * int(num), e_rchar * (5 - int(num))) return ans - def render_jacket(mi, output_profile, - alt_title=_('Unknown'), alt_tags=[], alt_comments=''): + alt_title=_('Unknown'), alt_tags=[], alt_comments='', + alt_publisher=('Unknown publisher')): css = P('jacket/stylesheet.css', data=True).decode('utf-8') try: @@ -124,12 +124,17 @@ def render_jacket(mi, output_profile, if not mi.series: series = '' + try: + publisher = mi.publisher if mi.publisher else alt_publisher + except: + publisher = _('Unknown publisher') + try: pubdate = strftime(u'%Y', mi.pubdate.timetuple()) except: pubdate = '' - rating = get_rating(mi.rating, output_profile.ratings_char) + rating = get_rating(mi.rating, output_profile.ratings_char, output_profile.empty_ratings_char) tags = mi.tags if mi.tags else alt_tags if tags: @@ -154,6 +159,7 @@ def render_jacket(mi, output_profile, css=css, title=title, author=author, + publisher=publisher, pubdate_label=_('Published'), pubdate=pubdate, series_label=_('Series'), series=series, rating_label=_('Rating'), rating=rating, @@ -168,16 +174,16 @@ def render_jacket(mi, output_profile, # Post-process the generated html to strip out empty header items soup = BeautifulSoup(generated_html) if not series: - series_tag = soup.find('tr', attrs={'class':'cbj_series'}) + series_tag = soup.find(attrs={'class':'cbj_series'}) series_tag.extract() if not rating: - rating_tag = soup.find('tr', attrs={'class':'cbj_rating'}) + rating_tag = soup.find(attrs={'class':'cbj_rating'}) rating_tag.extract() if not tags: - tags_tag = soup.find('tr', attrs={'class':'cbj_tags'}) + tags_tag = soup.find(attrs={'class':'cbj_tags'}) tags_tag.extract() if not pubdate: - pubdate_tag = soup.find('tr', attrs={'class':'cbj_pubdate'}) + pubdate_tag = soup.find(attrs={'class':'cbj_pubdate'}) pubdate_tag.extract() if output_profile.short_name != 'kindle': hr_tag = soup.find('hr', attrs={'class':'cbj_kindle_banner_hr'}) diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py index 77fa4755c1..a253664a1e 100644 --- a/src/calibre/gui2/actions/catalog.py +++ b/src/calibre/gui2/actions/catalog.py @@ -37,7 +37,8 @@ class GenerateCatalogAction(InterfaceAction): dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)} # Calling gui2.tools:generate_catalog() - ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager) + ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager, + db) if ret is None: return diff --git a/src/calibre/gui2/catalog/catalog_bibtex.py b/src/calibre/gui2/catalog/catalog_bibtex.py index 6bbe85833c..5030cf6ec8 100644 --- a/src/calibre/gui2/catalog/catalog_bibtex.py +++ b/src/calibre/gui2/catalog/catalog_bibtex.py @@ -34,7 +34,7 @@ class PluginWidget(QWidget, Ui_Form): self.all_fields.append(x) QListWidgetItem(x, self.db_fields) - def initialize(self, name): #not working properly to update + def initialize(self, name, db): #not working properly to update self.name = name fields = gprefs.get(name+'_db_fields', self.all_fields) # Restore the activated db_fields from last use diff --git a/src/calibre/gui2/catalog/catalog_csv_xml.py b/src/calibre/gui2/catalog/catalog_csv_xml.py index 7ccb5e017e..077d4cbbca 100644 --- a/src/calibre/gui2/catalog/catalog_csv_xml.py +++ b/src/calibre/gui2/catalog/catalog_csv_xml.py @@ -28,7 +28,7 @@ class PluginWidget(QWidget, Ui_Form): self.all_fields.append(x) QListWidgetItem(x, self.db_fields) - def initialize(self, name): + def initialize(self, name, db): self.name = name fields = gprefs.get(name+'_db_fields', self.all_fields) # Restore the activated fields from last use diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index ea4edb10b9..1ae4efd014 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -7,10 +7,11 @@ __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from calibre.gui2 import gprefs -from catalog_epub_mobi_ui import Ui_Form from calibre.ebooks.conversion.config import load_defaults -from PyQt4.Qt import QWidget +from calibre.gui2 import gprefs + +from catalog_epub_mobi_ui import Ui_Form +from PyQt4.Qt import QWidget, QLineEdit class PluginWidget(QWidget,Ui_Form): @@ -23,7 +24,8 @@ class PluginWidget(QWidget,Ui_Form): ('generate_recently_added', True), ('note_tag','*'), ('numbers_as_text', False), - ('read_tag','+'), + ('read_pattern','+'), + ('read_source_field_cb','Tag'), ('wishlist_tag','Wishlist'), ] @@ -38,16 +40,54 @@ class PluginWidget(QWidget,Ui_Form): QWidget.__init__(self, parent) self.setupUi(self) - def initialize(self, name): + 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) + # 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 ['numbers_as_text','generate_titles','generate_series','generate_recently_added']: + if opt[0] in [ + 'generate_recently_added', + 'generate_series', + 'generate_titles', + 'numbers_as_text', + ]: getattr(self, opt[0]).setChecked(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()) + read_source_spec = self.read_source_fields[cs] + self.read_source_field = read_source_spec['field'] + def options(self): # Save/return the current options # exclude_genre stores literally @@ -55,16 +95,60 @@ class PluginWidget(QWidget,Ui_Form): # others store as lists opts_dict = {} for opt in self.OPTION_FIELDS: - if opt[0] in ['numbers_as_text','generate_titles','generate_series','generate_recently_added']: + # 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() + + # Combo box uses .currentText() + elif opt[0] in ['read_source_field_cb']: + opt_value = unicode(getattr(self, opt[0]).currentText()) + + # text fields use .text() else: opt_value = unicode(getattr(self, opt[0]).text()) gprefs.set(self.name + '_' + opt[0], opt_value) - if opt[0] in ['exclude_genre','numbers_as_text','generate_titles','generate_series','generate_recently_added']: + # 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(',') - opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']] + # Generate read_book_marker + opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field, self.read_pattern.text()) + + # Append the output profile + opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']] return opts_dict + + 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()) + read_source_spec = self.read_source_fields[str(new_source)] + self.read_source_field = read_source_spec['field'] + + # Change pattern input widget to match the source field datatype + if read_source_spec['datatype'] in ['bool','composite','datetime','text']: + if not isinstance(self.read_pattern, QLineEdit): + self.read_spec_hl.removeWidget(self.read_pattern) + dw = QLineEdit(self) + dw.setObjectName('read_pattern') + dw.setToolTip('Pattern for read book') + self.read_pattern = dw + self.read_spec_hl.addWidget(dw) + diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 3956886c4a..d72566f581 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -6,8 +6,8 @@ 0 0 - 579 - 411 + 627 + 549 @@ -28,42 +28,28 @@ - - - - 'Mark this book as read' tag: - - - - - - - - - - - + Additional note tag prefix: - + - + - + Regex pattern describing tags to exclude as genres: @@ -76,7 +62,7 @@ - + Regex tips: @@ -88,7 +74,7 @@ - + Qt::Vertical @@ -101,44 +87,84 @@ - + Include 'Titles' Section - + Include 'Recently Added' Section - + Sort numbers as text - + Include 'Series' Section - + - + Wishlist tag: + + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + Source column for read book + + + + + + + + + + Pattern for read book + + + + + + + + + + + + Books marked as read: + + + diff --git a/src/calibre/gui2/dialogs/catalog.py b/src/calibre/gui2/dialogs/catalog.py index f8e0f83746..7bb288ed20 100644 --- a/src/calibre/gui2/dialogs/catalog.py +++ b/src/calibre/gui2/dialogs/catalog.py @@ -19,7 +19,7 @@ from calibre.customize.ui import catalog_plugins class Catalog(QDialog, Ui_Dialog): ''' Catalog Dialog builder''' - def __init__(self, parent, dbspec, ids): + def __init__(self, parent, dbspec, ids, db): import re, cStringIO from calibre import prints as info from PyQt4.uic import compileUi @@ -51,7 +51,7 @@ class Catalog(QDialog, Ui_Dialog): catalog_widget = __import__('calibre.gui2.catalog.'+name, fromlist=[1]) pw = catalog_widget.PluginWidget() - pw.initialize(name) + pw.initialize(name, db) pw.ICON = I('forward.png') self.widgets.append(pw) [self.fmts.append([file_type.upper(), pw.sync_enabled,pw]) for file_type in plugin.file_types] diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index 2919493d12..fc84e88d09 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -245,11 +245,11 @@ def fetch_scheduled_recipe(arg): return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt] -def generate_catalog(parent, dbspec, ids, device_manager): +def generate_catalog(parent, dbspec, ids, device_manager, db): from calibre.gui2.dialogs.catalog import Catalog # Build the Catalog dialog in gui2.dialogs.catalog - d = Catalog(parent, dbspec, ids) + d = Catalog(parent, dbspec, ids, db) if d.exec_() != d.Accepted: return None diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 33525f6540..b84ec99bed 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -606,12 +606,12 @@ class EPUB_MOBI(CatalogPlugin): help=_("Specifies the output profile. In some cases, an output profile is required to optimize the catalog for the device. For example, 'kindle' or 'kindle_dx' creates a structured Table of Contents with Sections and Articles.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), - Option('--read-tag', - default='+', - dest='read_tag', + Option('--read-book-marker', + default='tag:+', + dest='read_book_marker', action = None, - help=_("Tag indicating book has been read.\n" "Default: '%default'\n" - "Applies to: ePub, MOBI output formats")), + help=_("field:pattern indicating book has been read.\n" "Default: '%default'\n" + "Applies to ePub, MOBI output formats")), Option('--wishlist-tag', default='Wishlist', dest='wishlist_tag', @@ -898,6 +898,8 @@ class EPUB_MOBI(CatalogPlugin): self.__plugin = plugin self.__progressInt = 0.0 self.__progressString = '' + f, _, p = opts.read_book_marker.partition(':') + self.__read_book_marker = {'field':f, 'pattern':p} self.__reporter = report_progress self.__stylesheet = stylesheet self.__thumbs = None @@ -936,7 +938,6 @@ class EPUB_MOBI(CatalogPlugin): if self.opts.generate_series: self.__totalSteps += 2 - # Accessors if True: ''' @@ -1210,7 +1211,7 @@ class EPUB_MOBI(CatalogPlugin): def READING_SYMBOL(self): def fget(self): return '' if self.generateForKindle else \ - '%s' % self.opts.read_tag + '+' return property(fget=fget) @dynamic_property def READ_SYMBOL(self): @@ -1402,7 +1403,7 @@ class EPUB_MOBI(CatalogPlugin): this_title['cover'] = re.sub('&', '&', record['cover']) # This may be updated in self.processSpecialTags() - this_title['read'] = False + this_title['read'] = self.discoverReadStatus(record) if record['tags']: this_title['tags'] = self.processSpecialTags(record['tags'], @@ -2675,13 +2676,14 @@ class EPUB_MOBI(CatalogPlugin): pBookTag = Tag(soup, "p") ptc = 0 + # THIS SHOULDN'T BE NECESSARY # book with read/reading/unread symbol - for tag in book['tags']: - if tag == self.opts.read_tag: - book['read'] = True - break - else: - book['read'] = False +# for tag in book['tags']: +# if tag == self.opts.read_tag: +# book['read'] = True +# break +# else: +# book['read'] = False # book with read|reading|unread symbol or wishlist item if self.opts.wishlist_tag in book.get('tags', []): @@ -2689,7 +2691,7 @@ class EPUB_MOBI(CatalogPlugin): pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) ptc += 1 else: - if book['read']: + if book.get('read', False): # check mark pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) pBookTag['class'] = "read_book" @@ -4027,6 +4029,35 @@ class EPUB_MOBI(CatalogPlugin): if not os.path.isdir(images_path): os.makedirs(images_path) + def discoverReadStatus(self, record): + ''' + Given a field:pattern spec, discover if this book marked as read + + if field == tag, scan tags for pattern + if custom field, try regex match for pattern + This allows maximum flexibility with fields of type + datatype bool: #field_name:True + datatype text: #field_name: + datatype datetime: #field_name:.* + + ''' + # Legacy handling of special 'read' tag + field = self.__read_book_marker['field'] + pat = self.__read_book_marker['pattern'] + if field == 'tag' and pat in record['tags']: + return True + + 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: + return True + + return False + + def filterDbTags(self, tags): # Remove the special marker tags from the database's tag list, # return sorted list of normalized genre tags @@ -4519,7 +4550,7 @@ class EPUB_MOBI(CatalogPlugin): markerTags = [] markerTags.extend(self.opts.exclude_tags.split(',')) markerTags.extend(self.opts.note_tag.split(',')) - markerTags.extend(self.opts.read_tag.split(',')) + # Process read_book_marker if field is tag return markerTags def letter_or_symbol(self,char): @@ -4629,6 +4660,7 @@ class EPUB_MOBI(CatalogPlugin): if open_pTag: result.insert(rtc, pTag) + rtc += 1 paras = result.findAll('p') for p in paras: @@ -4647,10 +4679,12 @@ class EPUB_MOBI(CatalogPlugin): tag = self.convertHTMLEntities(tag) if tag.startswith(opts.note_tag): this_title['notes'] = tag[len(self.opts.note_tag):] - elif tag == opts.read_tag: - this_title['read'] = True elif re.search(opts.exclude_genre, tag): continue + elif self.__read_book_marker['field'] == 'tag' and \ + tag == self.__read_book_marker['pattern']: + # remove 'read' tag + continue else: tag_list.append(tag) return tag_list @@ -4759,7 +4793,7 @@ class EPUB_MOBI(CatalogPlugin): for key in keys: if key in ['catalog_title','authorClip','connected_kindle','descriptionClip', 'exclude_genre','exclude_tags','note_tag','numbers_as_text', - 'output_profile','read_tag', + '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])) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 7d3fb329e0..747cd59abb 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -640,7 +640,7 @@ def catalog_option_parser(args): log = Log() parser = get_parser(_( ''' - %prog catalog /path/to/destination.(csv|epub|mobi|xml ...) [options] + %prog catalog /path/to/destination.(CSV|EPUB|MOBI|XML ...) [options] Export a catalog in format specified by path/to/destination extension. Options control how entries are displayed in the generated catalog ouput.
{series_label}:{series}
{title}
{series}
{pubdate_label}:{pubdate}
{author}
{publisher} ({pubdate})
{rating_label}: {rating}