diff --git a/src/calibre/gui2/viewer/config.py b/src/calibre/gui2/viewer/config.py index fe1c0feb3f..02646ac499 100644 --- a/src/calibre/gui2/viewer/config.py +++ b/src/calibre/gui2/viewer/config.py @@ -1,9 +1,13 @@ #!/usr/bin/env python # License: GPL v3 Copyright: 2020, Kovid Goyal +import json import os -from calibre.constants import config_dir +import tempfile + +from calibre.constants import cache_dir, config_dir from calibre.utils.config import JSONConfig +from calibre.utils.filenames import atomic_rename vprefs = JSONConfig('viewer-webengine') viewer_config_dir = os.path.join(config_dir, 'viewer') @@ -26,3 +30,43 @@ def get_session_pref(name, default=None, group='standalone_misc_settings'): def get_pref_group(name): sd = vprefs['session_data'] return sd.get(name) or {} + + +def reading_rates_path(): + return os.path.join(cache_dir(), 'viewer-reading-rates.json') + + +def save_reading_rates(key, rates): + path = reading_rates_path() + try: + with open(path, 'rb') as f: + raw = f.read() + except OSError: + existing = {} + else: + existing = json.loads(raw) + existing.pop(key, None) + existing[key] = rates + while len(existing) > 50: + expired = next(iter(existing)) + del existing[expired] + ddata = json.dumps(existing, indent=2).encode('utf-8') + try: + with tempfile.NamedTemporaryFile(dir=os.path.dirname(path), delete=False) as f: + f.write(ddata) + atomic_rename(f.name, path) + except Exception: + import traceback + traceback.print_exc() + + +def load_reading_rates(key): + path = reading_rates_path() + try: + with open(path, 'rb') as f: + raw = f.read() + except OSError: + existing = {} + else: + existing = json.loads(raw) + return existing.get(key) diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 38288e8d3e..8c494bb415 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -30,7 +30,9 @@ from calibre.gui2.viewer.annotations import ( AnnotationsSaveWorker, annotations_dir, parse_annotations ) from calibre.gui2.viewer.bookmarks import BookmarkManager -from calibre.gui2.viewer.config import get_session_pref, vprefs +from calibre.gui2.viewer.config import ( + get_session_pref, load_reading_rates, save_reading_rates, vprefs +) from calibre.gui2.viewer.convert_book import clean_running_workers, prepare_book from calibre.gui2.viewer.highlights import HighlightsPanel from calibre.gui2.viewer.integration import ( @@ -191,6 +193,7 @@ class EbookViewer(MainWindow): self.web_view.scrollbar_context_menu.connect(self.scrollbar_context_menu) self.web_view.close_prep_finished.connect(self.close_prep_finished) self.web_view.highlights_changed.connect(self.highlights_changed) + self.web_view.update_reading_rates.connect(self.update_reading_rates) self.web_view.edit_book.connect(self.edit_book) self.actions_toolbar.initialize(self.web_view, self.search_dock.toggleViewAction()) at.update_action_state(False) @@ -487,6 +490,7 @@ class EbookViewer(MainWindow): self.setWindowTitle(_('Loading book') + f'… — {self.base_window_title}') self.loading_overlay(_('Loading book, please wait')) self.save_annotations() + self.save_reading_rates() self.current_book_data = {} get_current_book_data(self.current_book_data) self.search_widget.clear_searches() @@ -577,7 +581,8 @@ class EbookViewer(MainWindow): initial_position = {'type': 'bookpos', 'data': float(open_at)} highlights = self.current_book_data['annotations_map']['highlight'] self.highlights_widget.load(highlights) - self.web_view.start_book_load(initial_position=initial_position, highlights=highlights, current_book_data=self.current_book_data) + rates = load_reading_rates(self.current_book_data['annotations_path_key']) + self.web_view.start_book_load(initial_position=initial_position, highlights=highlights, current_book_data=self.current_book_data, reading_rates=rates) performance_monitor('webview loading requested') def load_book_data(self, calibre_book_data=None): @@ -666,6 +671,20 @@ class EbookViewer(MainWindow): get_session_pref('sync_annots_user', default='') ) + def update_reading_rates(self, rates): + if not self.current_book_data: + return + self.current_book_data['reading_rates'] = rates + self.save_reading_rates() + + def save_reading_rates(self): + if not self.current_book_data: + return + key = self.current_book_data.get('annotations_path_key') + rates = self.current_book_data.get('reading_rates') + if key and rates: + save_reading_rates(key, rates) + def highlights_changed(self, highlights): if not self.current_book_data: return @@ -763,6 +782,7 @@ class EbookViewer(MainWindow): try: self.save_state() self.save_annotations() + self.save_reading_rates() if self.annotations_saver is not None: self.annotations_saver.shutdown() self.annotations_saver = None diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index e32173e3e8..1d7b096ec3 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -274,6 +274,7 @@ class ViewerBridge(Bridge): edit_book = from_js(object, object, object) show_book_folder = from_js() show_help = from_js(object) + update_reading_rates = from_js(object) create_view = to_js() start_book_load = to_js() @@ -472,6 +473,7 @@ class WebView(RestartingWebEngineView): scrollbar_context_menu = pyqtSignal(object, object, object) close_prep_finished = pyqtSignal(object) highlights_changed = pyqtSignal(object) + update_reading_rates = pyqtSignal(object) edit_book = pyqtSignal(object, object, object) shortcuts_changed = pyqtSignal(object) paged_mode_changed = pyqtSignal() @@ -534,6 +536,7 @@ class WebView(RestartingWebEngineView): self.bridge.scrollbar_context_menu.connect(self.scrollbar_context_menu) self.bridge.close_prep_finished.connect(self.close_prep_finished) self.bridge.highlights_changed.connect(self.highlights_changed) + self.bridge.update_reading_rates.connect(self.update_reading_rates) self.bridge.edit_book.connect(self.edit_book) self.bridge.show_book_folder.connect(self.show_book_folder) self.bridge.show_help.connect(self.show_help) @@ -638,10 +641,10 @@ class WebView(RestartingWebEngineView): def on_content_file_changed(self, data): self.current_content_file = data - def start_book_load(self, initial_position=None, highlights=None, current_book_data=None): + def start_book_load(self, initial_position=None, highlights=None, current_book_data=None, reading_rates=None): key = (set_book_path.path,) book_url = link_prefix_for_location_links(add_open_at=False) - self.execute_when_ready('start_book_load', key, initial_position, set_book_path.pathtoebook, highlights or [], book_url) + self.execute_when_ready('start_book_load', key, initial_position, set_book_path.pathtoebook, highlights or [], book_url, reading_rates) def execute_when_ready(self, action, *args): if self.bridge.ready: diff --git a/src/pyj/read_book/timers.pyj b/src/pyj/read_book/timers.pyj index 728e9121c8..cabcca4c5c 100644 --- a/src/pyj/read_book/timers.pyj +++ b/src/pyj/read_book/timers.pyj @@ -2,6 +2,8 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal from __python__ import bound_methods, hash_literals +from read_book.globals import ui_operations + THRESHOLD = 5 FILTER_THRESHOLD = 25 MAX_SAMPLES = 256 @@ -16,6 +18,8 @@ class Timers: def start_book(self, book): self.reset_read_timer() self.rates = v'[]' + if book.saved_reading_rates?.rates: + self.rates = book.saved_reading_rates.rates.slice(0) def reset_read_timer(self): self.last_scroll_at = None @@ -54,6 +58,8 @@ class Timers: self.rates.shift() self.rates.push(rate) self.calculate() + if ui_operations.update_reading_rates: + ui_operations.update_reading_rates({'rates': self.rates.slice(0)}) def time_for(self, length): if length >= 0 and self.rates.length >= THRESHOLD and self.average > 0: diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index 8d75ba6b8e..a1ee403da2 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -95,7 +95,7 @@ def show_error(title, msg, details): to_python.show_error(title, msg, details) -def manifest_received(key, initial_position, pathtoebook, highlights, book_url, end_type, xhr, ev): +def manifest_received(key, initial_position, pathtoebook, highlights, book_url, reading_rates, end_type, xhr, ev): nonlocal book end_type = workaround_qt_bug(xhr, end_type) if end_type is 'load': @@ -107,6 +107,7 @@ def manifest_received(key, initial_position, pathtoebook, highlights, book_url, book.highlights = highlights book.stored_files = {} book.calibre_book_url = book_url + book.saved_reading_rates = reading_rates book.is_complete = True v'delete book.manifest["metadata"]' v'delete book.manifest["last_read_positions"]' @@ -228,8 +229,8 @@ def show_home_page(): @from_python -def start_book_load(key, initial_position, pathtoebook, highlights, book_url): - xhr = ajax('manifest', manifest_received.bind(None, key, initial_position, pathtoebook, highlights, book_url), ok_code=0) +def start_book_load(key, initial_position, pathtoebook, highlights, book_url, reading_rates): + xhr = ajax('manifest', manifest_received.bind(None, key, initial_position, pathtoebook, highlights, book_url, reading_rates), ok_code=0) xhr.responseType = 'json' xhr.send() @@ -439,6 +440,8 @@ if window is window.top: to_python.show_help(which) ui_operations.on_iframe_ready = def(): to_python.on_iframe_ready() + ui_operations.update_reading_rates = def(rates): + to_python.update_reading_rates(rates) document.body.appendChild(E.div(id='view')) window.onerror = onerror