diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index ceeea863d3..5ab2f6b418 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -308,6 +308,10 @@ class FileTypePlugin(Plugin): # {{{ #: to the database on_import = False + #: If True, this plugin is run after books are added + #: to the database + on_postimport = False + #: If True, this plugin is run just before a conversion on_preprocess = False @@ -337,6 +341,16 @@ class FileTypePlugin(Plugin): # {{{ # Default implementation does nothing return path_to_ebook + def postimport(self, book_id, book_format, db): + ''' + Called post import, i.e., after the book file has been added to the database. + + :param book_id: Database id of the added book. + :param book_format: The file type of the book that was added. + :param db: Library database. + ''' + pass # Default implementation does nothing + # }}} class MetadataReaderPlugin(Plugin): # {{{ diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 2799ed747a..82c4f3f83c 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -104,14 +104,17 @@ def is_disabled(plugin): # File type plugins {{{ _on_import = {} +_on_postimport = {} _on_preprocess = {} _on_postprocess = {} def reread_filetype_plugins(): global _on_import + global _on_postimport global _on_preprocess global _on_postprocess _on_import = {} + _on_postimport = {} _on_preprocess = {} _on_postprocess = {} @@ -122,6 +125,10 @@ def reread_filetype_plugins(): if not _on_import.has_key(ft): _on_import[ft] = [] _on_import[ft].append(plugin) + if plugin.on_postimport: + if not _on_postimport.has_key(ft): + _on_postimport[ft] = [] + _on_postimport[ft].append(plugin) if plugin.on_preprocess: if not _on_preprocess.has_key(ft): _on_preprocess[ft] = [] @@ -163,6 +170,22 @@ run_plugins_on_preprocess = functools.partial(_run_filetype_plugins, occasion='preprocess') run_plugins_on_postprocess = functools.partial(_run_filetype_plugins, occasion='postprocess') + +def run_plugins_on_postimport(db, book_id, fmt): + customization = config['plugin_customization'] + fmt = fmt.lower() + for plugin in _on_postimport.get(fmt, []): + if is_disabled(plugin): + continue + plugin.site_customization = customization.get(plugin.name, '') + with plugin: + try: + plugin.postimport(book_id, fmt, db) + except: + print ('Running file type plugin %s failed with traceback:'% + plugin.name) + traceback.print_exc() + # }}} # Plugin customization {{{ diff --git a/src/calibre/ebooks/oeb/iterator/book.py b/src/calibre/ebooks/oeb/iterator/book.py index 70d6cea67f..15bf24c35d 100644 --- a/src/calibre/ebooks/oeb/iterator/book.py +++ b/src/calibre/ebooks/oeb/iterator/book.py @@ -49,8 +49,9 @@ class EbookIterator(BookmarksMixin): CHARACTERS_PER_PAGE = 1000 - def __init__(self, pathtoebook, log=None): + def __init__(self, pathtoebook, log=None, exclude_cover=False): self.log = log or default_log + self.exclude_cover = exclude_cover pathtoebook = pathtoebook.strip() self.pathtoebook = os.path.abspath(pathtoebook) self.config = DynamicConfig(name='iterator') @@ -142,16 +143,23 @@ class EbookIterator(BookmarksMixin): self.log.warn('Missing spine item:', repr(spath)) cover = self.opf.cover - if cover and self.ebook_ext in {'lit', 'mobi', 'prc', 'opf', 'fb2', - 'azw', 'azw3'}: - cfile = os.path.join(self.base, 'calibre_iterator_cover.html') - rcpath = os.path.relpath(cover, self.base).replace(os.sep, '/') - chtml = (TITLEPAGE%prepare_string_for_xml(rcpath, True)).encode('utf-8') - with open(cfile, 'wb') as f: - f.write(chtml) - self.spine[0:0] = [Spiny(cfile, - mime_type='application/xhtml+xml')] - self.delete_on_exit.append(cfile) + if cover: + if not self.exclude_cover and self.ebook_ext in { + 'lit', 'mobi', 'prc', 'opf', 'fb2', 'azw', 'azw3'}: + cfile = os.path.join(self.base, 'calibre_iterator_cover.html') + rcpath = os.path.relpath(cover, self.base).replace(os.sep, '/') + chtml = (TITLEPAGE%prepare_string_for_xml(rcpath, True)).encode('utf-8') + with open(cfile, 'wb') as f: + f.write(chtml) + self.spine[0:0] = [Spiny(cfile, + mime_type='application/xhtml+xml')] + self.delete_on_exit.append(cfile) + elif self.exclude_cover and self.ebook_ext == 'epub': + try: + if (len(self.spine) > 1 and self.spine.index(cover) == 0): + self.spine = self.spine[1:] + except ValueError: + pass if self.opf.path_to_html_toc is not None and \ self.opf.path_to_html_toc not in self.spine: diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 895bb5a6b2..14776d1363 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -93,6 +93,7 @@ gprefs.defaults['tag_browser_dont_collapse'] = [] gprefs.defaults['edit_metadata_single_layout'] = 'default' gprefs.defaults['default_author_link'] = 'http://en.wikipedia.org/w/index.php?search={author}' gprefs.defaults['preserve_date_on_ctl'] = True +gprefs.defaults['manual_add_auto_convert'] = False gprefs.defaults['cb_fullscreen'] = False gprefs.defaults['worker_max_time'] = 0 gprefs.defaults['show_files_after_save'] = True diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 5a7a991607..fe7ef72986 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -267,8 +267,8 @@ class ViewAction(InterfaceAction): def _view_books(self, rows): if not rows or len(rows) == 0: - self._launch_viewer() - return + return error_dialog(self.gui, _('Cannot view'), + _('No books selected'), show=True) if not self._view_check(len(rows)): return diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py index 234c72abf4..2b817b663a 100644 --- a/src/calibre/gui2/add.py +++ b/src/calibre/gui2/add.py @@ -42,6 +42,7 @@ class DuplicatesAdder(QObject): # {{{ # here we add all the formats for dupe book record created above self.db_adder.add_formats(id, formats) self.db_adder.number_of_books_added += 1 + self.db_adder.auto_convert_books.add(id) self.count += 1 self.added.emit(self.count) single_shot(self.add_one) @@ -107,8 +108,16 @@ class DBAdder(QObject): # {{{ self.input_queue = Queue() self.output_queue = Queue() self.merged_books = set([]) + self.auto_convert_books = set() def end(self): + if (gprefs['manual_add_auto_convert'] and + self.auto_convert_books): + from calibre.gui2.ui import get_gui + gui = get_gui() + gui.iactions['Convert Books'].auto_convert_auto_add( + self.auto_convert_books) + self.input_queue.put((None, None, None)) def start(self): @@ -152,7 +161,6 @@ class DBAdder(QObject): # {{{ fmts[-1] = fmt return fmts - def add(self, id, opf, cover, name): formats = self.ids.pop(id) if opf.endswith('.error'): @@ -219,6 +227,7 @@ class DBAdder(QObject): # {{{ self.duplicates.append((mi, cover, orig_formats)) else: self.add_formats(id_, formats) + self.auto_convert_books.add(id_) self.number_of_books_added += 1 else: self.names.append(name) diff --git a/src/calibre/gui2/preferences/adding.py b/src/calibre/gui2/preferences/adding.py index fafc5b5a1c..266b0ca9cc 100644 --- a/src/calibre/gui2/preferences/adding.py +++ b/src/calibre/gui2/preferences/adding.py @@ -28,6 +28,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('swap_author_names', prefs) r('add_formats_to_existing', prefs) r('preserve_date_on_ctl', gprefs) + r('manual_add_auto_convert', gprefs) choices = [ (_('Ignore duplicate incoming formats'), 'ignore'), (_('Overwrite existing duplicate formats'), 'overwrite'), diff --git a/src/calibre/gui2/preferences/adding.ui b/src/calibre/gui2/preferences/adding.ui index abf5d5f7a5..bd06470383 100644 --- a/src/calibre/gui2/preferences/adding.ui +++ b/src/calibre/gui2/preferences/adding.ui @@ -6,7 +6,7 @@ 0 0 - 753 + 1035 547 @@ -24,6 +24,36 @@ The Add &Process + + + + Automerge: If books with similar titles and authors found, merge the incoming formats automatically into +existing book records. The box to the right controls what happens when an existing record already has +the incoming format. Note that this option also affects the Copy to library action. + +Title match ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. Author match is exact. + + + &Automerge added books if they already exist in the calibre library: + + + + + + + Automerge: If books with similar titles and authors found, merge the incoming formats automatically into +existing book records. This box controls what happens when an existing record already has +the incoming format: + +Ignore duplicate incoming files - means that existing files in your calibre library will not be replaced +Overwrite existing duplicate files - means that existing files in your calibre library will be replaced +Create new record for each duplicate file - means that a new book entry will be created for each duplicate file + +Title matching ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. +Author matching is exact. + + + @@ -68,44 +98,7 @@ - - - - When using the "&Copy to library" action to copy books between libraries, preserve the date - - - - - - - Automerge: If books with similar titles and authors found, merge the incoming formats automatically into -existing book records. The box to the right controls what happens when an existing record already has -the incoming format. Note that this option also affects the Copy to library action. - -Title match ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. Author match is exact. - - - &Automerge added books if they already exist in the calibre library: - - - - - - - Automerge: If books with similar titles and authors found, merge the incoming formats automatically into -existing book records. This box controls what happens when an existing record already has -the incoming format: - -Ignore duplicate incoming files - means that existing files in your calibre library will not be replaced -Overwrite existing duplicate files - means that existing files in your calibre library will be replaced -Create new record for each duplicate file - means that a new book entry will be created for each duplicate file - -Title matching ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. -Author matching is exact. - - - - + &Tags to apply when adding a book: @@ -115,14 +108,14 @@ Author matching is exact. - + A comma-separated list of tags that will be applied to books added to the library - + &Configure metadata from file name @@ -144,6 +137,20 @@ Author matching is exact. + + + + When using the "&Copy to library" action to copy books between libraries, preserve the date + + + + + + + Automatically &convert added books to the current output format + + + diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 185308202e..d528b1c0d8 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -16,6 +16,7 @@ from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from calibre.gui2.viewer.flip import SlideFlip from calibre.gui2.shortcuts import Shortcuts +from calibre.gui2 import open_url from calibre import prints from calibre.customize.ui import all_viewer_plugins from calibre.gui2.viewer.keys import SHORTCUTS @@ -481,7 +482,12 @@ class DocumentView(QWebView): # {{{ d = self.document self.unimplemented_actions = list(map(self.pageAction, [d.DownloadImageToDisk, d.OpenLinkInNewWindow, d.DownloadLinkToDisk, - d.OpenImageInNewWindow, d.OpenLink, d.Reload])) + d.OpenImageInNewWindow, d.OpenLink, d.Reload, d.InspectElement])) + + self.search_online_action = QAction(QIcon(I('search.png')), '', self) + self.search_online_action.setShortcut(Qt.CTRL+Qt.Key_E) + self.search_online_action.triggered.connect(self.search_online) + self.addAction(self.search_online_action) self.dictionary_action = QAction(QIcon(I('dictionary.png')), _('&Lookup in dictionary'), self) self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L) @@ -526,6 +532,10 @@ class DocumentView(QWebView): # {{{ self.goto_location_action.setMenu(self.goto_location_menu) self.grabGesture(Qt.SwipeGesture) + self.restore_fonts_action = QAction(_('Normal font size'), self) + self.restore_fonts_action.setCheckable(True) + self.restore_fonts_action.triggered.connect(self.restore_font_size) + def goto_next_section(self, *args): if self.manager is not None: self.manager.goto_next_section() @@ -582,6 +592,15 @@ class DocumentView(QWebView): # {{{ if self.manager is not None: self.manager.selection_changed(unicode(self.document.selectedText())) + def _selectedText(self): + t = unicode(self.selectedText()).strip() + if not t: + return u'' + if len(t) > 40: + t = t[:40] + u'...' + t = t.replace(u'&', u'&&') + return _("S&earch Google for '%s'")%t + def contextMenuEvent(self, ev): mf = self.document.mainFrame() r = mf.hitTestContent(ev.pos()) @@ -591,17 +610,48 @@ class DocumentView(QWebView): # {{{ menu = self.document.createStandardContextMenu() for action in self.unimplemented_actions: menu.removeAction(action) - text = unicode(self.selectedText()) - if text: - menu.insertAction(list(menu.actions())[0], self.dictionary_action) - menu.insertAction(list(menu.actions())[0], self.search_action) + if not img.isNull(): menu.addAction(self.view_image_action) - menu.addSeparator() - menu.addAction(self.goto_location_action) - if self.document.in_fullscreen_mode and self.manager is not None: + + text = self._selectedText() + if text and img.isNull(): + self.search_online_action.setText(text) + menu.addAction(self.search_online_action) + menu.addAction(self.dictionary_action) + menu.addAction(self.search_action) + + if not text and img.isNull(): menu.addSeparator() - menu.addAction(self.manager.toggle_toolbar_action) + if self.manager.action_back.isEnabled(): + menu.addAction(self.manager.action_back) + if self.manager.action_forward.isEnabled(): + menu.addAction(self.manager.action_forward) + menu.addAction(self.goto_location_action) + + if self.manager is not None: + menu.addSeparator() + menu.addAction(self.manager.action_table_of_contents) + + menu.addSeparator() + menu.addAction(self.manager.action_font_size_larger) + self.restore_fonts_action.setChecked(self.multiplier == 1) + menu.addAction(self.restore_fonts_action) + menu.addAction(self.manager.action_font_size_smaller) + + menu.addSeparator() + inspectAction = self.pageAction(self.document.InspectElement) + menu.addAction(inspectAction) + + if not text and img.isNull() and self.manager is not None: + menu.addSeparator() + if self.document.in_fullscreen_mode and self.manager is not None: + menu.addAction(self.manager.toggle_toolbar_action) + menu.addAction(self.manager.action_full_screen) + + menu.addSeparator() + menu.addAction(self.manager.action_quit) + menu.exec_(ev.globalPos()) def lookup(self, *args): @@ -616,6 +666,12 @@ class DocumentView(QWebView): # {{{ if t: self.manager.search.set_search_string(t) + def search_online(self): + t = unicode(self.selectedText()).strip() + if t: + url = 'https://www.google.com/search?q=' + QUrl().toPercentEncoding(t) + open_url(QUrl.fromEncoded(url)) + def set_manager(self, manager): self.manager = manager self.scrollbar = manager.horizontal_scrollbar @@ -993,6 +1049,11 @@ class DocumentView(QWebView): # {{{ self.multiplier -= amount return self.document.scroll_fraction + def restore_font_size(self): + with self.document.page_position: + self.multiplier = 1 + return self.document.scroll_fraction + def changeEvent(self, event): if event.type() == event.EnabledChange: self.update() diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 7b624f170a..9d5049d25a 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -178,6 +178,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.pending_restore = False self.existing_bookmarks= [] self.selected_text = None + self.was_maximized = False self.read_settings() self.dictionary_box.hide() self.close_dictionary_view.clicked.connect(lambda @@ -207,7 +208,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.view.set_manager(self) self.pi = ProgressIndicator(self) self.toc.setVisible(False) - self.action_quit = QAction(self) + self.action_quit = QAction(_('&Quit'), self) self.addAction(self.action_quit) self.view_resized_timer = QTimer(self) self.view_resized_timer.timeout.connect(self.viewport_resize_finished) @@ -299,6 +300,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): ''') self.window_mode_changed = None self.toggle_toolbar_action = QAction(_('Show/hide controls'), self) + self.toggle_toolbar_action.setCheckable(True) self.toggle_toolbar_action.triggered.connect(self.toggle_toolbars) self.addAction(self.toggle_toolbar_action) self.full_screen_label_anim = QPropertyAnimation( @@ -421,7 +423,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): def save_state(self): state = bytearray(self.saveState(self.STATE_VERSION)) vprefs['viewer_toolbar_state'] = state - vprefs.set('viewer_window_geometry', bytearray(self.saveGeometry())) + if not self.isFullScreen(): + vprefs.set('viewer_window_geometry', bytearray(self.saveGeometry())) if self.current_book_has_toc: vprefs.set('viewer_toc_isvisible', bool(self.toc.isVisible())) if self.toc.isVisible(): @@ -488,6 +491,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.window_mode_changed = 'fullscreen' self.tool_bar.setVisible(False) self.tool_bar2.setVisible(False) + self.was_maximized = self.isMaximized() if not self.view.document.fullscreen_scrollbar: self.vertical_scrollbar.setVisible(False) self.frame.layout().setSpacing(0) @@ -574,7 +578,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer): om = self._original_frame_margins self.centralwidget.layout().setContentsMargins(om[0]) self.frame.layout().setContentsMargins(om[1]) - super(EbookViewer, self).showNormal() + if self.was_maximized: + super(EbookViewer, self).showMaximized() + else: + super(EbookViewer, self).showNormal() def handle_window_mode_toggle(self): if self.window_mode_changed: @@ -681,13 +688,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer): def font_size_larger(self): self.view.magnify_fonts() - self.action_font_size_larger.setEnabled(self.view.multiplier < 3) - self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2) def font_size_smaller(self): self.view.shrink_fonts() - self.action_font_size_larger.setEnabled(self.view.multiplier < 3) - self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2) def magnification_changed(self, val): tt = _('Make font size %(which)s\nCurrent magnification: %(mag).1f') @@ -695,6 +698,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): tt %dict(which=_('larger'), mag=val)) self.action_font_size_smaller.setToolTip( tt %dict(which=_('smaller'), mag=val)) + self.action_font_size_larger.setEnabled(self.view.multiplier < 3) + self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2) def find(self, text, repeat=False, backwards=False): if not text: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 5952e11e57..ab85421697 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -28,7 +28,8 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.constants import preferred_encoding, iswindows, filesystem_encoding from calibre.ptempfile import (PersistentTemporaryFile, base_dir, SpooledTemporaryFile) -from calibre.customize.ui import run_plugins_on_import +from calibre.customize.ui import (run_plugins_on_import, + run_plugins_on_postimport) from calibre import isbytestring from calibre.utils.filenames import (ascii_filename, samefile, WindowsAtomicFolderMove, hardlink_file) @@ -1495,8 +1496,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): format = os.path.splitext(npath)[-1].lower().replace('.', '').upper() stream = lopen(npath, 'rb') format = check_ebook_format(stream, format) - return self.add_format(index, format, stream, + retval = self.add_format(index, format, stream, index_is_id=index_is_id, path=path, notify=notify) + run_plugins_on_postimport(self, id, format) + return retval def add_format(self, index, format, stream, index_is_id=False, path=None, notify=True, replace=True, copy_function=None): @@ -3475,6 +3478,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): formats, metadata = iter(formats), iter(metadata) duplicates = [] ids = [] + postimport = [] for path in paths: mi = metadata.next() self._add_newbook_tag(mi) @@ -3506,8 +3510,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): format = check_ebook_format(stream, format) self.add_format(id, format, stream, index_is_id=True) stream.close() + postimport.append((id, format)) self.conn.commit() self.data.refresh_ids(self, ids) # Needed to update format list and size + for book_id, fmt in postimport: + run_plugins_on_postimport(self, book_id, fmt) if duplicates: paths = list(duplicate[0] for duplicate in duplicates) formats = list(duplicate[1] for duplicate in duplicates)