mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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
This commit is contained in:
parent
1e123e35cf
commit
814afdf189
@ -7,11 +7,13 @@ import json
|
|||||||
import os
|
import os
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from PyQt5.Qt import (
|
from PyQt5.Qt import (
|
||||||
QApplication, QCheckBox, QComboBox, QCursor, QDateTime, QFont, QFormLayout, QDialog,
|
QAbstractItemView, QApplication, QCheckBox, QComboBox, QCursor, QDateTime,
|
||||||
QHBoxLayout, QIcon, QKeySequence, QLabel, QMenu, QPalette, QPlainTextEdit, QSize,
|
QDialog, QDialogButtonBox, QFont, QFormLayout, QFrame, QHBoxLayout, QIcon,
|
||||||
QSplitter, Qt, QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QFrame,
|
QKeySequence, QLabel, QMenu, QPalette, QPlainTextEdit, QSize, QSplitter, Qt,
|
||||||
QVBoxLayout, QWidget, pyqtSignal, QAbstractItemView, QDialogButtonBox
|
QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||||
|
QWidget, pyqtSignal
|
||||||
)
|
)
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from calibre import prepare_string_for_xml
|
from calibre import prepare_string_for_xml
|
||||||
from calibre.ebooks.metadata import authors_to_string, fmt_sidx
|
from calibre.ebooks.metadata import authors_to_string, fmt_sidx
|
||||||
@ -22,25 +24,40 @@ from calibre.gui2.widgets2 import Dialog
|
|||||||
|
|
||||||
|
|
||||||
# rendering {{{
|
# 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'])
|
lines.append(hl['highlighted_text'])
|
||||||
date = QDateTime.fromString(hl['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate)
|
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)
|
lines.append(date)
|
||||||
notes = hl.get('notes')
|
notes = hl.get('notes')
|
||||||
if notes:
|
if notes:
|
||||||
lines.append('')
|
lines.append('')
|
||||||
lines.append(notes)
|
lines.append(notes)
|
||||||
lines.append('')
|
lines.append('')
|
||||||
lines.append('───')
|
if as_markdown:
|
||||||
|
lines.append('-' * 20)
|
||||||
|
else:
|
||||||
|
lines.append('───')
|
||||||
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'])
|
lines.append(b['title'])
|
||||||
date = QDateTime.fromString(b['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate)
|
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(date)
|
||||||
lines.append('')
|
lines.append('')
|
||||||
lines.append('───')
|
if as_markdown:
|
||||||
|
lines.append('-' * 20)
|
||||||
|
else:
|
||||||
|
lines.append('───')
|
||||||
lines.append('')
|
lines.append('')
|
||||||
|
|
||||||
|
|
||||||
@ -115,6 +132,7 @@ class Export(Dialog): # {{{
|
|||||||
self.l = l = QFormLayout(self)
|
self.l = l = QFormLayout(self)
|
||||||
self.export_format = ef = QComboBox(self)
|
self.export_format = ef = QComboBox(self)
|
||||||
ef.addItem(_('Plain text'), 'txt')
|
ef.addItem(_('Plain text'), 'txt')
|
||||||
|
ef.addItem(_('Markdown'), 'md')
|
||||||
ef.addItem(*self.file_type_data())
|
ef.addItem(*self.file_type_data())
|
||||||
idx = ef.findData(self.prefs[self.pref_name])
|
idx = ef.findData(self.prefs[self.pref_name])
|
||||||
if idx > -1:
|
if idx > -1:
|
||||||
@ -151,7 +169,8 @@ class Export(Dialog): # {{{
|
|||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
def exported_data(self):
|
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({
|
return json.dumps({
|
||||||
'version': 1,
|
'version': 1,
|
||||||
'type': 'calibre_annotation_collection',
|
'type': 'calibre_annotation_collection',
|
||||||
@ -160,6 +179,10 @@ class Export(Dialog): # {{{
|
|||||||
lines = []
|
lines = []
|
||||||
db = current_db()
|
db = current_db()
|
||||||
bid_groups = {}
|
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:
|
for a in self.annotations:
|
||||||
bid_groups.setdefault(a['book_id'], []).append(a)
|
bid_groups.setdefault(a['book_id'], []).append(a)
|
||||||
for book_id, group in bid_groups.items():
|
for book_id, group in bid_groups.items():
|
||||||
@ -167,10 +190,14 @@ class Export(Dialog): # {{{
|
|||||||
lines.append('')
|
lines.append('')
|
||||||
for a in group:
|
for a in group:
|
||||||
atype = a['type']
|
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':
|
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':
|
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('')
|
lines.append('')
|
||||||
return '\n'.join(lines).strip()
|
return '\n'.join(lines).strip()
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -3,3 +3,7 @@
|
|||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
|
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', {})
|
||||||
|
@ -23,6 +23,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm
|
|||||||
from calibre.gui2.library.annotations import (
|
from calibre.gui2.library.annotations import (
|
||||||
Details, Export as ExportBase, render_highlight_as_text, render_notes
|
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.config import vprefs
|
||||||
from calibre.gui2.viewer.search import SearchInput
|
from calibre.gui2.viewer.search import SearchInput
|
||||||
from calibre.gui2.viewer.shortcuts import get_shortcut_for, index_to_key_sequence
|
from calibre.gui2.viewer.shortcuts import get_shortcut_for, index_to_key_sequence
|
||||||
@ -130,15 +131,31 @@ class Export(ExportBase):
|
|||||||
return _('highlights')
|
return _('highlights')
|
||||||
|
|
||||||
def exported_data(self):
|
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({
|
return json.dumps({
|
||||||
'version': 1,
|
'version': 1,
|
||||||
'type': 'calibre_highlights',
|
'type': 'calibre_highlights',
|
||||||
'highlights': self.annotations,
|
'highlights': self.annotations,
|
||||||
}, ensure_ascii=False, sort_keys=True, indent=2)
|
}, ensure_ascii=False, sort_keys=True, indent=2)
|
||||||
lines = []
|
lines = []
|
||||||
|
as_markdown = fmt == 'md'
|
||||||
for hl in self.annotations:
|
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()
|
return '\n'.join(lines).strip()
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import re
|
|||||||
|
|
||||||
|
|
||||||
def get_book_library_details(absolute_path_to_ebook):
|
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))
|
absolute_path_to_ebook = os.path.abspath(os.path.expanduser(absolute_path_to_ebook))
|
||||||
base = os.path.dirname(absolute_path_to_ebook)
|
base = os.path.dirname(absolute_path_to_ebook)
|
||||||
m = re.search(r' \((\d+)\)$', os.path.basename(base))
|
m = re.search(r' \((\d+)\)$', os.path.basename(base))
|
||||||
@ -14,11 +15,13 @@ def get_book_library_details(absolute_path_to_ebook):
|
|||||||
return
|
return
|
||||||
book_id = int(m.group(1))
|
book_id = int(m.group(1))
|
||||||
library_dir = os.path.dirname(os.path.dirname(base))
|
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.path.join(library_dir, 'metadata.db')
|
||||||
dbpath = os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH') or dbpath
|
dbpath = os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH') or dbpath
|
||||||
if not os.path.exists(dbpath):
|
if not os.path.exists(dbpath):
|
||||||
return
|
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):
|
def database_has_annotations_support(cursor):
|
||||||
|
@ -9,12 +9,12 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from PyQt5.Qt import (
|
from PyQt5.Qt import (
|
||||||
QApplication, QCursor, QDockWidget, QEvent, QMenu, QMimeData, QModelIndex,
|
QApplication, QCursor, QDockWidget, QEvent, QMainWindow, QMenu, QMimeData,
|
||||||
QPixmap, Qt, QTimer, QToolBar, QUrl, QVBoxLayout, QWidget, pyqtSignal, QMainWindow
|
QModelIndex, QPixmap, Qt, QTimer, QToolBar, QUrl, QVBoxLayout, QWidget,
|
||||||
|
pyqtSignal
|
||||||
)
|
)
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.constants import DEBUG
|
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.dialogs.drm_error import DRMErrorMessage
|
||||||
from calibre.gui2.image_popup import ImagePopup
|
from calibre.gui2.image_popup import ImagePopup
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
|
from calibre.gui2.viewer import get_current_book_data
|
||||||
from calibre.gui2.viewer.annotations import (
|
from calibre.gui2.viewer.annotations import (
|
||||||
AnnotationsSaveWorker, annotations_dir, parse_annotations
|
AnnotationsSaveWorker, annotations_dir, parse_annotations
|
||||||
)
|
)
|
||||||
@ -109,6 +110,7 @@ class EbookViewer(MainWindow):
|
|||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
pass
|
pass
|
||||||
self.current_book_data = {}
|
self.current_book_data = {}
|
||||||
|
get_current_book_data(self.current_book_data)
|
||||||
self.book_prepared.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection)
|
self.book_prepared.connect(self.load_finished, type=Qt.ConnectionType.QueuedConnection)
|
||||||
self.dock_defs = dock_defs()
|
self.dock_defs = dock_defs()
|
||||||
|
|
||||||
@ -457,6 +459,7 @@ class EbookViewer(MainWindow):
|
|||||||
self.loading_overlay(_('Loading book, please wait'))
|
self.loading_overlay(_('Loading book, please wait'))
|
||||||
self.save_annotations()
|
self.save_annotations()
|
||||||
self.current_book_data = {}
|
self.current_book_data = {}
|
||||||
|
get_current_book_data(self.current_book_data)
|
||||||
self.search_widget.clear_searches()
|
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 = Thread(name='LoadBook', target=self._load_ebook_worker, args=(pathtoebook, open_at, reload_book or self.force_reload))
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
@ -513,6 +516,7 @@ class EbookViewer(MainWindow):
|
|||||||
self.load_ebook(data['pathtoebook'], open_at=data['open_at'], reload_book=True)
|
self.load_ebook(data['pathtoebook'], open_at=data['open_at'], reload_book=True)
|
||||||
return
|
return
|
||||||
self.current_book_data = data
|
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_map'] = defaultdict(list)
|
||||||
self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json'
|
self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json'
|
||||||
self.load_book_data(cbd)
|
self.load_book_data(cbd)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user