diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index 05e081407e..8915ff934e 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -6,10 +6,26 @@ 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 +def line_height(): + if not line_height.ans: + ds = window.getComputedStyle(document.body) + try: + # will fail if line-height = "normal" + lh = float(ds.lineHeight) + except: + try: + lh = 1.2 * float(ds.fontSize) + except: + lh = 15 + line_height.ans = max(5, lh) + return line_height.ans + + def flow_to_scroll_fraction(frac, on_initial_load): scroll_viewport.scroll_to(0, document_height() * frac) @@ -41,15 +57,15 @@ def add_small_scroll(amt): def report_human_scroll(amt): + h = scroll_viewport.height() + is_large_scroll = (abs(amt) / h) >= 0.5 if amt > 0: - h = scroll_viewport.height() - is_large_scroll = (amt / h) >= 0.5 if is_large_scroll: clear_small_scrolls() get_boss().report_human_scroll(amt / document_height()) else: add_small_scroll(amt) - else: + elif amt is 0 or is_large_scroll: clear_small_scrolls() @@ -59,8 +75,11 @@ last_change_spine_item_request = {} def _check_for_scroll_end(func, obj, args, report): before = window.pageYOffset func.apply(obj, args) + + now = window.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 @@ -95,7 +114,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,24 +129,9 @@ 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) +def goto_boundary(dir): + scroll_viewport.scroll_to(window.pageXOffset, 0 if dir is DIRECTION.Up else document_height()) get_boss().report_human_scroll() @@ -139,16 +143,16 @@ 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, False) return True if sc_name is 'up': - smooth_y_scroll(True) + 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) @@ -168,12 +172,154 @@ 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): + line_height.ans = None + scroll_animator.wait = False + scroll_animator.sync() set_css(document.body, margin='0', border_width='0', padding='0') +# 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() + + +def is_scroll_end(pos): + return not (0 <= pos <= document_height() - window.innerHeight) + +DIRECTION = {'Up': -1, 'Down': 1} + + +class ScrollAnimator: + DURATION = 100 # milliseconds + + def __init__(self): + self.animation_id = None + self.auto = False + + def is_running(self): + return self.animation_id is not None + + def start(self, direction, auto): + if self.wait: + return + + now = window.performance.now() + self.end_time = now + self.DURATION + clearTimeout(self.auto_timer) + + if not self.is_running() or direction is not self.direction or auto is not self.auto: + 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): + 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() * opts.lines_per_sec_smooth) / 1000 + + window.scrollTo(0, scroll_target) + amt = window.pageYOffset - self.start_offset + + 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: + 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.pause() + if opts.scroll_auto_boundary_delay >= 0: + 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: + self.report() + self.csi_idx = current_spine_item().index + self.start_time = ts or window.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 + 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() + class FlickAnimator: diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index ea8d5ee03a..56fabe4e95 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 as flow_cancel_auto_scroll ) from read_book.footnotes import is_footnote_link from read_book.globals import ( @@ -91,27 +91,33 @@ 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, + 'change_scroll_speed': self.change_scroll_speed, + '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, + 'overlay_shown': self.on_overlay_shown, } self.comm = IframeClient(handlers) self.last_window_ypos = 0 self.length_before = None + def on_overlay_shown(self): + if current_layout_mode() is 'flow': + flow_cancel_auto_scroll() + def modify_selection(self, data): sel = window.getSelection() sel.modify('extend', data.direction, data.granularity) @@ -243,6 +249,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_auto?: + opts.lines_per_sec_auto = data.lines_per_sec_auto + 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..e296911f7d 100644 --- a/src/pyj/read_book/prefs/scrolling.pyj +++ b/src/pyj/read_book/prefs/scrolling.pyj @@ -12,17 +12,39 @@ from session import defaults CONTAINER = unique_id('standalone-scrolling-settings') +# Scroll speeds in lines/sec +MIN_SCROLL_SPEED_AUTO = 0.25 +MAX_SCROLL_SPEED_AUTO = 5 + +MIN_SCROLL_AUTO_DELAY = -1 +MAX_SCROLL_AUTO_DELAY = 10 + +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(): return document.getElementById(CONTAINER) +def change_scroll_speed(amt): + sd = get_session_data() + 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_auto', nlps) + return nlps + + def create_scrolling_panel(container, apply_func, cancel_func): container.appendChild(E.div(id=CONTAINER, style='margin: 1rem')) container = container.lastChild @@ -34,16 +56,54 @@ 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, 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(E.hr()) + container.appendChild(E.div(style='margin-top:1ex', _('Control how smooth scrolling works in flow mode'))) container.appendChild(cb( - 'book_scrollbar', _('Show a scrollbar'))) + 'scroll_stop_boundaries', + _('Stop at internal file boundaries when smooth scrolling by holding down the scroll key') + )) + container.appendChild( + 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'), + 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 + ), + *spinner( + 'scroll_auto_boundary_delay', + _('Seconds to pause before auto-scrolling past internal file boundaries'), + title=_('Use negative values to not auto-scroll past internal file boundaries'), + step=0.25, + min=MIN_SCROLL_AUTO_DELAY, + max=MAX_SCROLL_AUTO_DELAY + ) + ) + ) + + container.appendChild(E.hr()) + container.appendChild(cb('book_scrollbar', _('Show a scrollbar'))) container.appendChild(create_button_box(restore_defaults, apply_func, cancel_func)) @@ -57,8 +117,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/settings.pyj b/src/pyj/read_book/settings.pyj index cf922e4fe3..c3a1905153 100644 --- a/src/pyj/read_book/settings.pyj +++ b/src/pyj/read_book/settings.pyj @@ -4,23 +4,28 @@ 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 = 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.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.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) + 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/shortcuts.pyj b/src/pyj/read_book/shortcuts.pyj index 160c72e086..b214db9e4d 100644 --- a/src/pyj/read_book/shortcuts.pyj +++ b/src/pyj/read_book/shortcuts.pyj @@ -270,6 +270,24 @@ 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', + _('Auto scroll faster'), + ), + + 'scrollspeed_decrease': desc( + "Alt+ArrowDown", + 'scroll', + _('Auto scroll slower'), + ), + } return ans diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 6f9eee8971..e003c4b59e 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_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 @@ -133,7 +134,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 @@ -200,29 +201,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: @@ -338,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) @@ -409,6 +412,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 +710,10 @@ 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'), + '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): @@ -993,6 +1004,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_auto=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 17d88003ac..de899f9858 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -9,72 +9,80 @@ 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, + 'lines_per_sec_auto': 1, + 'lines_per_sec_smooth': 20, + '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', + 'scroll_auto_boundary_delay': 5, + 'scroll_stop_boundaries': False, '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, + 'lines_per_sec_auto': True, + 'lines_per_sec_smooth': 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', + 'scroll_auto_boundary_delay': True, + 'scroll_stop_boundaries': True, 'standalone_font_settings': True, 'standalone_misc_settings': True, 'standalone_recently_opened': True, + 'user_stylesheet': True, }