E-book viewer: When displaying estimated time to completion for reading a book, remember the reading rate the next time the book is opened so that the period spent calculating the time remaining is reduced. Fixes #1852929 [Time to read book is not saved](https://bugs.launchpad.net/calibre/+bug/1852929)

This commit is contained in:
Kovid Goyal 2022-03-03 20:09:51 +05:30
parent b2c8f6f8ee
commit 439da2712a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 84 additions and 8 deletions

View File

@ -1,9 +1,13 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@ -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

View File

@ -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:

View File

@ -2,6 +2,8 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
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:

View File

@ -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