From 575422bab2f9186e1e5f68633b563b1a35614381 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 Apr 2018 07:50:01 +0530 Subject: [PATCH] Start work on time left display for browser viewer --- src/pyj/read_book/iframe.pyj | 3 ++ src/pyj/read_book/paged_mode.pyj | 24 +++++++++++--- src/pyj/read_book/timers.pyj | 56 ++++++++++++++++++++++++++++++++ src/pyj/read_book/view.pyj | 14 ++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 src/pyj/read_book/timers.pyj diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 78958d2877..6ade378da0 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -170,6 +170,9 @@ class IframeBoss: def gesture_from_margin(self, data): self.handle_gesture(data.gesture) + def report_human_scroll(self, scrolled_by_frac, is_large_scroll): + self.send_message('human_scroll', scrolled_by_frac=scrolled_by_frac or None, is_large_scroll=v'!!is_large_scroll') + def on_scroll_to_anchor(self, data): frag = data.frag if frag: diff --git a/src/pyj/read_book/paged_mode.pyj b/src/pyj/read_book/paged_mode.pyj index 0209169c15..22a15e0972 100644 --- a/src/pyj/read_book/paged_mode.pyj +++ b/src/pyj/read_book/paged_mode.pyj @@ -56,13 +56,13 @@ def create_page_div(elem): _in_paged_mode = False def in_paged_mode(): return _in_paged_mode -col_width = screen_width = screen_height = cols_per_screen = gap = col_and_gap = 0 +col_width = screen_width = screen_height = cols_per_screen = gap = col_and_gap = number_of_cols = 0 is_full_screen_layout = False def reset_paged_mode_globals(): - nonlocal _in_paged_mode, col_width, col_and_gap, screen_height, gap, screen_width, is_full_screen_layout, cols_per_screen + nonlocal _in_paged_mode, col_width, col_and_gap, screen_height, gap, screen_width, is_full_screen_layout, cols_per_screen, number_of_cols scroll_viewport.reset_globals() - col_width = screen_width = screen_height = cols_per_screen = gap = col_and_gap = 0 + col_width = screen_width = screen_height = cols_per_screen = gap = col_and_gap = number_of_cols = 0 is_full_screen_layout = _in_paged_mode = False def column_at(xpos): @@ -216,12 +216,14 @@ def layout(is_single_page): # themselves, unless the container width is an exact multiple, so we check # for that and manually set the container widths. def check_column_widths(): - ncols = (scroll_viewport.paged_content_width() + gap) / col_and_gap + nonlocal number_of_cols + ncols = number_of_cols = (scroll_viewport.paged_content_width() + gap) / col_and_gap if ncols is not Math.floor(ncols): - n = Math.floor(ncols) + n = number_of_cols = Math.floor(ncols) dw = n*col_width + (n-1)*gap data = {'col_width':col_width, 'gap':gap, 'scrollWidth':scroll_viewport.paged_content_width(), 'ncols':ncols, 'desired_width':dw} return data + data = check_column_widths() if data: dw = data.desired_width @@ -459,11 +461,19 @@ def onwheel(evt): def scroll_by_page(backward, by_screen): if by_screen: pos = previous_screen_location() if backward else next_screen_location() + pages = cols_per_screen else: pos = previous_col_location() if backward else next_col_location() + pages = 1 if pos is -1: + get_boss().report_human_scroll() get_boss().send_message('next_spine_item', previous=backward) else: + if not backward: + scrolled_frac = (pages / number_of_cols) if number_of_cols > 0 else 0 + get_boss().report_human_scroll(scrolled_frac, True) + else: + get_boss().report_human_scroll() scroll_to_xpos(pos) def onkeydown(evt): @@ -472,21 +482,25 @@ def onkeydown(evt): if key is 'up' or key is 'down': handled = True if evt.ctrlKey: + get_boss().report_human_scroll() scroll_to_offset(0 if key is 'left' else document_width()) else: scroll_by_page(key is 'up', True) elif (key is 'left' or key is 'right') and not evt.altKey: handled = True if evt.ctrlKey: + get_boss().report_human_scroll() scroll_to_offset(0 if key is 'left' else document_width()) else: scroll_by_page(key is 'left', False) elif key is 'home' or key is 'end': handled = True if evt.ctrlKey: + get_boss().report_human_scroll() get_boss().send_message('goto_doc_boundary', start=key is 'home') else: if key is 'home': + get_boss().report_human_scroll() scroll_to_offset(0) else: scroll_to_offset(document_width()) diff --git a/src/pyj/read_book/timers.pyj b/src/pyj/read_book/timers.pyj new file mode 100644 index 0000000000..4897421417 --- /dev/null +++ b/src/pyj/read_book/timers.pyj @@ -0,0 +1,56 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal +from __python__ import bound_methods, hash_literals + +THRESHOLD = 5 +FILTER_THRESHOLD = 25 +MAX_SAMPLES = 256 + +class Timers: + + def __init__(self): + self.reset_read_timer() + self.rates = v'[]' + self.average = self.stddev = 0 + + def start_book(self, book): + self.reset_read_timer() + self.rates = v'[]' + + def reset_read_timer(self): + self.last_scroll_at = None + + def calculate(self): + rates = self.rates + rlen = rates.length + if rlen >= THRESHOLD: + avg = 0 + for v'var i = 0; i < rlen; i++': + avg += rates[i] + avg /= rlen + self.average = avg + sq = 0 + for v'var i = 0; i < rlen; i++': + x = rates[i] + sq += (x - avg) * (x - avg) + self.stddev = Math.sqrt(sq / (rlen - 1)) + else: + self.average = self.stddev = 0 + + def on_human_scroll(self, amt_scrolled, is_large_scroll): + last_scroll_at = self.last_scroll_at + self.last_scroll_at = now = window.performance.now() + if last_scroll_at is None: + return + time_since_last_scroll = now - last_scroll_at + if time_since_last_scroll <= 0 or time_since_last_scroll >= 300: + return + if is_large_scroll and time_since_last_scroll < 2: + return + rate = amt_scrolled / time_since_last_scroll + if self.rates.length >= FILTER_THRESHOLD and Math.abs(rate - self.average) > 2 * self.stddev: + return + if self.rates.length >= MAX_SAMPLES: + self.rates.shift() + self.rates.push(rate) + self.calculate() diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index b1c844b5a3..b3c0b06ef2 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -23,6 +23,7 @@ from read_book.prefs.font_size import change_font_size_by from read_book.prefs.head_foot import render_head_foot from read_book.resources import load_resources from read_book.search import SearchOverlay, find_in_spine +from read_book.timers import Timers from read_book.toc import get_current_toc_nodes, update_visible_toc_nodes from read_book.touch import set_left_margin_handler, set_right_margin_handler from session import get_device_uuid, get_interface_data @@ -116,6 +117,7 @@ class View: def __init__(self, container, ui): self.ui = ui + self.timers = Timers() self.loaded_resources = {} self.current_progress_frac = 0 self.current_toc_node = self.current_toc_toplevel_node = None @@ -163,6 +165,7 @@ class View: 'request_size': self.on_request_size, 'show_footnote': self.on_show_footnote, 'print': self.on_print, + 'human_scroll': self.on_human_scroll, } self.iframe_wrapper = IframeWrapper(handlers, document.getElementById(iframe_id), 'read_book.iframe', _('Bootstrapping book reader...')) self.search_overlay = SearchOverlay(self) @@ -214,6 +217,16 @@ class View: def on_print(self, data): print(data.string) + def on_human_scroll(self, data): + if data.scrolled_by_frac is None: + self.timers.reset_read_timer() + else: + name = self.currently_showing.name + length = self.book.manifest.files[name]?.length + if length: + amt_scrolled = data.scrolled_by_frac * length + self.timers.on_human_scroll(amt_scrolled, data.is_large_scroll) + def find(self, text, backwards): self.iframe_wrapper.send_message('find', text=text, backwards=backwards, searched_in_spine=False) @@ -361,6 +374,7 @@ class View: self.content_popup_overlay.iframe_wrapper.reset() self.loaded_resources = {} self.content_popup_overlay.loaded_resources = {} + self.timers.start_book(book) self.book = current_book.book = book self.ui.db.update_last_read_time(book) pos = {'replace_history':True}