From 742f322f89f6baf2f01c5af5bce6f02af22e9180 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 1 Aug 2019 19:50:48 +0530 Subject: [PATCH] Implement save/restore of last read position --- src/calibre/gui2/viewer/ui.py | 43 +++++++++++++++++++++++++++-- src/calibre/gui2/viewer/web_view.py | 12 +++++--- src/pyj/read_book/view.pyj | 4 +-- src/pyj/viewer-main.pyj | 8 +++--- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 587d895c2d..2f074041b5 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -7,15 +7,22 @@ from __future__ import absolute_import, division, print_function, unicode_litera import os from threading import Thread -from PyQt5.Qt import QDockWidget, Qt, pyqtSignal +from PyQt5.Qt import QDockWidget, Qt, QTimer, pyqtSignal from calibre.gui2 import error_dialog from calibre.gui2.main_window import MainWindow from calibre.gui2.viewer.convert_book import prepare_book from calibre.gui2.viewer.web_view import WebView, set_book_path +from calibre.utils.config import JSONConfig from calibre.utils.ipc.simple_worker import WorkerError +def viewer_data(): + if not hasattr(viewer_data, 'ans'): + viewer_data.ans = JSONConfig('viewer-data') + return viewer_data.ans + + class EbookViewer(MainWindow): msg_from_anotherinstance = pyqtSignal(object) @@ -23,6 +30,9 @@ class EbookViewer(MainWindow): def __init__(self): MainWindow.__init__(self, None) + self.current_book_data = {} + self.save_cfi_debounce_timer = t = QTimer(self) + t.setInterval(2000), t.timeout.connect(self.save_cfi) self.book_prepared.connect(self.load_finished, type=Qt.QueuedConnection) def create_dock(title, name, area, areas=Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea): @@ -35,6 +45,7 @@ class EbookViewer(MainWindow): self.toc_dock = create_dock(_('Table of Contents'), 'toc-dock', Qt.LeftDockWidgetArea) self.inspector_dock = create_dock(_('Inspector'), 'inspector', Qt.RightDockWidgetArea) self.web_view = WebView(self) + self.web_view.cfi_changed.connect(self.cfi_changed) self.setCentralWidget(self.web_view) def handle_commandline_arg(self, arg): @@ -51,6 +62,9 @@ class EbookViewer(MainWindow): def load_ebook(self, pathtoebook, open_at=None): # TODO: Implement open_at + if self.save_cfi_debounce_timer.isActive(): + self.save_cfi() + self.current_book_data = {} t = Thread(name='LoadBook', target=self._load_ebook_worker, args=(pathtoebook, open_at)) t.daemon = True t.start() @@ -73,4 +87,29 @@ class EbookViewer(MainWindow): det_msg=data['tb'], show=True) return set_book_path(data['base']) - self.web_view.start_book_load() + self.current_book_data = data + self.web_view.start_book_load(initial_cfi=self.initial_cfi_for_current_book()) + + def initial_cfi_for_current_book(self): + vd = viewer_data() + lrp = vd.get('last-read-positions', {}) + return lrp.get('path', {}).get(self.current_book_data['pathtoebook']) + + def cfi_changed(self, cfi): + if not self.current_book_data: + return + self.current_book_data['last_known_cfi'] = cfi + self.save_cfi_debounce_timer.start() + + def save_cfi(self): + self.save_cfi_debounce_timer.stop() + vd = viewer_data() + lrp = vd.get('last-read-positions', {}) + path = lrp.setdefault('path', {}) + path[self.current_book_data['pathtoebook']] = self.current_book_data['last_known_cfi'] + vd.set('last-read-positions', lrp) + + def closeEvent(self, ev): + if self.save_cfi_debounce_timer.isActive(): + self.save_cfi() + return MainWindow.closeEvent(self, ev) diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 88c1417821..1b7e62e5bc 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -7,7 +7,8 @@ from __future__ import absolute_import, division, print_function, unicode_litera import os from PyQt5.Qt import ( - QApplication, QBuffer, QByteArray, QHBoxLayout, QSize, QTimer, QUrl, QWidget + QApplication, QBuffer, QByteArray, QHBoxLayout, QSize, QTimer, QUrl, QWidget, + pyqtSignal ) from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler from PyQt5.QtWebEngineWidgets import ( @@ -238,6 +239,8 @@ class Inspector(QWidget): class WebView(RestartingWebEngineView): + cfi_changed = pyqtSignal(object) + def __init__(self, parent=None): self._host_widget = None self.current_cfi = None @@ -266,6 +269,7 @@ class WebView(RestartingWebEngineView): cfi = frag[len('bookpos='):] if cfi: self.current_cfi = cfi + self.cfi_changed.emit(cfi) @property def host_widget(self): @@ -305,12 +309,12 @@ class WebView(RestartingWebEngineView): for func, args in iteritems(self.pending_bridge_ready_actions): getattr(self.bridge, func)(*args) - def start_book_load(self): + def start_book_load(self, initial_cfi=None): key = (set_book_path.path,) if self.bridge.ready: - self.bridge.start_book_load(key, vprefs['session_data']) + self.bridge.start_book_load(key, vprefs['session_data'], initial_cfi) else: - self.pending_bridge_ready_actions['start_book_load'] = key, vprefs['session_data'] + self.pending_bridge_ready_actions['start_book_load'] = key, vprefs['session_data'], initial_cfi def set_session_data(self, key, val): if key == '*' and val is None: diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index b7f3242f8b..4a169f9f90 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -367,7 +367,7 @@ class View: cfi = '/' + rest return name, cfi - def display_book(self, book): + def display_book(self, book, initial_cfi): self.hide_overlays() self.iframe.focus() is_current_book = self.book and self.book.key == book.key @@ -383,7 +383,7 @@ class View: pos = {'replace_history':True} unkey = username_key(get_interface_data().username) name = book.manifest.spine[0] - cfi = None + cfi = initial_cfi or None q = parse_url_params() if q.bookpos and q.bookpos.startswith('epubcfi(/'): cfi = q.bookpos diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index 3a4cf247aa..1cb4aa18f3 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -126,7 +126,7 @@ def show_error(title, msg, details): error_dialog(title, msg, details) -def manifest_received(key, end_type, xhr, ev): +def manifest_received(key, initial_cfi, end_type, xhr, ev): nonlocal book if end_type is 'load': book = new_book(key, {}) @@ -137,7 +137,7 @@ def manifest_received(key, end_type, xhr, ev): book.is_complete = True v'delete book.manifest["metadata"]' v'delete book.manifest["last_read_positions"]' - view.display_book(book) + view.display_book(book, initial_cfi) else: error_dialog(_('Could not open book'), _( 'Failed to load book manifest, click "Show details" for more info'), @@ -177,12 +177,12 @@ def create_session_data(prefs): @from_python -def start_book_load(key, prefs): +def start_book_load(key, prefs, initial_cfi): nonlocal view if view is None: create_session_data(prefs) view = View(document.getElementById('view')) - xhr = ajax('manifest', manifest_received.bind(None, key), ok_code=0) + xhr = ajax('manifest', manifest_received.bind(None, key, initial_cfi), ok_code=0) xhr.responseType = 'json' xhr.send()