mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
b2c8f6f8ee
commit
439da2712a
@ -1,9 +1,13 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
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.config import JSONConfig
|
||||||
|
from calibre.utils.filenames import atomic_rename
|
||||||
|
|
||||||
vprefs = JSONConfig('viewer-webengine')
|
vprefs = JSONConfig('viewer-webengine')
|
||||||
viewer_config_dir = os.path.join(config_dir, 'viewer')
|
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):
|
def get_pref_group(name):
|
||||||
sd = vprefs['session_data']
|
sd = vprefs['session_data']
|
||||||
return sd.get(name) or {}
|
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)
|
||||||
|
@ -30,7 +30,9 @@ from calibre.gui2.viewer.annotations import (
|
|||||||
AnnotationsSaveWorker, annotations_dir, parse_annotations
|
AnnotationsSaveWorker, annotations_dir, parse_annotations
|
||||||
)
|
)
|
||||||
from calibre.gui2.viewer.bookmarks import BookmarkManager
|
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.convert_book import clean_running_workers, prepare_book
|
||||||
from calibre.gui2.viewer.highlights import HighlightsPanel
|
from calibre.gui2.viewer.highlights import HighlightsPanel
|
||||||
from calibre.gui2.viewer.integration import (
|
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.scrollbar_context_menu.connect(self.scrollbar_context_menu)
|
||||||
self.web_view.close_prep_finished.connect(self.close_prep_finished)
|
self.web_view.close_prep_finished.connect(self.close_prep_finished)
|
||||||
self.web_view.highlights_changed.connect(self.highlights_changed)
|
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.web_view.edit_book.connect(self.edit_book)
|
||||||
self.actions_toolbar.initialize(self.web_view, self.search_dock.toggleViewAction())
|
self.actions_toolbar.initialize(self.web_view, self.search_dock.toggleViewAction())
|
||||||
at.update_action_state(False)
|
at.update_action_state(False)
|
||||||
@ -487,6 +490,7 @@ class EbookViewer(MainWindow):
|
|||||||
self.setWindowTitle(_('Loading book') + f'… — {self.base_window_title}')
|
self.setWindowTitle(_('Loading book') + f'… — {self.base_window_title}')
|
||||||
self.loading_overlay(_('Loading book, please wait'))
|
self.loading_overlay(_('Loading book, please wait'))
|
||||||
self.save_annotations()
|
self.save_annotations()
|
||||||
|
self.save_reading_rates()
|
||||||
self.current_book_data = {}
|
self.current_book_data = {}
|
||||||
get_current_book_data(self.current_book_data)
|
get_current_book_data(self.current_book_data)
|
||||||
self.search_widget.clear_searches()
|
self.search_widget.clear_searches()
|
||||||
@ -577,7 +581,8 @@ class EbookViewer(MainWindow):
|
|||||||
initial_position = {'type': 'bookpos', 'data': float(open_at)}
|
initial_position = {'type': 'bookpos', 'data': float(open_at)}
|
||||||
highlights = self.current_book_data['annotations_map']['highlight']
|
highlights = self.current_book_data['annotations_map']['highlight']
|
||||||
self.highlights_widget.load(highlights)
|
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')
|
performance_monitor('webview loading requested')
|
||||||
|
|
||||||
def load_book_data(self, calibre_book_data=None):
|
def load_book_data(self, calibre_book_data=None):
|
||||||
@ -666,6 +671,20 @@ class EbookViewer(MainWindow):
|
|||||||
get_session_pref('sync_annots_user', default='')
|
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):
|
def highlights_changed(self, highlights):
|
||||||
if not self.current_book_data:
|
if not self.current_book_data:
|
||||||
return
|
return
|
||||||
@ -763,6 +782,7 @@ class EbookViewer(MainWindow):
|
|||||||
try:
|
try:
|
||||||
self.save_state()
|
self.save_state()
|
||||||
self.save_annotations()
|
self.save_annotations()
|
||||||
|
self.save_reading_rates()
|
||||||
if self.annotations_saver is not None:
|
if self.annotations_saver is not None:
|
||||||
self.annotations_saver.shutdown()
|
self.annotations_saver.shutdown()
|
||||||
self.annotations_saver = None
|
self.annotations_saver = None
|
||||||
|
@ -274,6 +274,7 @@ class ViewerBridge(Bridge):
|
|||||||
edit_book = from_js(object, object, object)
|
edit_book = from_js(object, object, object)
|
||||||
show_book_folder = from_js()
|
show_book_folder = from_js()
|
||||||
show_help = from_js(object)
|
show_help = from_js(object)
|
||||||
|
update_reading_rates = from_js(object)
|
||||||
|
|
||||||
create_view = to_js()
|
create_view = to_js()
|
||||||
start_book_load = to_js()
|
start_book_load = to_js()
|
||||||
@ -472,6 +473,7 @@ class WebView(RestartingWebEngineView):
|
|||||||
scrollbar_context_menu = pyqtSignal(object, object, object)
|
scrollbar_context_menu = pyqtSignal(object, object, object)
|
||||||
close_prep_finished = pyqtSignal(object)
|
close_prep_finished = pyqtSignal(object)
|
||||||
highlights_changed = pyqtSignal(object)
|
highlights_changed = pyqtSignal(object)
|
||||||
|
update_reading_rates = pyqtSignal(object)
|
||||||
edit_book = pyqtSignal(object, object, object)
|
edit_book = pyqtSignal(object, object, object)
|
||||||
shortcuts_changed = pyqtSignal(object)
|
shortcuts_changed = pyqtSignal(object)
|
||||||
paged_mode_changed = pyqtSignal()
|
paged_mode_changed = pyqtSignal()
|
||||||
@ -534,6 +536,7 @@ class WebView(RestartingWebEngineView):
|
|||||||
self.bridge.scrollbar_context_menu.connect(self.scrollbar_context_menu)
|
self.bridge.scrollbar_context_menu.connect(self.scrollbar_context_menu)
|
||||||
self.bridge.close_prep_finished.connect(self.close_prep_finished)
|
self.bridge.close_prep_finished.connect(self.close_prep_finished)
|
||||||
self.bridge.highlights_changed.connect(self.highlights_changed)
|
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.edit_book.connect(self.edit_book)
|
||||||
self.bridge.show_book_folder.connect(self.show_book_folder)
|
self.bridge.show_book_folder.connect(self.show_book_folder)
|
||||||
self.bridge.show_help.connect(self.show_help)
|
self.bridge.show_help.connect(self.show_help)
|
||||||
@ -638,10 +641,10 @@ class WebView(RestartingWebEngineView):
|
|||||||
def on_content_file_changed(self, data):
|
def on_content_file_changed(self, data):
|
||||||
self.current_content_file = 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,)
|
key = (set_book_path.path,)
|
||||||
book_url = link_prefix_for_location_links(add_open_at=False)
|
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):
|
def execute_when_ready(self, action, *args):
|
||||||
if self.bridge.ready:
|
if self.bridge.ready:
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
from __python__ import bound_methods, hash_literals
|
from __python__ import bound_methods, hash_literals
|
||||||
|
|
||||||
|
from read_book.globals import ui_operations
|
||||||
|
|
||||||
THRESHOLD = 5
|
THRESHOLD = 5
|
||||||
FILTER_THRESHOLD = 25
|
FILTER_THRESHOLD = 25
|
||||||
MAX_SAMPLES = 256
|
MAX_SAMPLES = 256
|
||||||
@ -16,6 +18,8 @@ class Timers:
|
|||||||
def start_book(self, book):
|
def start_book(self, book):
|
||||||
self.reset_read_timer()
|
self.reset_read_timer()
|
||||||
self.rates = v'[]'
|
self.rates = v'[]'
|
||||||
|
if book.saved_reading_rates?.rates:
|
||||||
|
self.rates = book.saved_reading_rates.rates.slice(0)
|
||||||
|
|
||||||
def reset_read_timer(self):
|
def reset_read_timer(self):
|
||||||
self.last_scroll_at = None
|
self.last_scroll_at = None
|
||||||
@ -54,6 +58,8 @@ class Timers:
|
|||||||
self.rates.shift()
|
self.rates.shift()
|
||||||
self.rates.push(rate)
|
self.rates.push(rate)
|
||||||
self.calculate()
|
self.calculate()
|
||||||
|
if ui_operations.update_reading_rates:
|
||||||
|
ui_operations.update_reading_rates({'rates': self.rates.slice(0)})
|
||||||
|
|
||||||
def time_for(self, length):
|
def time_for(self, length):
|
||||||
if length >= 0 and self.rates.length >= THRESHOLD and self.average > 0:
|
if length >= 0 and self.rates.length >= THRESHOLD and self.average > 0:
|
||||||
|
@ -95,7 +95,7 @@ def show_error(title, msg, details):
|
|||||||
to_python.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
|
nonlocal book
|
||||||
end_type = workaround_qt_bug(xhr, end_type)
|
end_type = workaround_qt_bug(xhr, end_type)
|
||||||
if end_type is 'load':
|
if end_type is 'load':
|
||||||
@ -107,6 +107,7 @@ def manifest_received(key, initial_position, pathtoebook, highlights, book_url,
|
|||||||
book.highlights = highlights
|
book.highlights = highlights
|
||||||
book.stored_files = {}
|
book.stored_files = {}
|
||||||
book.calibre_book_url = book_url
|
book.calibre_book_url = book_url
|
||||||
|
book.saved_reading_rates = reading_rates
|
||||||
book.is_complete = True
|
book.is_complete = True
|
||||||
v'delete book.manifest["metadata"]'
|
v'delete book.manifest["metadata"]'
|
||||||
v'delete book.manifest["last_read_positions"]'
|
v'delete book.manifest["last_read_positions"]'
|
||||||
@ -228,8 +229,8 @@ def show_home_page():
|
|||||||
|
|
||||||
|
|
||||||
@from_python
|
@from_python
|
||||||
def start_book_load(key, initial_position, pathtoebook, highlights, book_url):
|
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), ok_code=0)
|
xhr = ajax('manifest', manifest_received.bind(None, key, initial_position, pathtoebook, highlights, book_url, reading_rates), ok_code=0)
|
||||||
xhr.responseType = 'json'
|
xhr.responseType = 'json'
|
||||||
xhr.send()
|
xhr.send()
|
||||||
|
|
||||||
@ -439,6 +440,8 @@ if window is window.top:
|
|||||||
to_python.show_help(which)
|
to_python.show_help(which)
|
||||||
ui_operations.on_iframe_ready = def():
|
ui_operations.on_iframe_ready = def():
|
||||||
to_python.on_iframe_ready()
|
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'))
|
document.body.appendChild(E.div(id='view'))
|
||||||
window.onerror = onerror
|
window.onerror = onerror
|
||||||
|
Loading…
x
Reference in New Issue
Block a user