From 21f3348ab71507fadf2e5c0f0060090c3f9d6d45 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 9 Jul 2020 16:43:50 +0530 Subject: [PATCH] Annots browser: Allow browsing upto 4096 annotations with no search query --- src/calibre/db/backend.py | 39 ++++++++++++++++- src/calibre/db/cache.py | 4 ++ src/calibre/gui2/library/annotations.py | 57 ++++++++++++++----------- src/calibre/gui2/viewer/widgets.py | 8 +++- 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index 46d1e2cdcb..61ced67000 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -1820,12 +1820,49 @@ class DB(object): raise FTSQueryError(fts_engine_query, query, e) def all_annotations_for_book(self, book_id): - for (fmt, user_type, user, data) in self.execute('SELECT format, user_type, user, annot_data FROM annotations WHERE book=?', (book_id,)): + for (fmt, user_type, user, data) in self.execute('SELECT id, book, format, user_type, user, annot_data FROM annotations WHERE book=?', (book_id,)): + try: yield {'format': fmt, 'user_type': user_type, 'user': user, 'annotation': json.loads(data)} except Exception: pass + def all_annotations(self, restrict_to_user=None, limit=None, annotation_type=None): + ls = json.loads + q = 'SELECT id, book, format, user_type, user, annot_data FROM annotations' + data = [] + if restrict_to_user or annotation_type: + q += ' WHERE ' + if restrict_to_user is not None: + data.extend(restrict_to_user) + q += ' user_type = ? AND user = ?' + if annotation_type: + data.append(annotation_type) + q += ' annot_type = ? ' + q += ' ORDER BY timestamp' + if limit is not None: + q += ' LIMIT %d' % limit + for (rowid, book_id, fmt, user_type, user, annot_data) in self.execute(q, tuple(data)): + try: + annot = ls(annot_data) + atype = annot['type'] + except Exception: + continue + text = '' + if atype == 'bookmark': + text = annot['title'] + elif atype == 'highlight': + text = annot.get('highlighted_text') or '' + yield { + 'id': rowid, + 'book_id': book_id, + 'format': fmt, + 'user_type': user_type, + 'user': user, + 'text': text, + 'annotation': annot, + } + def all_annotation_users(self): return self.execute('SELECT DISTINCT user_type, user FROM annotations') diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index e26f1c55bc..b58e936aee 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -2310,6 +2310,10 @@ class Cache(object): def all_annotation_types(self): return tuple(self.backend.all_annotation_types()) + @read_api + def all_annotations(self, restrict_to_user=None, limit=None, annotation_type=None): + return tuple(self.backend.all_annotations(restrict_to_user, limit, annotation_type)) + @read_api def search_annotations( self, diff --git a/src/calibre/gui2/library/annotations.py b/src/calibre/gui2/library/annotations.py index b885e35cb0..ebded8dfee 100644 --- a/src/calibre/gui2/library/annotations.py +++ b/src/calibre/gui2/library/annotations.py @@ -9,7 +9,7 @@ from textwrap import fill from PyQt5.Qt import ( QApplication, QCheckBox, QComboBox, QCursor, QFont, QHBoxLayout, QIcon, QLabel, - QPalette, QPushButton, QSize, QSplitter, Qt, QTextBrowser, QToolButton, + QPalette, QPushButton, QSize, QSplitter, Qt, QTextBrowser, QTimer, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal ) @@ -43,16 +43,17 @@ class BusyCursor(object): class AnnotsResultsDelegate(ResultsDelegate): add_ellipsis = False + emphasize_text = True def result_data(self, result): if not isinstance(result, dict): return None, None, None, None - full_text = result['text'].replace('0x1f', ' ') - parts = full_text.split('\x1d') + full_text = result['text'].replace('\x1f', ' ') + parts = full_text.split('\x1d', 2) before = after = '' if len(parts) > 2: before, text = parts[:2] - after = ' '.join(parts[2:]).replace('\x1d', '') + after = parts[2].replace('\x1d', '') elif len(parts) == 2: before, text = parts else: @@ -82,8 +83,9 @@ class ResultsList(QTreeWidget): if isinstance(r, dict): self.open_annotation.emit(r['book_id'], r['format'], r['annotation']) - def set_results(self, results): + def set_results(self, results, emphasize_text): self.clear() + self.delegate.emphasize_text = emphasize_text self.number_of_results = 0 self.item_map = [] book_id_map = {} @@ -191,8 +193,12 @@ class Restrictions(QWidget): tb.clear() tb.addItem(' ', ' ') for user_type, user in db.all_annotation_users(): - q = '{}: {}'.format(user_type, user) - tb.addItem(q, '{}:{}'.format(user_type, user)) + q = display_name = '{}: {}'.format(user_type, user) + if q == 'web: *': + display_name = _('Anonymous content server users') + elif q == 'local: viewer': + display_name = _('Local viewer users') + tb.addItem(display_name, '{}:{}'.format(user_type, user)) if before: row = tb.findData(before) if row > -1: @@ -218,7 +224,7 @@ class BrowsePanel(QWidget): l.addLayout(h) self.search_box = sb = SearchBox(self) sb.initialize('library-annotations-browser-search-box') - sb.cleared.connect(self.cleared) + sb.cleared.connect(self.cleared, type=Qt.QueuedConnection) sb.lineEdit().returnPressed.connect(self.show_next) sb.lineEdit().setPlaceholderText(_('Enter words to search for')) h.addWidget(sb) @@ -251,45 +257,46 @@ class BrowsePanel(QWidget): db = current_db() self.search_box.setFocus(Qt.OtherFocusReason) self.restrictions.re_initialize(db) - self.cleared() + self.current_query = None + self.results_list.clear() def sizeHint(self): return QSize(450, 600) + @property + def restrict_to_user(self): + user = self.restrictions.user_box.currentData() + if user and ':' in user: + return user.split(':', 1) + @property def effective_query(self): text = self.search_box.lineEdit().text().strip() - if not text: - return None atype = self.restrictions.types_box.currentData() - if not atype or not atype.strip(): - atype = None - user = self.restrictions.user_box.currentData() - restrict_to_user = None - if user and ':' in user: - restrict_to_user = user.split(':', 1) return { 'fts_engine_query': text, - 'annotation_type': atype, - 'restrict_to_user': restrict_to_user, + 'annotation_type': (atype or '').strip(), + 'restrict_to_user': self.restrict_to_user, 'use_stemming': bool(self.use_stemmer.isChecked()), } def cleared(self): self.current_query = None - self.results_list.clear() + self.effective_query_changed() def do_find(self, backwards=False): q = self.effective_query - if not q: - return if q == self.current_query: self.results_list.show_next(backwards) return with BusyCursor(): db = current_db() - results = db.search_annotations(highlight_start='\x1d', highlight_end='\x1d', snippet_size=64, **q) - self.results_list.set_results(results) + if not q['fts_engine_query']: + results = db.all_annotations(restrict_to_user=q['restrict_to_user'], limit=4096, annotation_type=q['annotation_type']) + else: + results = db.search_annotations(highlight_start='\x1d', highlight_end='\x1d', snippet_size=64, **q) + + self.results_list.set_results(results, bool(q['fts_engine_query'])) self.current_query = q def effective_query_changed(self): @@ -454,6 +461,8 @@ class AnnotationsBrowser(Dialog): h.addWidget(us), h.addStretch(10), h.addWidget(self.bb) def show_dialog(self): + if self.browse_panel.current_query is None: + QTimer.singleShot(80, self.browse_panel.effective_query_changed) if self.parent() is None: self.exec_() else: diff --git a/src/calibre/gui2/viewer/widgets.py b/src/calibre/gui2/viewer/widgets.py index c70d816e95..8de34d0d80 100644 --- a/src/calibre/gui2/viewer/widgets.py +++ b/src/calibre/gui2/viewer/widgets.py @@ -17,6 +17,7 @@ from calibre.gui2.widgets2 import HistoryComboBox class ResultsDelegate(QStyledItemDelegate): # {{{ add_ellipsis = True + emphasize_text = True def result_data(self, result): if not hasattr(result, 'is_hidden'): @@ -37,8 +38,11 @@ class ResultsDelegate(QStyledItemDelegate): # {{{ c = p.color(group, c) painter.setPen(c) font = option.font - emphasis_font = QFont(font) - emphasis_font.setBold(True) + if self.emphasize_text: + emphasis_font = QFont(font) + emphasis_font.setBold(True) + else: + emphasis_font = font flags = Qt.AlignTop | Qt.TextSingleLine | Qt.TextIncludeTrailingSpaces rect = option.rect.adjusted(option.decorationSize.width() + 4 if is_hidden else 0, 0, 0, 0) painter.setClipRect(rect)