From 814afdf18908cdd8f0ae4ec6bd36a10879860e0d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 24 Dec 2020 13:08:13 +0530 Subject: [PATCH] E-book viewer: When exporting highlights, add a new Markdown export type, where the date of the highlight becomes a link to open the book in calibre showing the highlight --- src/calibre/gui2/library/annotations.py | 49 +++++++++++++++++++------ src/calibre/gui2/viewer/__init__.py | 4 ++ src/calibre/gui2/viewer/highlights.py | 21 ++++++++++- src/calibre/gui2/viewer/integration.py | 5 ++- src/calibre/gui2/viewer/ui.py | 12 ++++-- 5 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/calibre/gui2/library/annotations.py b/src/calibre/gui2/library/annotations.py index 7550dae08a..cdfdceb761 100644 --- a/src/calibre/gui2/library/annotations.py +++ b/src/calibre/gui2/library/annotations.py @@ -7,11 +7,13 @@ import json import os from functools import partial from PyQt5.Qt import ( - QApplication, QCheckBox, QComboBox, QCursor, QDateTime, QFont, QFormLayout, QDialog, - QHBoxLayout, QIcon, QKeySequence, QLabel, QMenu, QPalette, QPlainTextEdit, QSize, - QSplitter, Qt, QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QFrame, - QVBoxLayout, QWidget, pyqtSignal, QAbstractItemView, QDialogButtonBox + QAbstractItemView, QApplication, QCheckBox, QComboBox, QCursor, QDateTime, + QDialog, QDialogButtonBox, QFont, QFormLayout, QFrame, QHBoxLayout, QIcon, + QKeySequence, QLabel, QMenu, QPalette, QPlainTextEdit, QSize, QSplitter, Qt, + QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget, pyqtSignal ) +from urllib.parse import quote from calibre import prepare_string_for_xml from calibre.ebooks.metadata import authors_to_string, fmt_sidx @@ -22,25 +24,40 @@ from calibre.gui2.widgets2 import Dialog # rendering {{{ -def render_highlight_as_text(hl, lines): + +def render_highlight_as_text(hl, lines, as_markdown=False, link_prefix=None): lines.append(hl['highlighted_text']) date = QDateTime.fromString(hl['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate) + if as_markdown and link_prefix: + cfi = hl['start_cfi'] + spine_index = (1 + hl['spine_index']) * 2 + link = (link_prefix + quote(f'epubcfi(/{spine_index}{cfi})')).replace(')', '%29') + date = f'[{date}]({link})' lines.append(date) notes = hl.get('notes') if notes: lines.append('') lines.append(notes) lines.append('') - lines.append('───') + if as_markdown: + lines.append('-' * 20) + else: + lines.append('───') lines.append('') -def render_bookmark_as_text(b, lines): +def render_bookmark_as_text(b, lines, as_markdown=False, link_prefix=None): lines.append(b['title']) date = QDateTime.fromString(b['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate) + if as_markdown and link_prefix and b['pos_type'] == 'epubcfi': + link = (link_prefix + quote(b['pos'])).replace(')', '%29') + date = f'[{date}]({link})' lines.append(date) lines.append('') - lines.append('───') + if as_markdown: + lines.append('-' * 20) + else: + lines.append('───') lines.append('') @@ -115,6 +132,7 @@ class Export(Dialog): # {{{ self.l = l = QFormLayout(self) self.export_format = ef = QComboBox(self) ef.addItem(_('Plain text'), 'txt') + ef.addItem(_('Markdown'), 'md') ef.addItem(*self.file_type_data()) idx = ef.findData(self.prefs[self.pref_name]) if idx > -1: @@ -151,7 +169,8 @@ class Export(Dialog): # {{{ self.accept() def exported_data(self): - if self.export_format.currentData() == 'calibre_annotation_collection': + fmt = self.export_format.currentData() + if fmt == 'calibre_annotation_collection': return json.dumps({ 'version': 1, 'type': 'calibre_annotation_collection', @@ -160,6 +179,10 @@ class Export(Dialog): # {{{ lines = [] db = current_db() bid_groups = {} + as_markdown = fmt == 'md' + library_id = getattr(db, 'server_library_id', None) + if library_id: + library_id = '_hex_-' + library_id.encode('utf-8').hex() for a in self.annotations: bid_groups.setdefault(a['book_id'], []).append(a) for book_id, group in bid_groups.items(): @@ -167,10 +190,14 @@ class Export(Dialog): # {{{ lines.append('') for a in group: atype = a['type'] + if library_id: + link_prefix = f'calibre://show-book/{library_id}/{book_id}/{a["format"]}?open_at=' + else: + link_prefix = None if atype == 'highlight': - render_highlight_as_text(a, lines) + render_highlight_as_text(a, lines, as_markdown=as_markdown, link_prefix=link_prefix) elif atype == 'bookmark': - render_bookmark_as_text(a, lines) + render_bookmark_as_text(a, lines, as_markdown=as_markdown, link_prefix=link_prefix) lines.append('') return '\n'.join(lines).strip() # }}} diff --git a/src/calibre/gui2/viewer/__init__.py b/src/calibre/gui2/viewer/__init__.py index 635349d71f..c20fcc1471 100644 --- a/src/calibre/gui2/viewer/__init__.py +++ b/src/calibre/gui2/viewer/__init__.py @@ -3,3 +3,7 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal +def get_current_book_data(set_val=False): + if set_val is not False: + setattr(get_current_book_data, 'ans', set_val) + return getattr(get_current_book_data, 'ans', {}) diff --git a/src/calibre/gui2/viewer/highlights.py b/src/calibre/gui2/viewer/highlights.py index 9d9cab196f..ae1ce16571 100644 --- a/src/calibre/gui2/viewer/highlights.py +++ b/src/calibre/gui2/viewer/highlights.py @@ -23,6 +23,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.library.annotations import ( Details, Export as ExportBase, render_highlight_as_text, render_notes ) +from calibre.gui2.viewer import get_current_book_data from calibre.gui2.viewer.config import vprefs from calibre.gui2.viewer.search import SearchInput from calibre.gui2.viewer.shortcuts import get_shortcut_for, index_to_key_sequence @@ -130,15 +131,31 @@ class Export(ExportBase): return _('highlights') def exported_data(self): - if self.export_format.currentData() == 'calibre_highlights': + cbd = get_current_book_data() + link_prefix = library_id = None + if 'calibre_library_id' in cbd: + library_id = cbd['calibre_library_id'] + book_id = cbd['calibre_book_id'] + book_fmt = cbd['calibre_book_fmt'] + elif cbd.get('book_library_details'): + bld = cbd['book_library_details'] + book_id = bld['book_id'] + book_fmt = bld['fmt'].upper() + library_id = bld['library_id'] + if library_id: + library_id = '_hex_-' + library_id.encode('utf-8').hex() + link_prefix = f'calibre://show-book/{library_id}/{book_id}/{book_fmt}?open_at=' + fmt = self.export_format.currentData() + if fmt == 'calibre_highlights': return json.dumps({ 'version': 1, 'type': 'calibre_highlights', 'highlights': self.annotations, }, ensure_ascii=False, sort_keys=True, indent=2) lines = [] + as_markdown = fmt == 'md' for hl in self.annotations: - render_highlight_as_text(hl, lines) + render_highlight_as_text(hl, lines, as_markdown=as_markdown, link_prefix=link_prefix) return '\n'.join(lines).strip() diff --git a/src/calibre/gui2/viewer/integration.py b/src/calibre/gui2/viewer/integration.py index a214a7492d..d1f46586a2 100644 --- a/src/calibre/gui2/viewer/integration.py +++ b/src/calibre/gui2/viewer/integration.py @@ -7,6 +7,7 @@ import re def get_book_library_details(absolute_path_to_ebook): + from calibre.srv.library_broker import correct_case_of_last_path_component, library_id_from_path absolute_path_to_ebook = os.path.abspath(os.path.expanduser(absolute_path_to_ebook)) base = os.path.dirname(absolute_path_to_ebook) m = re.search(r' \((\d+)\)$', os.path.basename(base)) @@ -14,11 +15,13 @@ def get_book_library_details(absolute_path_to_ebook): return book_id = int(m.group(1)) library_dir = os.path.dirname(os.path.dirname(base)) + corrected_path = correct_case_of_last_path_component(library_dir) + library_id = library_id_from_path(corrected_path) dbpath = os.path.join(library_dir, 'metadata.db') dbpath = os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH') or dbpath if not os.path.exists(dbpath): return - return {'dbpath': dbpath, 'book_id': book_id, 'fmt': absolute_path_to_ebook.rpartition('.')[-1].upper()} + return {'dbpath': dbpath, 'book_id': book_id, 'fmt': absolute_path_to_ebook.rpartition('.')[-1].upper(), 'library_id': library_id} def database_has_annotations_support(cursor): diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index ce8afa7294..af302b7719 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -9,12 +9,12 @@ import re import sys from collections import defaultdict, namedtuple from hashlib import sha256 -from threading import Thread - from PyQt5.Qt import ( - QApplication, QCursor, QDockWidget, QEvent, QMenu, QMimeData, QModelIndex, - QPixmap, Qt, QTimer, QToolBar, QUrl, QVBoxLayout, QWidget, pyqtSignal, QMainWindow + QApplication, QCursor, QDockWidget, QEvent, QMainWindow, QMenu, QMimeData, + QModelIndex, QPixmap, Qt, QTimer, QToolBar, QUrl, QVBoxLayout, QWidget, + pyqtSignal ) +from threading import Thread from calibre import prints from calibre.constants import DEBUG @@ -24,6 +24,7 @@ from calibre.gui2 import choose_files, error_dialog from calibre.gui2.dialogs.drm_error import DRMErrorMessage from calibre.gui2.image_popup import ImagePopup from calibre.gui2.main_window import MainWindow +from calibre.gui2.viewer import get_current_book_data from calibre.gui2.viewer.annotations import ( AnnotationsSaveWorker, annotations_dir, parse_annotations ) @@ -109,6 +110,7 @@ class EbookViewer(MainWindow): except EnvironmentError: pass self.current_book_data = {} + get_current_book_data(self.current_book_data) self.book_prepared.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection) self.dock_defs = dock_defs() @@ -457,6 +459,7 @@ class EbookViewer(MainWindow): self.loading_overlay(_('Loading book, please wait')) self.save_annotations() self.current_book_data = {} + get_current_book_data(self.current_book_data) self.search_widget.clear_searches() t = Thread(name='LoadBook', target=self._load_ebook_worker, args=(pathtoebook, open_at, reload_book or self.force_reload)) t.daemon = True @@ -513,6 +516,7 @@ class EbookViewer(MainWindow): self.load_ebook(data['pathtoebook'], open_at=data['open_at'], reload_book=True) return self.current_book_data = data + get_current_book_data(self.current_book_data) self.current_book_data['annotations_map'] = defaultdict(list) self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json' self.load_book_data(cbd)