Allow exporting annotations from the annotations browser

This commit is contained in:
Kovid Goyal 2020-08-19 09:53:57 +05:30
parent 9034ea03de
commit 69beb825ad
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 142 additions and 66 deletions

View File

@ -195,6 +195,7 @@ def create_defs():
defs['browse_annots_restrict_to_user'] = None defs['browse_annots_restrict_to_user'] = None
defs['browse_annots_restrict_to_type'] = None defs['browse_annots_restrict_to_type'] = None
defs['browse_annots_use_stemmer'] = True defs['browse_annots_use_stemmer'] = True
defs['annots_export_format'] = 'txt'
create_defs() create_defs()

View File

@ -2,24 +2,119 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import codecs
import json
import os import os
from textwrap import fill from textwrap import fill
from PyQt5.Qt import ( from PyQt5.Qt import (
QApplication, QCheckBox, QComboBox, QCursor, QDateTime, QFont, QHBoxLayout, QApplication, QCheckBox, QComboBox, QCursor, QDateTime, QFont, QFormLayout,
QIcon, QLabel, QPalette, QPlainTextEdit, QSize, QSplitter, Qt, QTextBrowser, QHBoxLayout, QIcon, QLabel, QPalette, QPlainTextEdit, QSize, QSplitter, Qt,
QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
pyqtSignal QWidget, pyqtSignal
) )
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
from calibre.gui2 import Application, config, error_dialog, gprefs from calibre.gui2 import Application, choose_save_file, config, error_dialog, gprefs
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.viewer.widgets import ResultsDelegate, SearchBox from calibre.gui2.viewer.widgets import ResultsDelegate, SearchBox
from calibre.gui2.widgets2 import Dialog from calibre.gui2.widgets2 import Dialog
def render_highlight_as_text(hl, lines):
lines.append(hl['highlighted_text'])
date = QDateTime.fromString(hl['timestamp'], Qt.ISODate).toLocalTime().toString(Qt.SystemLocaleShortDate)
lines.append(date)
notes = hl.get('notes')
if notes:
lines.append('')
lines.append(notes)
lines.append('')
lines.append('───')
lines.append('')
def render_bookmark_as_text(b, lines):
lines.append(b['title'])
date = QDateTime.fromString(b['timestamp'], Qt.ISODate).toLocalTime().toString(Qt.SystemLocaleShortDate)
lines.append(date)
lines.append('')
lines.append('───')
lines.append('')
class Export(Dialog):
prefs = gprefs
pref_name = 'annots_export_format'
def __init__(self, annots, parent=None):
self.annotations = annots
super().__init__(name='export-annotations', title=_('Export {} annotations').format(len(annots)), parent=parent)
def file_type_data(self):
return _('calibre annotation collection'), 'calibre_annotation_collection'
def initial_filename(self):
return _('annotations')
def setup_ui(self):
self.l = l = QFormLayout(self)
self.export_format = ef = QComboBox(self)
ef.addItem(_('Plain text'), 'txt')
ef.addItem(*self.file_type_data())
idx = ef.findData(self.prefs[self.pref_name])
if idx > -1:
ef.setCurrentIndex(idx)
ef.currentIndexChanged.connect(self.save_format_pref)
l.addRow(_('Format to export in:'), ef)
l.addRow(self.bb)
self.bb.clear()
self.bb.addButton(self.bb.Cancel)
b = self.bb.addButton(_('Copy to clipboard'), self.bb.ActionRole)
b.clicked.connect(self.copy_to_clipboard)
b.setIcon(QIcon(I('edit-copy.png')))
b = self.bb.addButton(_('Save to file'), self.bb.ActionRole)
b.clicked.connect(self.save_to_file)
b.setIcon(QIcon(I('save.png')))
def save_format_pref(self):
self.prefs[self.pref_name] = self.export_format.currentData()
def copy_to_clipboard(self):
QApplication.instance().clipboard().setText(self.exported_data())
self.accept()
def save_to_file(self):
filters = [(self.export_format.currentText(), self.export_format.currentData())]
path = choose_save_file(
self, 'annots-export-save', _('File for exports'), filters=filters,
initial_filename=self.initial_filename() + '.' + filters[0][1])
if path:
data = self.exported_data().encode('utf-8')
with open(path, 'wb') as f:
f.write(codecs.BOM_UTF8)
f.write(data)
self.accept()
def exported_data(self):
if self.export_format.currentData() == 'calibre_annotation_collection':
return json.dumps({
'version': 1,
'type': 'calibre_annotation_collection',
'annotations': self.annotations,
}, ensure_ascii=False, sort_keys=True, indent=2)
lines = []
for a in self.annotations:
atype = a['type']
if atype == 'highlight':
render_highlight_as_text(a, lines)
elif atype == 'bookmark':
render_bookmark_as_text(a, lines)
return '\n'.join(lines)
def render_notes(notes, tag='p'): def render_notes(notes, tag='p'):
current_lines = [] current_lines = []
for line in notes.splitlines(): for line in notes.splitlines():
@ -166,6 +261,15 @@ class ResultsList(QTreeWidget):
for item in self.selectedItems(): for item in self.selectedItems():
yield item.data(0, Qt.UserRole)['id'] yield item.data(0, Qt.UserRole)['id']
@property
def selected_annotations(self):
for item in self.selectedItems():
x = item.data(0, Qt.UserRole)
ans = x['annotation'].copy()
for key in ('book_id', 'format'):
ans[key] = x[key]
yield ans
class Restrictions(QWidget): class Restrictions(QWidget):
@ -351,6 +455,10 @@ class BrowsePanel(QWidget):
def selected_annot_ids(self): def selected_annot_ids(self):
return self.results_list.selected_annot_ids return self.results_list.selected_annot_ids
@property
def selected_annotations(self):
return self.results_list.selected_annotations
class Details(QTextBrowser): class Details(QTextBrowser):
@ -560,6 +668,10 @@ class AnnotationsBrowser(Dialog):
b.setToolTip(_('Delete the selected annotations')) b.setToolTip(_('Delete the selected annotations'))
b.setIcon(QIcon(I('trash.png'))) b.setIcon(QIcon(I('trash.png')))
b.clicked.connect(self.delete_selected) b.clicked.connect(self.delete_selected)
self.export_button = b = self.bb.addButton(_('Export all selected'), self.bb.ActionRole)
b.setToolTip(_('Export the selected annotations'))
b.setIcon(QIcon(I('save.png')))
b.clicked.connect(self.export_selected)
def delete_selected(self): def delete_selected(self):
ids = frozenset(self.browse_panel.selected_annot_ids) ids = frozenset(self.browse_panel.selected_annot_ids)
@ -568,6 +680,13 @@ class AnnotationsBrowser(Dialog):
'No annotations have been selected'), show=True) 'No annotations have been selected'), show=True)
self.delete_annotations(ids) self.delete_annotations(ids)
def export_selected(self):
annots = tuple(self.browse_panel.selected_annotations)
if not annots:
return error_dialog(self, _('No selected annotations'), _(
'No annotations have been selected'), show=True)
Export(annots, self).exec_()
def delete_annotations(self, ids): def delete_annotations(self, ids):
if confirm(ngettext( if confirm(ngettext(
'Are you sure you want to <b>permanently</b> delete this annotation?', 'Are you sure you want to <b>permanently</b> delete this annotation?',

View File

@ -2,21 +2,22 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import codecs
import json import json
from itertools import chain from itertools import chain
from PyQt5.Qt import ( from PyQt5.Qt import (
QApplication, QComboBox, QDateTime, QFormLayout, QHBoxLayout, QIcon, QHBoxLayout, QIcon, QItemSelectionModel, QKeySequence, QLabel, QListWidget,
QItemSelectionModel, QKeySequence, QLabel, QListWidget, QListWidgetItem, QListWidgetItem, QPushButton, Qt, QTextEdit, QToolButton, QVBoxLayout, QWidget,
QPushButton, Qt, QTextEdit, QToolButton, QVBoxLayout, QWidget, pyqtSignal pyqtSignal
) )
from calibre.constants import plugins from calibre.constants import plugins
from calibre.ebooks.epub.cfi.parse import cfi_sort_key from calibre.ebooks.epub.cfi.parse import cfi_sort_key
from calibre.gui2 import choose_save_file, error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.library.annotations import Details, render_notes from calibre.gui2.library.annotations import (
Details, Export as ExportBase, render_highlight_as_text, render_notes
)
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 index_to_key_sequence from calibre.gui2.viewer.shortcuts import index_to_key_sequence
@ -24,71 +25,26 @@ from calibre.gui2.widgets2 import Dialog
from polyglot.builtins import range from polyglot.builtins import range
class Export(Dialog): class Export(ExportBase):
prefs = vprefs
pref_name = 'highlight_export_format'
def __init__(self, highlights, parent=None): def file_type_data(self):
self.highlights = highlights return _('calibre highlights'), 'calibre_highlights'
super().__init__('export-highlights', _('Export {} highlights').format(len(highlights)), parent=parent)
def setup_ui(self): def initial_filename(self):
self.l = l = QFormLayout(self) return _('highlights')
self.export_format = ef = QComboBox(self)
ef.addItem(_('Plain text'), 'txt')
ef.addItem(_('calibre highlights'), 'calibre_highlights')
idx = ef.findData(vprefs['highlight_export_format'])
if idx > -1:
ef.setCurrentIndex(idx)
ef.currentIndexChanged.connect(self.save_format_pref)
l.addRow(_('Format to export in:'), ef)
l.addRow(self.bb)
self.bb.clear()
self.bb.addButton(self.bb.Cancel)
b = self.bb.addButton(_('Copy to clipboard'), self.bb.ActionRole)
b.clicked.connect(self.copy_to_clipboard)
b.setIcon(QIcon(I('edit-copy.png')))
b = self.bb.addButton(_('Save to file'), self.bb.ActionRole)
b.clicked.connect(self.save_to_file)
b.setIcon(QIcon(I('save.png')))
def save_format_pref(self):
vprefs['highlight_export_format'] = self.export_format.currentData()
def copy_to_clipboard(self):
QApplication.instance().clipboard().setText(self.exported_data)
self.accept()
def save_to_file(self):
filters = [(self.export_format.currentText(), self.export_format.currentData())]
path = choose_save_file(
self, 'highlights-export-save', _('File for exports'), filters=filters,
initial_filename=_('highlights') + '.' + filters[0][1])
if path:
data = self.exported_data.encode('utf-8')
with open(path, 'wb') as f:
f.write(codecs.BOM_UTF8)
f.write(data)
self.accept()
@property
def exported_data(self): def exported_data(self):
if self.export_format.currentData() == 'calibre_highlights': if self.export_format.currentData() == 'calibre_highlights':
return json.dumps({ return json.dumps({
'version': 1, 'version': 1,
'type': 'calibre_highlights', 'type': 'calibre_highlights',
'highlights': self.highlights 'highlights': self.annotations,
}, ensure_ascii=False, sort_keys=True, indent=2) }, ensure_ascii=False, sort_keys=True, indent=2)
lines = [] lines = []
for hl in self.highlights: for hl in self.annotations:
lines.append(hl['highlighted_text']) render_highlight_as_text(hl, lines)
date = QDateTime.fromString(hl['timestamp'], Qt.ISODate).toLocalTime().toString(Qt.SystemLocaleShortDate)
lines.append(date)
notes = hl.get('notes')
if notes:
lines.append('')
lines.append(notes)
lines.append('')
lines.append('───')
lines.append('')
return '\n'.join(lines) return '\n'.join(lines)