mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 18:24:30 -04:00
Allow editing highlight notes from annotations browser
This commit is contained in:
parent
b90e6964d0
commit
9da5c6eb57
@ -98,3 +98,17 @@ def merge_annotations(annots, annots_map, merge_last_read=True):
|
|||||||
if not b:
|
if not b:
|
||||||
continue
|
continue
|
||||||
changed, annots_map[annot_type] = merge_annots_with_identical_field(a or [], b, field=field)
|
changed, annots_map[annot_type] = merge_annots_with_identical_field(a or [], b, field=field)
|
||||||
|
|
||||||
|
|
||||||
|
def annot_db_data(annot):
|
||||||
|
aid = text = None
|
||||||
|
atype = annot['type'].lower()
|
||||||
|
if atype == 'bookmark':
|
||||||
|
aid = text = annot['title']
|
||||||
|
elif atype == 'highlight':
|
||||||
|
aid = annot['uuid']
|
||||||
|
text = annot.get('highlighted_text') or ''
|
||||||
|
notes = annot.get('notes') or ''
|
||||||
|
if notes:
|
||||||
|
text += '\n\x1f\n' + notes
|
||||||
|
return aid, text
|
||||||
|
@ -19,6 +19,7 @@ from calibre.constants import (iswindows, filesystem_encoding,
|
|||||||
preferred_encoding)
|
preferred_encoding)
|
||||||
from calibre.ptempfile import PersistentTemporaryFile, TemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile, TemporaryFile
|
||||||
from calibre.db import SPOOL_SIZE
|
from calibre.db import SPOOL_SIZE
|
||||||
|
from calibre.db.annotations import annot_db_data
|
||||||
from calibre.db.schema_upgrades import SchemaUpgrade
|
from calibre.db.schema_upgrades import SchemaUpgrade
|
||||||
from calibre.db.delete_service import delete_service
|
from calibre.db.delete_service import delete_service
|
||||||
from calibre.db.errors import NoSuchFormat
|
from calibre.db.errors import NoSuchFormat
|
||||||
@ -27,7 +28,7 @@ from calibre.ebooks.metadata import title_sort, author_to_author_sort
|
|||||||
from calibre.utils import pickle_binary_string, unpickle_binary_string
|
from calibre.utils import pickle_binary_string, unpickle_binary_string
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.config import to_json, from_json, prefs, tweaks
|
from calibre.utils.config import to_json, from_json, prefs, tweaks
|
||||||
from calibre.utils.date import utcfromtimestamp, parse_date
|
from calibre.utils.date import utcfromtimestamp, parse_date, utcnow, EPOCH
|
||||||
from calibre.utils.filenames import (
|
from calibre.utils.filenames import (
|
||||||
is_case_sensitive, samefile, hardlink_file, ascii_filename,
|
is_case_sensitive, samefile, hardlink_file, ascii_filename,
|
||||||
WindowsAtomicFolderMove, atomic_rename, remove_dir_if_empty,
|
WindowsAtomicFolderMove, atomic_rename, remove_dir_if_empty,
|
||||||
@ -301,15 +302,8 @@ def save_annotations_for_book(cursor, book_id, fmt, annots_list, user_type='loca
|
|||||||
fmt = fmt.upper()
|
fmt = fmt.upper()
|
||||||
for annot, timestamp_in_secs in annots_list:
|
for annot, timestamp_in_secs in annots_list:
|
||||||
atype = annot['type'].lower()
|
atype = annot['type'].lower()
|
||||||
if atype == 'bookmark':
|
aid, text = annot_db_data(annot)
|
||||||
aid = text = annot['title']
|
if aid is None:
|
||||||
elif atype == 'highlight':
|
|
||||||
aid = annot['uuid']
|
|
||||||
text = annot.get('highlighted_text') or ''
|
|
||||||
notes = annot.get('notes') or ''
|
|
||||||
if notes:
|
|
||||||
text += '\n\x1f\n' + notes
|
|
||||||
else:
|
|
||||||
continue
|
continue
|
||||||
data.append((book_id, fmt, user_type, user, timestamp_in_secs, aid, atype, json.dumps(annot), text))
|
data.append((book_id, fmt, user_type, user, timestamp_in_secs, aid, atype, json.dumps(annot), text))
|
||||||
cursor.execute('INSERT OR IGNORE INTO annotations_dirtied (book) VALUES (?)', (book_id,))
|
cursor.execute('INSERT OR IGNORE INTO annotations_dirtied (book) VALUES (?)', (book_id,))
|
||||||
@ -1835,9 +1829,11 @@ class DB(object):
|
|||||||
yield {'format': fmt, 'user_type': user_type, 'user': user, 'annotation': annot}
|
yield {'format': fmt, 'user_type': user_type, 'user': user, 'annotation': annot}
|
||||||
|
|
||||||
def delete_annotations(self, annot_ids):
|
def delete_annotations(self, annot_ids):
|
||||||
from calibre.utils.date import utcnow
|
|
||||||
replacements = []
|
replacements = []
|
||||||
removals = []
|
removals = []
|
||||||
|
now = utcnow()
|
||||||
|
ts = now.isoformat()
|
||||||
|
timestamp = (now - EPOCH).total_seconds()
|
||||||
for annot_id in annot_ids:
|
for annot_id in annot_ids:
|
||||||
for (raw_annot_data, annot_type) in self.execute(
|
for (raw_annot_data, annot_type) in self.execute(
|
||||||
'SELECT annot_data, annot_type FROM annotations WHERE id=?', (annot_id,)
|
'SELECT annot_data, annot_type FROM annotations WHERE id=?', (annot_id,)
|
||||||
@ -1847,18 +1843,32 @@ class DB(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
removals.append((annot_id,))
|
removals.append((annot_id,))
|
||||||
continue
|
continue
|
||||||
new_annot = {'removed': True, 'timestamp': utcnow().isoformat(), 'type': annot_type}
|
now = utcnow()
|
||||||
|
new_annot = {'removed': True, 'timestamp': ts, 'type': annot_type}
|
||||||
uuid = annot_data.get('uuid')
|
uuid = annot_data.get('uuid')
|
||||||
if uuid is not None:
|
if uuid is not None:
|
||||||
new_annot['uuid'] = uuid
|
new_annot['uuid'] = uuid
|
||||||
else:
|
else:
|
||||||
new_annot['title'] = annot_data['title']
|
new_annot['title'] = annot_data['title']
|
||||||
replacements.append((json.dumps(new_annot), annot_id))
|
replacements.append((json.dumps(new_annot), timestamp, annot_id))
|
||||||
if replacements:
|
if replacements:
|
||||||
self.executemany('UPDATE annotations SET annot_data=?, searchable_text="" WHERE id=?', replacements)
|
self.executemany('UPDATE annotations SET annot_data=?, timestamp=?, searchable_text="" WHERE id=?', replacements)
|
||||||
if removals:
|
if removals:
|
||||||
self.executemany('DELETE FROM annotations WHERE id=?', removals)
|
self.executemany('DELETE FROM annotations WHERE id=?', removals)
|
||||||
|
|
||||||
|
def update_annotations(self, annot_id_map):
|
||||||
|
now = utcnow()
|
||||||
|
ts = now.isoformat()
|
||||||
|
timestamp = (now - EPOCH).total_seconds()
|
||||||
|
with self.conn:
|
||||||
|
for annot_id, annot in annot_id_map.items():
|
||||||
|
atype = annot['type']
|
||||||
|
aid, text = annot_db_data(annot)
|
||||||
|
if aid is not None:
|
||||||
|
annot['timestamp'] = ts
|
||||||
|
self.execute('UPDATE annotations SET annot_data=?, timestamp=?, annot_type=?, searchable_text=?, annot_id=? WHERE id=?',
|
||||||
|
(json.dumps(annot), timestamp, atype, text, aid, annot_id))
|
||||||
|
|
||||||
def all_annotations(self, restrict_to_user=None, limit=None, annotation_type=None, ignore_removed=False):
|
def all_annotations(self, restrict_to_user=None, limit=None, annotation_type=None, ignore_removed=False):
|
||||||
ls = json.loads
|
ls = json.loads
|
||||||
q = 'SELECT id, book, format, user_type, user, annot_data FROM annotations'
|
q = 'SELECT id, book, format, user_type, user, annot_data FROM annotations'
|
||||||
|
@ -2337,6 +2337,10 @@ class Cache(object):
|
|||||||
def delete_annotations(self, annot_ids):
|
def delete_annotations(self, annot_ids):
|
||||||
self.backend.delete_annotations(annot_ids)
|
self.backend.delete_annotations(annot_ids)
|
||||||
|
|
||||||
|
@write_api
|
||||||
|
def update_annotations(self, annot_id_map):
|
||||||
|
self.backend.update_annotations(annot_id_map)
|
||||||
|
|
||||||
@write_api
|
@write_api
|
||||||
def restore_annotations(self, book_id, annotations):
|
def restore_annotations(self, book_id, annotations):
|
||||||
from calibre.utils.iso8601 import parse_iso8601
|
from calibre.utils.iso8601 import parse_iso8601
|
||||||
|
@ -7,8 +7,9 @@ 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, QHBoxLayout,
|
||||||
QIcon, QLabel, QPalette, QPushButton, QSize, QSplitter, Qt, QTextBrowser, QTimer,
|
QIcon, QInputDialog, QLabel, QPalette, QPushButton, QSize, QSplitter, Qt,
|
||||||
QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
|
QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||||
|
QWidget, pyqtSignal
|
||||||
)
|
)
|
||||||
|
|
||||||
from calibre import prepare_string_for_xml
|
from calibre import prepare_string_for_xml
|
||||||
@ -348,6 +349,7 @@ class DetailsPanel(QWidget):
|
|||||||
|
|
||||||
open_annotation = pyqtSignal(object, object, object)
|
open_annotation = pyqtSignal(object, object, object)
|
||||||
show_book = pyqtSignal(object, object)
|
show_book = pyqtSignal(object, object)
|
||||||
|
edit_annotation = pyqtSignal(object, object)
|
||||||
delete_annotation = pyqtSignal(object)
|
delete_annotation = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
@ -371,6 +373,10 @@ class DetailsPanel(QWidget):
|
|||||||
|
|
||||||
h = QHBoxLayout()
|
h = QHBoxLayout()
|
||||||
l.addLayout(h)
|
l.addLayout(h)
|
||||||
|
self.edit_button = ob = QPushButton(QIcon(I('edit_input.png')), _('Edit'), self)
|
||||||
|
ob.setToolTip(_('Edit the notes if any for this highlight'))
|
||||||
|
ob.clicked.connect(self.edit_result)
|
||||||
|
h.addWidget(ob)
|
||||||
self.delete_button = ob = QPushButton(QIcon(I('trash.png')), _('Delete'), self)
|
self.delete_button = ob = QPushButton(QIcon(I('trash.png')), _('Delete'), self)
|
||||||
ob.setToolTip(_('Delete this annotation'))
|
ob.setToolTip(_('Delete this annotation'))
|
||||||
ob.clicked.connect(self.delete_result)
|
ob.clicked.connect(self.delete_result)
|
||||||
@ -388,6 +394,11 @@ class DetailsPanel(QWidget):
|
|||||||
r = self.current_result
|
r = self.current_result
|
||||||
self.delete_annotation.emit(r['id'])
|
self.delete_annotation.emit(r['id'])
|
||||||
|
|
||||||
|
def edit_result(self):
|
||||||
|
if self.current_result is not None:
|
||||||
|
r = self.current_result
|
||||||
|
self.edit_annotation.emit(r['id'], r['annotation'])
|
||||||
|
|
||||||
def show_in_library(self):
|
def show_in_library(self):
|
||||||
if self.current_result is not None:
|
if self.current_result is not None:
|
||||||
self.show_book.emit(self.current_result['book_id'], self.current_result['format'])
|
self.show_book.emit(self.current_result['book_id'], self.current_result['format'])
|
||||||
@ -395,16 +406,24 @@ class DetailsPanel(QWidget):
|
|||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
return QSize(450, 600)
|
return QSize(450, 600)
|
||||||
|
|
||||||
|
def set_controls_visibility(self, visible):
|
||||||
|
self.text_browser.setVisible(visible)
|
||||||
|
self.open_button.setVisible(visible)
|
||||||
|
self.library_button.setVisible(visible)
|
||||||
|
self.delete_button.setVisible(visible)
|
||||||
|
self.edit_button.setVisible(visible)
|
||||||
|
|
||||||
|
def update_notes(self, annot):
|
||||||
|
if self.current_result:
|
||||||
|
self.current_result['annotation'] = annot
|
||||||
|
self.show_result(self.current_result)
|
||||||
|
|
||||||
def show_result(self, result_or_none):
|
def show_result(self, result_or_none):
|
||||||
self.current_result = r = result_or_none
|
self.current_result = r = result_or_none
|
||||||
if r is None:
|
if r is None:
|
||||||
self.text_browser.setVisible(False)
|
self.set_controls_visibility(False)
|
||||||
self.open_button.setVisible(False)
|
|
||||||
self.library_button.setVisible(False)
|
|
||||||
return
|
return
|
||||||
self.text_browser.setVisible(True)
|
self.set_controls_visibility(True)
|
||||||
self.open_button.setVisible(True)
|
|
||||||
self.library_button.setVisible(True)
|
|
||||||
db = current_db()
|
db = current_db()
|
||||||
book_id = r['book_id']
|
book_id = r['book_id']
|
||||||
title, authors = db.field_for('title', book_id), db.field_for('authors', book_id)
|
title, authors = db.field_for('title', book_id), db.field_for('authors', book_id)
|
||||||
@ -427,7 +446,9 @@ class DetailsPanel(QWidget):
|
|||||||
|
|
||||||
if annot['type'] == 'bookmark':
|
if annot['type'] == 'bookmark':
|
||||||
p(annot['title'])
|
p(annot['title'])
|
||||||
|
self.edit_button.setEnabled(False)
|
||||||
elif annot['type'] == 'highlight':
|
elif annot['type'] == 'highlight':
|
||||||
|
self.edit_button.setEnabled(True)
|
||||||
p(annot['highlighted_text'])
|
p(annot['highlighted_text'])
|
||||||
notes = annot.get('notes')
|
notes = annot.get('notes')
|
||||||
if notes:
|
if notes:
|
||||||
@ -509,6 +530,7 @@ class AnnotationsBrowser(Dialog):
|
|||||||
dp.open_annotation.connect(self.do_open_annotation)
|
dp.open_annotation.connect(self.do_open_annotation)
|
||||||
dp.show_book.connect(self.show_book)
|
dp.show_book.connect(self.show_book)
|
||||||
dp.delete_annotation.connect(self.delete_annotation)
|
dp.delete_annotation.connect(self.delete_annotation)
|
||||||
|
dp.edit_annotation.connect(self.edit_annotation)
|
||||||
bp.current_result_changed.connect(dp.show_result)
|
bp.current_result_changed.connect(dp.show_result)
|
||||||
|
|
||||||
h = QHBoxLayout()
|
h = QHBoxLayout()
|
||||||
@ -538,6 +560,21 @@ class AnnotationsBrowser(Dialog):
|
|||||||
def delete_annotation(self, annot_id):
|
def delete_annotation(self, annot_id):
|
||||||
self.delete_annotations(frozenset({annot_id}))
|
self.delete_annotations(frozenset({annot_id}))
|
||||||
|
|
||||||
|
def edit_annotation(self, annot_id, annot):
|
||||||
|
if annot.get('type') != 'highlight':
|
||||||
|
return error_dialog(self, _('Cannot edit'), _(
|
||||||
|
'Editing is only supported for the notes associated with highlights'), show=True)
|
||||||
|
notes = annot.get('notes')
|
||||||
|
notes, ok = QInputDialog.getMultiLineText(self, _('Edit notes for highlight'), '', notes)
|
||||||
|
if ok:
|
||||||
|
if notes and notes.strip():
|
||||||
|
annot['notes'] = notes.strip()
|
||||||
|
else:
|
||||||
|
annot.pop('notes', None)
|
||||||
|
db = current_db()
|
||||||
|
db.update_annotations({annot_id: annot})
|
||||||
|
self.details_panel.update_notes(annot)
|
||||||
|
|
||||||
def show_dialog(self):
|
def show_dialog(self):
|
||||||
if self.parent() is None:
|
if self.parent() is None:
|
||||||
self.browse_panel.effective_query_changed()
|
self.browse_panel.effective_query_changed()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user