From 281592bdaa837761aba5eeb8468fc6eb27041cf6 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Sun, 22 Dec 2019 22:26:07 -0700 Subject: [PATCH 1/9] Fix what I assume is a copy/paste oversight --- src/pyj/read_book/view.pyj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 2208ad31d5..82e291028f 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -133,7 +133,7 @@ def margin_elem(sd, which, id, onclick, oncontextmenu): if onclick: ans.addEventListener('click', onclick) if oncontextmenu: - ans.addEventListener('contextmenu', onclick) + ans.addEventListener('contextmenu', oncontextmenu) if is_ios and which is 'margin_bottom' and not window.navigator.standalone and not /CriOS\//.test(window.navigator.userAgent): # On iOS Safari 100vh includes the size of the navbar and there is no way to # go fullscreen, so to make the bottom bar visible we add a margin to From 94ac5ef102954da4a2a66618a2c40ebd5ad393e3 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Mon, 23 Dec 2019 22:24:17 -0700 Subject: [PATCH 2/9] Rework flow mode scrolling to be smoother and deterministic This is the first step towards built-in autoscroll and configurable scroll speed. The old impl is jumpy and inconsistent, ie: scrolling up N times then down N times may not result in the same start and end positions. --- src/pyj/read_book/flow_mode.pyj | 73 +++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index 05e081407e..9059e790fe 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -10,6 +10,9 @@ from read_book.viewport import scroll_viewport from utils import document_height, viewport_to_document +def line_height(): + return parseFloat(line_height.doc_style.lineHeight) + def flow_to_scroll_fraction(frac, on_initial_load): scroll_viewport.scroll_to(0, document_height() * frac) @@ -95,7 +98,7 @@ def flow_onwheel(evt): if evt.deltaMode is WheelEvent.DOM_DELTA_PIXEL: dy = evt.deltaY elif evt.deltaMode is WheelEvent.DOM_DELTA_LINE: - dy = 15 * evt.deltaY + dy = line_height() * evt.deltaY if evt.deltaMode is WheelEvent.DOM_DELTA_PAGE: dy = (scroll_viewport.height() - 30) * evt.deltaY if evt.deltaX: @@ -110,21 +113,6 @@ def flow_onwheel(evt): elif Math.abs(dy) >= 1: scroll_by(dy) -smooth_y_data = {'last_event_at':0, 'up': False, 'timer':None, 'source':'wheel', 'pixels_per_ms': 0.2, 'scroll_interval':10, 'stop_scrolling_after':100} - -def do_y_scroll(): - dy = (-1 if smooth_y_data.up else 1) * smooth_y_data.pixels_per_ms * smooth_y_data.scroll_interval - if Math.abs(dy) >= 1 and scroll_by(dy): - if Date.now() - smooth_y_data.last_event_at < smooth_y_data.stop_scrolling_after: - smooth_y_data.timer = setTimeout(do_y_scroll, smooth_y_data.scroll_interval) - -def smooth_y_scroll(up): - clearTimeout(smooth_y_data.timer) - smooth_y_data.last_event_at = Date.now() - smooth_y_data.up = up - do_y_scroll() - - @check_for_scroll_end def goto_boundary(y): scroll_viewport.scroll_to(window.pageXOffset, 0) @@ -139,10 +127,10 @@ def scroll_by_page(direction): def handle_shortcut(sc_name, evt): if sc_name is 'down': - smooth_y_scroll(False) + scroll_animator.start(DIRECTION.Down) return True if sc_name is 'up': - smooth_y_scroll(True) + scroll_animator.start(DIRECTION.Up) return True if sc_name is 'start_of_file': goto_boundary(-1) @@ -173,6 +161,55 @@ def handle_shortcut(sc_name, evt): def layout(is_single_page): set_css(document.body, margin='0', border_width='0', padding='0') + line_height.doc_style = window.getComputedStyle(document.body) + + +DIRECTION = {'Up': -1, 'Down': 1} +class ScrollAnimator: + DURATION = 100 # milliseconds + SCROLL_SPEED = 30 # lines/sec TODO: This will be configurable + + def __init__(self): + self.animation_id = None + + def start(self, direction): + now = performance.now() + self.end_time = now + self.DURATION + + if self.animation_id is None or direction != self.direction: + self.stop() + self.direction = direction + self.start_time = now + self.start_offset = window.pageYOffset + self.animation_id = window.requestAnimationFrame(self.smooth_scroll) + + def smooth_scroll(self, ts): + duration = (self.end_time - self.start_time) + progress = max(0, min(1, (ts - self.start_time) / duration)) # max/min to account for jitter + scroll_target = self.start_offset + scroll_target += Math.trunc(self.direction * progress * duration * line_height() * self.SCROLL_SPEED) / 1000 + + window.scrollTo(0, scroll_target) + + if progress < 1: + self.animation_id = window.requestAnimationFrame(self.smooth_scroll) + else: + self.animation_id = None + amt = window.pageYOffset - self.start_offset + if abs(amt) < 3 and duration is self.DURATION: + get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up) + else: + report_human_scroll(amt) + + def stop(self): + if self.animation_id is not None: + window.cancelAnimationFrame(self.animation_id) + self.animation_id = None + amt = window.pageYOffset - self.start_offset + if amt > 0: + report_human_scroll(amt) + +scroll_animator = ScrollAnimator() class FlickAnimator: From 208734f47b7bfcecb9e3d7593a752e76b8a38ca7 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Tue, 17 Dec 2019 23:05:43 -0700 Subject: [PATCH 3/9] Sort default settings and larger handler definitions Use session defaults in reader settings to reduce code duplication --- src/pyj/read_book/iframe.pyj | 22 +++++----- src/pyj/read_book/settings.pyj | 23 +++++----- src/pyj/read_book/view.pyj | 34 +++++++-------- src/pyj/session.pyj | 76 +++++++++++++++++----------------- 4 files changed, 78 insertions(+), 77 deletions(-) diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index ea8d5ee03a..02bb34276b 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -91,22 +91,22 @@ class IframeBoss: self.forward_keypresses = False set_boss(self) handlers = { - 'initialize':self.initialize, - 'display': self.display, - 'scroll_to_anchor': self.on_scroll_to_anchor, - 'scroll_to_ref': self.on_scroll_to_ref, - 'scroll_to_frac': self.on_scroll_to_frac, - 'next_screen': self.on_next_screen, - 'change_font_size': self.change_font_size, 'change_color_scheme': self.change_color_scheme, - 'gesture_from_margin': self.gesture_from_margin, - 'wheel_from_margin': self.wheel_from_margin, + 'change_font_size': self.change_font_size, + 'display': self.display, 'find': self.find, - 'window_size': self.received_window_size, + 'gesture_from_margin': self.gesture_from_margin, 'get_current_cfi': self.get_current_cfi, + 'initialize':self.initialize, + 'modify_selection': self.modify_selection, + 'next_screen': self.on_next_screen, + 'scroll_to_anchor': self.on_scroll_to_anchor, + 'scroll_to_frac': self.on_scroll_to_frac, + 'scroll_to_ref': self.on_scroll_to_ref, 'set_forward_keypresses': self.set_forward_keypresses, 'set_reference_mode': self.set_reference_mode, - 'modify_selection': self.modify_selection, + 'wheel_from_margin': self.wheel_from_margin, + 'window_size': self.received_window_size, } self.comm = IframeClient(handlers) self.last_window_ypos = 0 diff --git a/src/pyj/read_book/settings.pyj b/src/pyj/read_book/settings.pyj index cf922e4fe3..e0892e75cd 100644 --- a/src/pyj/read_book/settings.pyj +++ b/src/pyj/read_book/settings.pyj @@ -4,23 +4,24 @@ from __python__ import hash_literals from elementmaker import E from read_book.globals import runtime +from session import defaults opts = {} def update_settings(settings): - settings = settings or {} - opts.columns_per_screen = settings.columns_per_screen or {'portrait':0, 'landscape':0} - opts.margin_left = max(0, settings.margin_left or 0) - opts.margin_right = max(0, settings.margin_right or 0) - opts.color_scheme = settings.color_scheme - opts.base_font_size = max(8, min(settings.base_font_size or 16, 64)) - opts.user_stylesheet = settings.user_stylesheet or '' - opts.hide_tooltips = settings.hide_tooltips - opts.cover_preserve_aspect_ratio = v'!!settings.cover_preserve_aspect_ratio' + settings = settings or defaults + opts.base_font_size = max(8, min(settings.base_font_size, 64)) opts.bg_image_fade = settings.bg_image_fade or 'transparent' - opts.paged_wheel_scrolls_by_screen = v'!!settings.paged_wheel_scrolls_by_screen' + opts.color_scheme = settings.color_scheme + opts.columns_per_screen = settings.columns_per_screen + opts.cover_preserve_aspect_ratio = v'!!settings.cover_preserve_aspect_ratio' + opts.hide_tooltips = settings.hide_tooltips opts.is_dark_theme = v'!!settings.is_dark_theme' - opts.override_book_colors = settings.override_book_colors or 'never' + opts.margin_left = max(0, settings.margin_left) + opts.margin_right = max(0, settings.margin_right) + opts.override_book_colors = settings.override_book_colors + opts.paged_wheel_scrolls_by_screen = v'!!settings.paged_wheel_scrolls_by_screen' + opts.user_stylesheet = settings.user_stylesheet update_settings() diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 82e291028f..6b4dabbf44 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -200,29 +200,29 @@ class View: ), ) handlers = { - 'ready': self.on_iframe_ready, + 'bump_font_size': self.bump_font_size, + 'content_loaded': self.on_content_loaded, 'error': self.on_iframe_error, - 'next_spine_item': self.on_next_spine_item, - 'next_section': self.on_next_section, - 'lookup_word': self.on_lookup_word, + 'find_in_spine': self.on_find_in_spine, 'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);, + 'handle_keypress': self.on_handle_keypress, + 'handle_shortcut': self.on_handle_shortcut, + 'human_scroll': self.on_human_scroll, + 'lookup_word': self.on_lookup_word, + 'next_section': self.on_next_section, + 'next_spine_item': self.on_next_spine_item, + 'print': self.on_print, + 'ready': self.on_iframe_ready, + 'reference_item_changed': self.on_reference_item_changed, + 'report_cfi': self.on_report_cfi, + 'request_size': self.on_request_size, 'scroll_to_anchor': self.on_scroll_to_anchor, + 'selectionchange': self.on_selection_change, + 'show_chrome': self.show_chrome, + 'show_footnote': self.on_show_footnote, 'update_cfi': self.on_update_cfi, 'update_progress_frac': self.on_update_progress_frac, - 'report_cfi': self.on_report_cfi, 'update_toc_position': self.on_update_toc_position, - 'content_loaded': self.on_content_loaded, - 'show_chrome': self.show_chrome, - 'bump_font_size': self.bump_font_size, - 'find_in_spine': self.on_find_in_spine, - 'request_size': self.on_request_size, - 'show_footnote': self.on_show_footnote, - 'print': self.on_print, - 'human_scroll': self.on_human_scroll, - 'selectionchange': self.on_selection_change, - 'handle_shortcut': self.on_handle_shortcut, - 'handle_keypress': self.on_handle_keypress, - 'reference_item_changed': self.on_reference_item_changed, } entry_point = None if runtime.is_standalone_viewer else 'read_book.iframe' if runtime.is_standalone_viewer: diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj index 17d88003ac..d8a79d9679 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -9,72 +9,72 @@ from ajax import ajax_send defaults = { # Book list settings - 'view_mode': 'cover_grid', - 'sort': 'timestamp.desc', # comma separated list of items of the form: field.order + 'copy_to_library_dupes': 'add;overwrite', 'last_sort_order': {}, 'show_all_metadata': False, # show all metadata fields in the book details panel - 'copy_to_library_dupes': 'add;overwrite', + 'sort': 'timestamp.desc', # comma separated list of items of the form: field.order + 'view_mode': 'cover_grid', # Tag Browser settings - 'partition_method': 'first letter', # other choices: 'disable', 'partition' + 'and_search_terms': False, # how to add search terms to the search expression from the Tag Browser 'collapse_at': 25, # number of items at which sub-groups are created, 0 to disable 'dont_collapse': '', # comma separated list of category names - 'sort_tags_by': 'name', # other choices: popularity, rating 'hide_empty_categories': 'no', - 'and_search_terms': False, # how to add search terms to the search expression from the Tag Browser + 'partition_method': 'first letter', # other choices: 'disable', 'partition' + 'sort_tags_by': 'name', # other choices: popularity, rating # Book reader settings - 'margin_right': 20, - 'margin_left': 20, - 'margin_top': 20, - 'margin_bottom': 20, - 'read_mode': 'paged', - 'cover_preserve_aspect_ratio': True, - 'max_text_height': 0, - 'max_text_width': 0, - 'columns_per_screen': {'portrait':0, 'landscape':0}, - 'user_stylesheet': '', - 'background_image': None, - 'background_image_style': 'scaled', 'background_image_fade': 0, - 'current_color_scheme': 'system', - 'user_color_schemes': {}, - 'override_book_colors': 'never', + 'background_image_style': 'scaled', + 'background_image': None, 'base_font_size': 16, + 'book_scrollbar': False, + 'columns_per_screen': {'portrait':0, 'landscape':0}, 'controls_help_shown_count': 0, - 'header': {}, + 'cover_preserve_aspect_ratio': True, + 'current_color_scheme': 'system', 'footer': {'right': 'progress'}, - 'word_actions': v'[]', + 'header': {}, 'hide_tooltips': False, 'keyboard_shortcuts': {}, - 'book_scrollbar': False, + 'margin_bottom': 20, + 'margin_left': 20, + 'margin_right': 20, + 'margin_top': 20, + 'max_text_height': 0, + 'max_text_width': 0, + 'override_book_colors': 'never', + 'paged_margin_clicks_scroll_by_screen': True, + 'paged_wheel_scrolls_by_screen': False, + 'read_mode': 'paged', 'standalone_font_settings': {}, 'standalone_misc_settings': {}, 'standalone_recently_opened': v'[]', - 'paged_wheel_scrolls_by_screen': False, - 'paged_margin_clicks_scroll_by_screen': True, + 'user_color_schemes': {}, + 'user_stylesheet': '', + 'word_actions': v'[]', } is_local_setting = { - 'margin_right': True, - 'margin_left': True, - 'margin_top': True, + 'background_image_fade': True, + 'background_image_style': True, + 'background_image': True, + 'base_font_size': True, + 'columns_per_screen': True, + 'controls_help_shown_count': True, + 'current_color_scheme': True, 'margin_bottom': True, - 'read_mode': 'paged', + 'margin_left': True, + 'margin_right': True, + 'margin_top': True, 'max_text_height': True, 'max_text_width': True, - 'columns_per_screen': True, - 'user_stylesheet': True, - 'background_image': True, - 'background_image_style': True, - 'background_image_fade': True, - 'current_color_scheme': True, 'override_book_colors': True, - 'base_font_size': True, - 'controls_help_shown_count': True, + 'read_mode': 'paged', 'standalone_font_settings': True, 'standalone_misc_settings': True, 'standalone_recently_opened': True, + 'user_stylesheet': True, } From 0229c684f3f5e0b32c83e7b470f7ef68cf68ecfe Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Sat, 21 Dec 2019 17:39:38 -0700 Subject: [PATCH 4/9] Make smooth scroll speed configurable Includes keyboard shortcuts, will add menu options next --- src/pyj/read_book/flow_mode.pyj | 6 +++--- src/pyj/read_book/iframe.pyj | 5 +++++ src/pyj/read_book/prefs/scrolling.pyj | 11 +++++++++++ src/pyj/read_book/settings.pyj | 1 + src/pyj/read_book/shortcuts.pyj | 12 ++++++++++++ src/pyj/read_book/view.pyj | 9 +++++++++ src/pyj/session.pyj | 2 ++ 7 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index 9059e790fe..22671bc943 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -6,6 +6,7 @@ from select import word_at_point from dom import set_css from read_book.globals import current_spine_item, get_boss +from read_book.settings import opts from read_book.viewport import scroll_viewport from utils import document_height, viewport_to_document @@ -167,7 +168,6 @@ def layout(is_single_page): DIRECTION = {'Up': -1, 'Down': 1} class ScrollAnimator: DURATION = 100 # milliseconds - SCROLL_SPEED = 30 # lines/sec TODO: This will be configurable def __init__(self): self.animation_id = None @@ -187,7 +187,7 @@ class ScrollAnimator: duration = (self.end_time - self.start_time) progress = max(0, min(1, (ts - self.start_time) / duration)) # max/min to account for jitter scroll_target = self.start_offset - scroll_target += Math.trunc(self.direction * progress * duration * line_height() * self.SCROLL_SPEED) / 1000 + scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth) / 1000 window.scrollTo(0, scroll_target) @@ -196,7 +196,7 @@ class ScrollAnimator: else: self.animation_id = None amt = window.pageYOffset - self.start_offset - if abs(amt) < 3 and duration is self.DURATION: + if abs(amt) < 3 and duration is self.DURATION and !(0 <= scroll_target <= document_height() - window.innerHeight): get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up) else: report_human_scroll(amt) diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 02bb34276b..0b16865ff3 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -93,6 +93,7 @@ class IframeBoss: handlers = { 'change_color_scheme': self.change_color_scheme, 'change_font_size': self.change_font_size, + 'change_scroll_speed': self.change_scroll_speed, 'display': self.display, 'find': self.find, 'gesture_from_margin': self.gesture_from_margin, @@ -243,6 +244,10 @@ class IframeBoss: opts.base_font_size = data.base_font_size apply_font_size() + def change_scroll_speed(self, data): + if data.lines_per_sec_smooth?: + opts.lines_per_sec_smooth = data.lines_per_sec_smooth + def change_stylesheet(self, data): opts.user_stylesheet = data.sheet or '' apply_stylesheet() diff --git a/src/pyj/read_book/prefs/scrolling.pyj b/src/pyj/read_book/prefs/scrolling.pyj index 96eebe56e6..8fdd1263a6 100644 --- a/src/pyj/read_book/prefs/scrolling.pyj +++ b/src/pyj/read_book/prefs/scrolling.pyj @@ -11,6 +11,8 @@ from read_book.prefs.utils import create_button_box from session import defaults CONTAINER = unique_id('standalone-scrolling-settings') +MIN_SCROLL_SPEED = 0.5 +MAX_SCROLL_SPEED = 50 def restore_defaults(): @@ -23,6 +25,15 @@ def get_container(): return document.getElementById(CONTAINER) +def change_scroll_speed(amt): + sd = get_session_data() + lps = sd.get('lines_per_sec_smooth') + nlps = max(MIN_SCROLL_SPEED, min(lps + amt, MAX_SCROLL_SPEED)) + if nlps != lps: + sd.set('lines_per_sec_smooth', nlps) + return nlps + + def create_scrolling_panel(container, apply_func, cancel_func): container.appendChild(E.div(id=CONTAINER, style='margin: 1rem')) container = container.lastChild diff --git a/src/pyj/read_book/settings.pyj b/src/pyj/read_book/settings.pyj index e0892e75cd..4126ddc62f 100644 --- a/src/pyj/read_book/settings.pyj +++ b/src/pyj/read_book/settings.pyj @@ -17,6 +17,7 @@ def update_settings(settings): opts.cover_preserve_aspect_ratio = v'!!settings.cover_preserve_aspect_ratio' opts.hide_tooltips = settings.hide_tooltips opts.is_dark_theme = v'!!settings.is_dark_theme' + opts.lines_per_sec_smooth = settings.lines_per_sec_smooth opts.margin_left = max(0, settings.margin_left) opts.margin_right = max(0, settings.margin_right) opts.override_book_colors = settings.override_book_colors diff --git a/src/pyj/read_book/shortcuts.pyj b/src/pyj/read_book/shortcuts.pyj index 99f3cb1937..f2827fb0ae 100644 --- a/src/pyj/read_book/shortcuts.pyj +++ b/src/pyj/read_book/shortcuts.pyj @@ -270,6 +270,18 @@ def shortcuts_definition(): _('Go to a specified book location or position'), ), + 'scrollspeed_increase': desc( + "Alt+ArrowUp", + 'scroll', + _('Smooth scroll faster'), + ), + + 'scrollspeed_decrease': desc( + "Alt+ArrowDown", + 'scroll', + _('Smooth scroll slower'), + ), + } return ans diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 6b4dabbf44..2335debba1 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -23,6 +23,7 @@ from read_book.overlay import Overlay from read_book.prefs.colors import resolve_color_scheme from read_book.prefs.font_size import change_font_size_by from read_book.prefs.head_foot import render_head_foot +from read_book.prefs.scrolling import change_scroll_speed, MIN_SCROLL_SPEED as SCROLL_SPEED_STEP from read_book.resources import load_resources from read_book.search import SearchOverlay, find_in_spine from read_book.shortcuts import create_shortcut_map @@ -409,6 +410,10 @@ class View: self.iframe_wrapper.send_message('modify_selection', direction='backward', granularity='word') elif data.name is 'extend_selection_by_word': self.iframe_wrapper.send_message('modify_selection', direction='forward', granularity='word') + elif data.name is 'scrollspeed_increase': + self.update_scroll_speed(SCROLL_SPEED_STEP) + elif data.name is 'scrollspeed_decrease': + self.update_scroll_speed(-SCROLL_SPEED_STEP) def on_selection_change(self, data): self.currently_showing.selected_text = data.text @@ -703,6 +708,7 @@ class View: 'hide_tooltips': sd.get('hide_tooltips'), 'cover_preserve_aspect_ratio': sd.get('cover_preserve_aspect_ratio'), 'paged_wheel_scrolls_by_screen': sd.get('paged_wheel_scrolls_by_screen'), + 'lines_per_sec_smooth': sd.get('lines_per_sec_smooth'), } def show_name(self, name, initial_position=None): @@ -993,6 +999,9 @@ class View: def update_font_size(self): self.iframe_wrapper.send_message('change_font_size', base_font_size=get_session_data().get('base_font_size')) + def update_scroll_speed(self, amt): + self.iframe_wrapper.send_message('change_scroll_speed', lines_per_sec_smooth=change_scroll_speed(amt)) + def update_color_scheme(self): cs = self.get_color_scheme(True) self.iframe_wrapper.send_message('change_color_scheme', color_scheme=cs) diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj index d8a79d9679..c224e3463d 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -37,6 +37,7 @@ defaults = { 'header': {}, 'hide_tooltips': False, 'keyboard_shortcuts': {}, + 'lines_per_sec_smooth': 30, 'margin_bottom': 20, 'margin_left': 20, 'margin_right': 20, @@ -63,6 +64,7 @@ is_local_setting = { 'columns_per_screen': True, 'controls_help_shown_count': True, 'current_color_scheme': True, + 'lines_per_sec_smooth': True, 'margin_bottom': True, 'margin_left': True, 'margin_right': True, From e028bbb3f65fd240c0fc8344d60b2388fb3ed617 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Tue, 24 Dec 2019 00:29:01 -0700 Subject: [PATCH 5/9] Add auto-scroll with keyboard shortcut --- src/pyj/read_book/flow_mode.pyj | 67 +++++++++++++++++++++++---- src/pyj/read_book/iframe.pyj | 7 +-- src/pyj/read_book/prefs/scrolling.pyj | 2 +- src/pyj/read_book/settings.pyj | 1 + src/pyj/read_book/shortcuts.pyj | 10 +++- src/pyj/read_book/view.pyj | 5 +- src/pyj/session.pyj | 2 + 7 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index 22671bc943..5393728c24 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -63,8 +63,11 @@ last_change_spine_item_request = {} def _check_for_scroll_end(func, obj, args, report): before = window.pageYOffset func.apply(obj, args) + + now = performance.now() + scroll_animator.sync(now) + if window.pageYOffset is before: - now = Date.now() csi = current_spine_item() if last_change_spine_item_request.name is csi.name and now - last_change_spine_item_request.at < 2000: return False @@ -128,10 +131,10 @@ def scroll_by_page(direction): def handle_shortcut(sc_name, evt): if sc_name is 'down': - scroll_animator.start(DIRECTION.Down) + scroll_animator.start(DIRECTION.Down, False) return True if sc_name is 'up': - scroll_animator.start(DIRECTION.Up) + scroll_animator.start(DIRECTION.Up, False) return True if sc_name is 'start_of_file': goto_boundary(-1) @@ -157,13 +160,29 @@ def handle_shortcut(sc_name, evt): if sc_name is 'pagedown': scroll_by_page(1) return True + if sc_name is 'toggle_autoscroll': + if scroll_animator.auto and scroll_animator.is_running(): + cancel_scroll() + else: + scroll_animator.start(DIRECTION.Down, True) + return True + + if sc_name.startsWith('scrollspeed_'): + scroll_animator.sync() + return False def layout(is_single_page): + cancel_scroll() set_css(document.body, margin='0', border_width='0', padding='0') line_height.doc_style = window.getComputedStyle(document.body) +def cancel_scroll(): + scroll_animator.stop() + +def is_scroll_end(pos): + return !(0 <= pos <= document_height() - window.innerHeight) DIRECTION = {'Up': -1, 'Down': 1} class ScrollAnimator: @@ -171,17 +190,23 @@ class ScrollAnimator: def __init__(self): self.animation_id = None + self.auto = False - def start(self, direction): + def is_running(self): + return self.animation_id != None + + def start(self, direction, auto): now = performance.now() self.end_time = now + self.DURATION - if self.animation_id is None or direction != self.direction: + if !self.is_running() or direction != self.direction or auto != self.auto: + self.paused = self.direction if self.auto and not auto else False self.stop() + self.auto = auto self.direction = direction self.start_time = now self.start_offset = window.pageYOffset - self.animation_id = window.requestAnimationFrame(self.smooth_scroll) + self.animation_id = window.requestAnimationFrame(self.auto_scroll if auto else self.smooth_scroll) def smooth_scroll(self, ts): duration = (self.end_time - self.start_time) @@ -194,14 +219,40 @@ class ScrollAnimator: if progress < 1: self.animation_id = window.requestAnimationFrame(self.smooth_scroll) else: - self.animation_id = None amt = window.pageYOffset - self.start_offset - if abs(amt) < 3 and duration is self.DURATION and !(0 <= scroll_target <= document_height() - window.innerHeight): + if abs(amt) < 3 and duration is self.DURATION and is_scroll_end(scroll_target): get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up) + elif self.paused: + self.start(self.paused, True) else: + self.animation_id = None report_human_scroll(amt) + def auto_scroll(self, ts): + elapsed = max(0, ts - self.start_time) # max to account for jitter + scroll_target = self.start_offset + scroll_target += Math.trunc(self.direction * elapsed * line_height() * opts.lines_per_sec_auto) / 1000 + + window.scrollTo(0, scroll_target) + scroll_finished = is_scroll_end(scroll_target) + + # report every second + if elapsed >= 1000: + self.sync(ts) + + if scroll_finished: + self.stop() + else: + self.animation_id = window.requestAnimationFrame(self.auto_scroll) + + def sync(self, ts): + if self.auto: + report_human_scroll(window.pageYOffset - self.start_offset) + self.start_time = ts or performance.now() + self.start_offset = window.pageYOffset + def stop(self): + self.auto = False if self.animation_id is not None: window.cancelAnimationFrame(self.animation_id) self.animation_id = None diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 0b16865ff3..2969963d5a 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -12,7 +12,7 @@ from read_book.extract import get_elements from read_book.flow_mode import ( anchor_funcs as flow_anchor_funcs, flow_onwheel, flow_to_scroll_fraction, handle_gesture as flow_handle_gesture, handle_shortcut as flow_handle_shortcut, - layout as flow_layout, scroll_by_page as flow_scroll_by_page + layout as flow_layout, scroll_by_page as flow_scroll_by_page, cancel_scroll ) from read_book.footnotes import is_footnote_link from read_book.globals import ( @@ -108,6 +108,7 @@ class IframeBoss: 'set_reference_mode': self.set_reference_mode, 'wheel_from_margin': self.wheel_from_margin, 'window_size': self.received_window_size, + 'overlay_shown': cancel_scroll, } self.comm = IframeClient(handlers) self.last_window_ypos = 0 @@ -245,8 +246,8 @@ class IframeBoss: apply_font_size() def change_scroll_speed(self, data): - if data.lines_per_sec_smooth?: - opts.lines_per_sec_smooth = data.lines_per_sec_smooth + if data.lines_per_sec_auto?: + opts.lines_per_sec_auto = data.lines_per_sec_auto def change_stylesheet(self, data): opts.user_stylesheet = data.sheet or '' diff --git a/src/pyj/read_book/prefs/scrolling.pyj b/src/pyj/read_book/prefs/scrolling.pyj index 8fdd1263a6..7066b9cb71 100644 --- a/src/pyj/read_book/prefs/scrolling.pyj +++ b/src/pyj/read_book/prefs/scrolling.pyj @@ -12,7 +12,7 @@ from session import defaults CONTAINER = unique_id('standalone-scrolling-settings') MIN_SCROLL_SPEED = 0.5 -MAX_SCROLL_SPEED = 50 +MAX_SCROLL_SPEED = 5 def restore_defaults(): diff --git a/src/pyj/read_book/settings.pyj b/src/pyj/read_book/settings.pyj index 4126ddc62f..adfb516d6d 100644 --- a/src/pyj/read_book/settings.pyj +++ b/src/pyj/read_book/settings.pyj @@ -17,6 +17,7 @@ def update_settings(settings): opts.cover_preserve_aspect_ratio = v'!!settings.cover_preserve_aspect_ratio' opts.hide_tooltips = settings.hide_tooltips opts.is_dark_theme = v'!!settings.is_dark_theme' + opts.lines_per_sec_auto = settings.lines_per_sec_auto opts.lines_per_sec_smooth = settings.lines_per_sec_smooth opts.margin_left = max(0, settings.margin_left) opts.margin_right = max(0, settings.margin_right) diff --git a/src/pyj/read_book/shortcuts.pyj b/src/pyj/read_book/shortcuts.pyj index f2827fb0ae..0100fe813d 100644 --- a/src/pyj/read_book/shortcuts.pyj +++ b/src/pyj/read_book/shortcuts.pyj @@ -270,16 +270,22 @@ def shortcuts_definition(): _('Go to a specified book location or position'), ), + 'toggle_autoscroll': desc( + "Ctrl+ ", + 'scroll', + _('Toggle auto-scroll'), + ), + 'scrollspeed_increase': desc( "Alt+ArrowUp", 'scroll', - _('Smooth scroll faster'), + _('Auto scroll faster'), ), 'scrollspeed_decrease': desc( "Alt+ArrowDown", 'scroll', - _('Smooth scroll slower'), + _('Auto scroll slower'), ), } diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 2335debba1..715855fecb 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -339,6 +339,8 @@ class View: def overlay_visibility_changed(self, visible): if self.iframe_wrapper.send_message: + if visible: + self.iframe_wrapper.send_message('overlay_shown') self.iframe_wrapper.send_message('set_forward_keypresses', forward=v'!!visible') if ui_operations.overlay_visibility_changed: ui_operations.overlay_visibility_changed(visible) @@ -708,6 +710,7 @@ class View: 'hide_tooltips': sd.get('hide_tooltips'), 'cover_preserve_aspect_ratio': sd.get('cover_preserve_aspect_ratio'), 'paged_wheel_scrolls_by_screen': sd.get('paged_wheel_scrolls_by_screen'), + 'lines_per_sec_auto': sd.get('lines_per_sec_auto'), 'lines_per_sec_smooth': sd.get('lines_per_sec_smooth'), } @@ -1000,7 +1003,7 @@ class View: self.iframe_wrapper.send_message('change_font_size', base_font_size=get_session_data().get('base_font_size')) def update_scroll_speed(self, amt): - self.iframe_wrapper.send_message('change_scroll_speed', lines_per_sec_smooth=change_scroll_speed(amt)) + self.iframe_wrapper.send_message('change_scroll_speed', lines_per_sec_auto=change_scroll_speed(amt)) def update_color_scheme(self): cs = self.get_color_scheme(True) diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj index c224e3463d..1a17109508 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -37,6 +37,7 @@ defaults = { 'header': {}, 'hide_tooltips': False, 'keyboard_shortcuts': {}, + 'lines_per_sec_auto': 1, 'lines_per_sec_smooth': 30, 'margin_bottom': 20, 'margin_left': 20, @@ -64,6 +65,7 @@ is_local_setting = { 'columns_per_screen': True, 'controls_help_shown_count': True, 'current_color_scheme': True, + 'lines_per_sec_auto': True, 'lines_per_sec_smooth': True, 'margin_bottom': True, 'margin_left': True, From 1ecf881cb42005cc058a2faee9a5f00e6b0a1ecd Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Sun, 22 Dec 2019 23:38:16 -0700 Subject: [PATCH 6/9] Add settings elements to change scroll speeds --- src/pyj/read_book/prefs/scrolling.pyj | 60 +++++++++++++++++++++------ src/pyj/read_book/view.pyj | 2 +- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/pyj/read_book/prefs/scrolling.pyj b/src/pyj/read_book/prefs/scrolling.pyj index 7066b9cb71..3d4d5e35f3 100644 --- a/src/pyj/read_book/prefs/scrolling.pyj +++ b/src/pyj/read_book/prefs/scrolling.pyj @@ -11,14 +11,22 @@ from read_book.prefs.utils import create_button_box from session import defaults CONTAINER = unique_id('standalone-scrolling-settings') -MIN_SCROLL_SPEED = 0.5 -MAX_SCROLL_SPEED = 5 +# Scroll speeds in lines/sec +MIN_SCROLL_SPEED_AUTO = 0.25 +MAX_SCROLL_SPEED_AUTO = 5 + +MIN_SCROLL_SPEED_SMOOTH = 10 +MAX_SCROLL_SPEED_SMOOTH = 50 def restore_defaults(): container = get_container() for control in container.querySelectorAll('input[name]'): - control.checked = defaults[control.getAttribute('name')] + val = defaults[control.getAttribute('name')] + if control.type is 'checkbox': + control.checked = val + else: + control.valueAsNumber = val def get_container(): @@ -27,10 +35,10 @@ def get_container(): def change_scroll_speed(amt): sd = get_session_data() - lps = sd.get('lines_per_sec_smooth') - nlps = max(MIN_SCROLL_SPEED, min(lps + amt, MAX_SCROLL_SPEED)) + lps = sd.get('lines_per_sec_auto') + nlps = max(MIN_SCROLL_SPEED_AUTO, min(lps + amt, MAX_SCROLL_SPEED_AUTO)) if nlps != lps: - sd.set('lines_per_sec_smooth', nlps) + sd.set('lines_per_sec_auto', nlps) return nlps @@ -45,16 +53,42 @@ def create_scrolling_panel(container, apply_func, cancel_func): ans.checked = True return E.div(style='margin-top:1ex', E.label(ans, '\xa0' + text)) - container.appendChild(E.div(style='margin-top:1ex', _( - 'Control how mouse based scrolling works in paged mode'))) + def spinner(name, text, **kwargs): + ans = E.input(type='number', name=name, id=name) + for key, val in Object.entries(kwargs): + ans[key] = val + ans.valueAsNumber = sd.get(name) or defaults[name] + return E.label("for"=name, text), ans + + container.appendChild(E.div(style='margin-top:1ex', _('Control how mouse based scrolling works in paged mode'))) container.appendChild(cb( 'paged_wheel_scrolls_by_screen', _('Mouse wheel scrolls by screen fulls instead of pages'))) container.appendChild(cb( 'paged_margin_clicks_scroll_by_screen', _('Clicking on the margins scrolls by screen fulls instead of pages'))) - container.appendChild(E.div(style='margin-top:1ex; border-top: solid 1px', '\xa0')) - container.appendChild(cb( - 'book_scrollbar', _('Show a scrollbar'))) + container.appendChild(E.hr()) + container.appendChild(E.div(style='margin-top:1ex', _('Control how smooth scrolling works in flow mode'))) + container.appendChild( + E.div(style='display:grid;margin-top:1ex;align-items:center;grid-template-columns:auto auto;grid-gap:1ex;justify-content:flex-start;', + *spinner( + 'lines_per_sec_smooth', + _('Smooth scrolling speed in lines/sec'), + step=5, + min=MIN_SCROLL_SPEED_SMOOTH, + max=MAX_SCROLL_SPEED_SMOOTH + ), + *spinner( + 'lines_per_sec_auto', + _('Auto scrolling speed in lines/sec'), + step=MIN_SCROLL_SPEED_AUTO, + min=MIN_SCROLL_SPEED_AUTO, + max=MAX_SCROLL_SPEED_AUTO + ) + ) + ) + + container.appendChild(E.hr()) + container.appendChild(cb('book_scrollbar', _('Show a scrollbar'))) container.appendChild(create_button_box(restore_defaults, apply_func, cancel_func)) @@ -68,8 +102,8 @@ def commit_scrolling(onchange): changed = False for control in container.querySelectorAll('input[name]'): name = control.getAttribute('name') - val = control.checked - if val is not sd.get(name): + val = control.checked if control.type is 'checkbox' else control.valueAsNumber + if val is not sd.get(name) and control.validity.valid: sd.set(name, val) changed = True if changed: diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 715855fecb..7ed54af7e6 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -23,7 +23,7 @@ from read_book.overlay import Overlay from read_book.prefs.colors import resolve_color_scheme from read_book.prefs.font_size import change_font_size_by from read_book.prefs.head_foot import render_head_foot -from read_book.prefs.scrolling import change_scroll_speed, MIN_SCROLL_SPEED as SCROLL_SPEED_STEP +from read_book.prefs.scrolling import change_scroll_speed, MIN_SCROLL_SPEED_AUTO as SCROLL_SPEED_STEP from read_book.resources import load_resources from read_book.search import SearchOverlay, find_in_spine from read_book.shortcuts import create_shortcut_map From b70ec308693ef70ce40c6d0ae5ae815eedbe74a2 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Wed, 25 Dec 2019 11:04:59 -0700 Subject: [PATCH 7/9] More robust line_height computation and settings init --- src/pyj/read_book/flow_mode.pyj | 8 +++++++- src/pyj/read_book/settings.pyj | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index 5393728c24..b50a7f7dfa 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -12,7 +12,13 @@ from utils import document_height, viewport_to_document def line_height(): - return parseFloat(line_height.doc_style.lineHeight) + try: + # will fail if line-height = "normal" + lh = parseFloat(line_height.doc_style.lineHeight) + except: + lh = 1.2 * parseFloat(line_height.doc_style.fontSize) + + return lh def flow_to_scroll_fraction(frac, on_initial_load): scroll_viewport.scroll_to(0, document_height() * frac) diff --git a/src/pyj/read_book/settings.pyj b/src/pyj/read_book/settings.pyj index adfb516d6d..0c5defb9dd 100644 --- a/src/pyj/read_book/settings.pyj +++ b/src/pyj/read_book/settings.pyj @@ -9,7 +9,7 @@ from session import defaults opts = {} def update_settings(settings): - settings = settings or defaults + settings = Object.assign({}, defaults, settings) opts.base_font_size = max(8, min(settings.base_font_size, 64)) opts.bg_image_fade = settings.bg_image_fade or 'transparent' opts.color_scheme = settings.color_scheme From 568d32ec16fa5e104cf456341da383412f2c5e0c Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Thu, 26 Dec 2019 11:40:22 -0700 Subject: [PATCH 8/9] Fix goto start/end of file in flow mode --- src/pyj/read_book/flow_mode.pyj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index b50a7f7dfa..2015f4feab 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -124,8 +124,8 @@ def flow_onwheel(evt): scroll_by(dy) @check_for_scroll_end -def goto_boundary(y): - scroll_viewport.scroll_to(window.pageXOffset, 0) +def goto_boundary(dir): + scroll_viewport.scroll_to(window.pageXOffset, 0 if dir is DIRECTION.Up else document_height()) get_boss().report_human_scroll() @@ -143,10 +143,10 @@ def handle_shortcut(sc_name, evt): scroll_animator.start(DIRECTION.Up, False) return True if sc_name is 'start_of_file': - goto_boundary(-1) + goto_boundary(DIRECTION.Up) return True if sc_name is 'end_of_file': - goto_boundary(1) + goto_boundary(DIRECTION.Down) return True if sc_name is 'left': window.scrollBy(-15, 0) From 0a9e52d212d433c542d61956573a5841ac7485b9 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Thu, 26 Dec 2019 12:28:42 -0700 Subject: [PATCH 9/9] Additional scroll options... Option for whether or not to load next file after reaching the start/end when using key based scrolling Configurable delay before loading the next file after reaching the end with auto-scroll --- src/pyj/read_book/flow_mode.pyj | 76 ++++++++++++++++++++------- src/pyj/read_book/iframe.pyj | 2 +- src/pyj/read_book/prefs/scrolling.pyj | 18 ++++++- src/pyj/read_book/settings.pyj | 2 + src/pyj/read_book/view.pyj | 2 + src/pyj/session.pyj | 4 ++ 6 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index 2015f4feab..24c057b16a 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -51,9 +51,9 @@ def add_small_scroll(amt): def report_human_scroll(amt): - if amt > 0: + if abs(amt) > 0: h = scroll_viewport.height() - is_large_scroll = (amt / h) >= 0.5 + is_large_scroll = (abs(amt) / h) >= 0.5 if is_large_scroll: clear_small_scrolls() get_boss().report_human_scroll(amt / document_height()) @@ -180,10 +180,19 @@ def handle_shortcut(sc_name, evt): def layout(is_single_page): - cancel_scroll() + scroll_animator.wait = False + scroll_animator.sync() set_css(document.body, margin='0', border_width='0', padding='0') line_height.doc_style = window.getComputedStyle(document.body) +# Pause auto-scroll while minimized +document.addEventListener("visibilitychange", def(): + if (document.visibilityState is 'visible'): + scroll_animator.sync() + else: + scroll_animator.pause() +) + def cancel_scroll(): scroll_animator.stop() @@ -202,16 +211,22 @@ class ScrollAnimator: return self.animation_id != None def start(self, direction, auto): + if self.wait: + return + now = performance.now() self.end_time = now + self.DURATION + clearTimeout(self.auto_timer) if !self.is_running() or direction != self.direction or auto != self.auto: - self.paused = self.direction if self.auto and not auto else False + if self.auto and not auto: + self.pause() self.stop() self.auto = auto self.direction = direction self.start_time = now self.start_offset = window.pageYOffset + self.csi_idx = current_spine_item().index self.animation_id = window.requestAnimationFrame(self.auto_scroll if auto else self.smooth_scroll) def smooth_scroll(self, ts): @@ -221,18 +236,22 @@ class ScrollAnimator: scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth) / 1000 window.scrollTo(0, scroll_target) + amt = window.pageYOffset - self.start_offset - if progress < 1: + if is_scroll_end(scroll_target) and (not opts.scroll_stop_boundaries or (abs(amt) < 3 and duration is self.DURATION)): + # "Turn the page" if stop at boundaries option is false or + # this is a new scroll action and we were already at the end + self.animation_id = None + self.wait = True + report_human_scroll(amt) + get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up) + elif progress < 1: self.animation_id = window.requestAnimationFrame(self.smooth_scroll) + elif self.paused: + self.resume() else: - amt = window.pageYOffset - self.start_offset - if abs(amt) < 3 and duration is self.DURATION and is_scroll_end(scroll_target): - get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up) - elif self.paused: - self.start(self.paused, True) - else: - self.animation_id = None - report_human_scroll(amt) + self.animation_id = None + report_human_scroll(amt) def auto_scroll(self, ts): elapsed = max(0, ts - self.start_time) # max to account for jitter @@ -247,24 +266,45 @@ class ScrollAnimator: self.sync(ts) if scroll_finished: - self.stop() + self.pause() + if opts.scroll_auto_boundary_delay: + self.auto_timer = setTimeout(def(): get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up);, opts.scroll_auto_boundary_delay * 1000) else: self.animation_id = window.requestAnimationFrame(self.auto_scroll) + def report(self): + amt = window.pageYOffset - self.start_offset + if abs(amt) > 0 and self.csi_idx is current_spine_item().index: + report_human_scroll(amt) + def sync(self, ts): if self.auto: - report_human_scroll(window.pageYOffset - self.start_offset) + self.report() + self.csi_idx = current_spine_item().index self.start_time = ts or performance.now() self.start_offset = window.pageYOffset + else: + self.resume() def stop(self): self.auto = False if self.animation_id is not None: window.cancelAnimationFrame(self.animation_id) self.animation_id = None - amt = window.pageYOffset - self.start_offset - if amt > 0: - report_human_scroll(amt) + self.report() + + def pause(self): + if self.auto: + self.paused = self.direction + self.stop() + else: + self.paused = False + + # Resume auto-scroll + def resume(self): + if self.paused: + self.start(self.paused, True) + self.paused = False scroll_animator = ScrollAnimator() diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 2969963d5a..542e4ada75 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -270,7 +270,6 @@ class IframeBoss: self.last_window_width, self.last_window_height = scroll_viewport.width(), scroll_viewport.height() apply_settings() fix_fullscreen_svg_images() - self.do_layout(self.is_titlepage) if self.mathjax: return apply_mathjax(self.mathjax, self.book.manifest.link_uid, self.content_loaded_stage2) # window.setTimeout(self.content_loaded_stage2, 1000) @@ -307,6 +306,7 @@ class IframeBoss: if si: self.length_before += files[si]?.length or 0 self.onscroll() + self.do_layout(self.is_titlepage) self.send_message('content_loaded', progress_frac=self.calculate_progress_frac(), file_progress_frac=progress_frac()) self.last_cfi = None window.setTimeout(self.update_cfi, 0) diff --git a/src/pyj/read_book/prefs/scrolling.pyj b/src/pyj/read_book/prefs/scrolling.pyj index 3d4d5e35f3..fcadb4deab 100644 --- a/src/pyj/read_book/prefs/scrolling.pyj +++ b/src/pyj/read_book/prefs/scrolling.pyj @@ -16,6 +16,9 @@ CONTAINER = unique_id('standalone-scrolling-settings') MIN_SCROLL_SPEED_AUTO = 0.25 MAX_SCROLL_SPEED_AUTO = 5 +MIN_SCROLL_AUTO_DELAY = 0 +MAX_SCROLL_AUTO_DELAY = 10 + MIN_SCROLL_SPEED_SMOOTH = 10 MAX_SCROLL_SPEED_SMOOTH = 50 @@ -57,7 +60,7 @@ def create_scrolling_panel(container, apply_func, cancel_func): ans = E.input(type='number', name=name, id=name) for key, val in Object.entries(kwargs): ans[key] = val - ans.valueAsNumber = sd.get(name) or defaults[name] + ans.valueAsNumber = sd.get(name, defaults[name]) return E.label("for"=name, text), ans container.appendChild(E.div(style='margin-top:1ex', _('Control how mouse based scrolling works in paged mode'))) @@ -68,8 +71,12 @@ def create_scrolling_panel(container, apply_func, cancel_func): container.appendChild(E.hr()) container.appendChild(E.div(style='margin-top:1ex', _('Control how smooth scrolling works in flow mode'))) + container.appendChild(cb( + 'scroll_stop_boundaries', + _('Stop at file boundaries when continuous scrolling while holding down the scroll key') + )) container.appendChild( - E.div(style='display:grid;margin-top:1ex;align-items:center;grid-template-columns:auto auto;grid-gap:1ex;justify-content:flex-start;', + E.div(style='display:grid;margin-top:1ex;align-items:center;grid-template-columns:25em min-content;grid-gap:1ex', *spinner( 'lines_per_sec_smooth', _('Smooth scrolling speed in lines/sec'), @@ -83,6 +90,13 @@ def create_scrolling_panel(container, apply_func, cancel_func): step=MIN_SCROLL_SPEED_AUTO, min=MIN_SCROLL_SPEED_AUTO, max=MAX_SCROLL_SPEED_AUTO + ), + *spinner( + 'scroll_auto_boundary_delay', + _('Seconds to wait before loading the next file after auto-scroll reaches the end; 0 to disable'), + step=0.25, + min=MIN_SCROLL_AUTO_DELAY, + max=MAX_SCROLL_AUTO_DELAY ) ) ) diff --git a/src/pyj/read_book/settings.pyj b/src/pyj/read_book/settings.pyj index 0c5defb9dd..c3a1905153 100644 --- a/src/pyj/read_book/settings.pyj +++ b/src/pyj/read_book/settings.pyj @@ -23,6 +23,8 @@ def update_settings(settings): opts.margin_right = max(0, settings.margin_right) opts.override_book_colors = settings.override_book_colors opts.paged_wheel_scrolls_by_screen = v'!!settings.paged_wheel_scrolls_by_screen' + opts.scroll_auto_boundary_delay = settings.scroll_auto_boundary_delay + opts.scroll_stop_boundaries = v'!!settings.scroll_stop_boundaries' opts.user_stylesheet = settings.user_stylesheet update_settings() diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 7ed54af7e6..97f7324bdf 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -712,6 +712,8 @@ class View: 'paged_wheel_scrolls_by_screen': sd.get('paged_wheel_scrolls_by_screen'), 'lines_per_sec_auto': sd.get('lines_per_sec_auto'), 'lines_per_sec_smooth': sd.get('lines_per_sec_smooth'), + 'scroll_auto_boundary_delay': sd.get('scroll_auto_boundary_delay'), + 'scroll_stop_boundaries': sd.get('scroll_stop_boundaries'), } def show_name(self, name, initial_position=None): diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj index 1a17109508..760d44c506 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -49,6 +49,8 @@ defaults = { 'paged_margin_clicks_scroll_by_screen': True, 'paged_wheel_scrolls_by_screen': False, 'read_mode': 'paged', + 'scroll_auto_boundary_delay': 5, + 'scroll_stop_boundaries': False, 'standalone_font_settings': {}, 'standalone_misc_settings': {}, 'standalone_recently_opened': v'[]', @@ -75,6 +77,8 @@ is_local_setting = { 'max_text_width': True, 'override_book_colors': True, 'read_mode': 'paged', + 'scroll_auto_boundary_delay': True, + 'scroll_stop_boundaries': True, 'standalone_font_settings': True, 'standalone_misc_settings': True, 'standalone_recently_opened': True,