# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal from __python__ import bound_methods, hash_literals from elementmaker import E import read_book.iframe # noqa from ajax import ajax_send from book_list.globals import get_session_data from book_list.home import update_book_in_recently_read_by_user_on_home_page from book_list.theme import cached_color_to_rgba, get_color, set_ui_colors from book_list.ui import query_as_href from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id from gettext import gettext as _ from iframe_comm import create_wrapped_iframe from modals import error_dialog, warning_dialog from read_book.annotations import AnnotationsManager from read_book.bookmarks import create_new_bookmark from read_book.content_popup import ContentPopupOverlay from read_book.globals import ( current_book, current_spine_item, is_dark_theme, rtl_page_progression, runtime, set_current_spine_item, ui_operations ) from read_book.goto import get_next_section from read_book.highlights import get_current_link_prefix, link_to_epubcfi from read_book.hints import Hints from read_book.open_book import add_book_to_recently_viewed 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, restore_default_font_size from read_book.prefs.fonts import current_zoom_step_size from read_book.prefs.head_foot import render_head_foot from read_book.prefs.scrolling import ( MIN_SCROLL_SPEED_AUTO as SCROLL_SPEED_STEP, change_scroll_speed ) from read_book.read_aloud import ReadAloud from read_book.resources import load_resources from read_book.scrollbar import BookScrollbar from read_book.search import SearchOverlay from read_book.selection_bar import SelectionBar from read_book.shortcuts import create_shortcut_map 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 from utils import ( default_context_menu_should_be_allowed, html_escape, is_ios, parse_url_params, safe_set_inner_html, username_key ) from viewer.constants import READER_BACKGROUND_URL add_extra_css(def(): sel = '.book-side-margin' ans = build_rule(sel, cursor='pointer', text_align='center', height='100vh', user_select='none', display='flex', align_items='center', justify_content='space-between', flex_direction='column') ans += build_rule(sel + ' > .arrow', display='none') ans += build_rule(sel + ' > *', max_width='100%', overflow='hidden') ans += build_rule(sel + ':hover > .not-arrow', display='none') ans += build_rule(sel + ':active > .not-arrow', display='none') ans += build_rule(sel + ':hover > .arrow', display='block') ans += build_rule(sel + ':active > .arrow', display='block', transform='scale(2)') return ans ) # Simple Overlays {{{ def show_controls_help(): container = document.getElementById('controls-help-overlay') container.style.display = 'block' container.style.backgroundColor = get_color('window-background') if not show_controls_help.listener_added: show_controls_help.listener_added = True container.addEventListener('click', def(): document.getElementById('controls-help-overlay').style.display = 'none' ui_operations.focus_iframe() ) container.addEventListener('contextmenu', def(evt): evt.preventDefault(), evt.stopPropagation() document.getElementById('controls-help-overlay').style.display = 'none' ui_operations.focus_iframe() , {'passive': False}) container.addEventListener('keydown', def(event): event.preventDefault(), event.stopPropagation() document.getElementById('controls-help-overlay').style.display = 'none' ui_operations.focus_iframe() , {'passive': False}) def focus(): if container.style.display is 'block': container.querySelector('input').focus() window.setTimeout(focus, 10) if runtime.is_standalone_viewer: clear(container) container.appendChild(E.div( style='margin: 1rem', E.div(style='margin-top: 1rem'), E.div(style='margin-top: 1rem'), E.div(style='margin-top: 1rem'), E.div(style='margin-top: 1rem'), E.input(style='background: transparent; border-width: 0; outline: none', readonly='readonly'), )) div = container.lastChild.firstChild safe_set_inner_html(div, _('Welcome to the calibre E-book viewer!')) div = div.nextSibling safe_set_inner_html(div, _('Use the PageUp/PageDn or Arrow keys to turn pages')) div = div.nextSibling safe_set_inner_html(div, _('Press the Esc key or {} or tap on the top third of the text area to show the viewer controls').format( _('control+click') if 'macos' in window.navigator.userAgent else _('right click') )) div = div.nextSibling safe_set_inner_html(div, _('Press any key to continue…')) focus() return def msg(txt): return set_css(E.div(txt), padding='1ex 1em', text_align='center', margin='auto') left_msg = msg(_('Tap to turn back')) left_width = 'min(25vw, 1in)' right_msg = msg(_('Tap to turn page')) right_width = 'auto' left_grow = 0 right_grow = 1 if rtl_page_progression(): left_msg, right_msg = right_msg, left_msg left_width, right_width = right_width, left_width left_grow, right_grow = right_grow, left_grow # Clear it out if this is not the first time it's created. # Needed to correctly show it again in a different page progression direction. if container.firstChild: container.removeChild(container.firstChild) container.appendChild(E.div( style=f'overflow: hidden; width: 100vw; height: 100vh; text-align: center; font-size: 1.3rem; font-weight: bold; background: {get_color("window-background")};' + 'display:flex; flex-direction: column; align-items: stretch', E.div( msg(_('Tap (or right click) for controls')), style='height: 25vh; display:flex; align-items: center; border-bottom: solid 2px currentColor', ), E.div( style="display: flex; align-items: stretch; flex-grow: 10", E.div( left_msg, style=f'width: {left_width}; flex-grow: {left_grow}; display:flex; align-items: center; border-right: solid 2px currentColor', ), E.div( right_msg, style=f'width: {right_width}; display:flex; flex-grow: {right_grow}; align-items: center', ) ) )) # }}} def body_font_size(): ans = body_font_size.ans if not ans: q = window.getComputedStyle(document.body).fontSize if q and q.endsWith('px'): q = parseInt(q) if q and not isNaN(q): ans = body_font_size.ans = q return ans ans = body_font_size.ans = 12 return ans def header_footer_font_size(sz): return min(max(0, sz - 6), body_font_size()) def margin_elem(sd, which, id, onclick, oncontextmenu): sz = sd.get(which, 20) fsz = header_footer_font_size(sz) s = '; text-overflow: ellipsis; white-space: nowrap; overflow: hidden' ans = E.div( style=f'height:{sz}px; overflow: hidden; font-size:{fsz}px; width:100%; padding: 0; display: flex; justify-content: space-between; align-items: center; user-select: none', id=id, E.div(style='margin-right: 1.5em' + s), E.div(style=s), E.div(style='margin-left: 1.5em' + s) ) if onclick: ans.addEventListener('click', onclick) if oncontextmenu: 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 # the bottom bar. CriOS is for Chrome on iOS. And in standalone # (web-app mode) there is no nav bar. ans.style.marginBottom = '25px' return ans def side_margin_elem(self, sd, which, icon): ans = E.div( E.div(class_='arrow', style='order: 3', svgicon(f'caret-{icon}', '100%', '100%')), E.div(style='order:1'), E.div(style='order:2', class_='not-arrow'), E.div(style='order:4'), style='width:{}px; user-select: none'.format(sd.get(f'margin_{which}', 20)), class_='book-side-margin', id=f'book-{which}-margin', onclick=self.side_margin_clicked.bind(None, which), oncontextmenu=self.margin_context_menu.bind(None, which), onwheel=self.on_margin_wheel.bind(None, which) ) return ans class View: def __init__(self, container): self.timers = Timers() self.reference_mode_enabled = False self.loaded_resources = {} self.current_progress_frac = self.current_file_progress_frac = 0 self.current_page_counts = {'current': 0, 'total': 0, 'pages_per_screen': 1} self.current_status_message = '' self.current_toc_node = self.current_toc_toplevel_node = None self.current_toc_families = v'[]' self.report_cfi_callbacks = {} self.get_cfi_counter = 0 self.show_loading_callback_timer = None self.timer_ids = {'clock': 0} self.book_scrollbar = BookScrollbar(self) sd = get_session_data() self.keyboard_shortcut_map = create_shortcut_map(sd.get('keyboard_shortcuts')) if ui_operations.export_shortcut_map: ui_operations.export_shortcut_map(self.keyboard_shortcut_map) left_margin = side_margin_elem(self, sd, 'left', 'right' if sd.get('reverse_page_turn_zones') else 'left') set_left_margin_handler(left_margin) right_margin = side_margin_elem(self, sd, 'right', 'left' if sd.get('reverse_page_turn_zones') else 'right') set_right_margin_handler(right_margin) handlers = { 'autoscroll_state_changed': def(data): self.autoscroll_active = v'!!data.running' if ui_operations.autoscroll_state_changed: ui_operations.autoscroll_state_changed(self.autoscroll_active) , 'bump_font_size': self.bump_font_size, 'content_loaded': self.on_content_loaded, 'error': self.on_iframe_error, 'invisible_text': self.on_invisible_text, '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, 'next_section': self.on_next_section, 'next_spine_item': self.on_next_spine_item, 'print': self.on_print, 'ready': self.on_iframe_ready, '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, 'update_selection_position': self.update_selection_position, 'columns_per_screen_changed': self.on_columns_per_screen_changed, '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, 'update_toc_position': self.on_update_toc_position, 'search_result_not_found': self.search_result_not_found, 'search_result_discovered': self.search_result_discovered, 'annotations': self.on_annotations_message, 'tts': self.on_tts_message, 'hints': self.on_hints_message, 'copy_text_to_clipboard': def(data): ui_operations.copy_selection(data.text, data.html) , 'view_image': def(data): if ui_operations.view_image: ui_operations.view_image(data.calibre_src) , } iframe_id = unique_id('read-book-iframe') if runtime.is_standalone_viewer: entry_point = f'{runtime.FAKE_PROTOCOL}://{runtime.SANDBOX_HOST}/book/__index__' else: entry_point = 'read_book.iframe' iframe_kw = { 'id': iframe_id, 'seamless': True, 'sandbox': 'allow-popups allow-scripts allow-popups-to-escape-sandbox', 'style': 'flex-grow: 2', 'allowfullscreen': 'true', } iframe, self.iframe_wrapper = create_wrapped_iframe(handlers, _('Bootstrapping book reader...'), entry_point, iframe_kw) container.appendChild( E.div(style='max-height: 100vh; width: 100vw; height: 100vh; overflow: hidden; display: flex; align-items: stretch', # container for horizontally aligned panels oncontextmenu=def (ev): if not default_context_menu_should_be_allowed(ev): ev.preventDefault() , E.div(style='max-height: 100vh; display: flex; flex-direction: column; align-items: stretch; flex-grow:2', # container for iframe and any other panels in the same column E.div(style='max-height: 100vh; flex-grow: 2; display:flex; align-items: stretch', # container for iframe and its overlay left_margin, E.div(style='flex-grow:2; display:flex; align-items:stretch; flex-direction: column', # container for top and bottom margins margin_elem(sd, 'margin_top', 'book-top-margin', self.top_margin_clicked, self.margin_context_menu.bind(None, 'top')), iframe, margin_elem(sd, 'margin_bottom', 'book-bottom-margin', self.bottom_margin_clicked, self.margin_context_menu.bind(None, 'bottom')), ), right_margin, self.book_scrollbar.create(), E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none;', id='book-selection-bar-overlay'), # selection bar overlay E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none;', id='book-read-aloud-overlay'), # read aloud overlay E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none;', id='book-hints-overlay'), # hints overlay E.div(style='position: absolute; top:0; left:0; width: 100%; height:100%; display:none', id=SearchOverlay.CONTAINER_ID), # search overlay E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='book-content-popup-overlay'), # content popup overlay E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; overflow: auto; display:none', id='book-overlay'), # main overlay E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='controls-help-overlay'), # controls help overlay ) ), ), ) self.current_color_scheme = resolve_color_scheme() if runtime.is_standalone_viewer: document.documentElement.addEventListener('keydown', self.handle_keypress, {'passive': False}) set_ui_colors(self.current_color_scheme.is_dark_theme) is_dark_theme(self.current_color_scheme.is_dark_theme) self.search_overlay = SearchOverlay(self) self.content_popup_overlay = ContentPopupOverlay(self) self.overlay = Overlay(self) self.selection_bar = SelectionBar(self) self.read_aloud = ReadAloud(self) self.hints = Hints(self) self.modal_overlays = v'[self.selection_bar, self.read_aloud, self.hints]' self.processing_spine_item_display = False self.pending_load = None self.currently_showing = {'selection': {'empty': True}} self.book_scrollbar.apply_visibility() self.annotations_manager = AnnotationsManager(self) @property def iframe(self): return self.iframe_wrapper.iframe def copy_to_clipboard(self): self.iframe_wrapper.send_message('copy_selection') def show_not_a_library_book_error(self): error_dialog(_('Not a calibre library book'), _( 'This book is not a part of a calibre library, so no calibre:// URL for it exists.')) def copy_current_location_to_clipboard(self, as_url): link_prefix = get_current_link_prefix() if not link_prefix and as_url: return self.show_not_a_library_book_error() self.get_current_cfi('copy-location-url', def (req_id, data): if as_url: text = link_to_epubcfi(data.cfi, link_prefix) else: text = data.cfi ui_operations.copy_selection(text) ) def set_scrollbar_visibility(self, visible): sd = get_session_data() sd.set('book_scrollbar', bool(visible)) self.book_scrollbar.apply_visibility() def toggle_scrollbar(self): sd = get_session_data() self.set_scrollbar_visibility(not sd.get('book_scrollbar')) def on_annotations_message(self, data): self.selection_bar.handle_message(data) def on_tts_message(self, data): self.read_aloud.handle_message(data) def on_hints_message(self, data): self.hints.handle_message(data) def side_margin_clicked(self, which, event): backwards = which is 'left' if get_session_data().get('reverse_page_turn_zones'): backwards = not backwards if event.button is 0: event.preventDefault(), event.stopPropagation() sd = get_session_data() self.iframe_wrapper.send_message( 'next_screen', backwards=backwards, flip_if_rtl_page_progression=True, all_pages_on_screen=sd.get('paged_margin_clicks_scroll_by_screen')) elif event.button is 2: event.preventDefault(), event.stopPropagation() window.setTimeout(self.show_chrome, 0) self.focus_iframe() def top_margin_clicked(self, event): if event.button is 0 or event.button is 2: event.preventDefault(), event.stopPropagation() self.show_chrome() else: self.focus_iframe() def bottom_margin_clicked(self, event): if event.button is 2: event.preventDefault(), event.stopPropagation() window.setTimeout(self.show_chrome, 0) self.focus_iframe() def margin_context_menu(self, which, event): event.preventDefault(), event.stopPropagation() self.show_chrome() def on_margin_wheel(self, which, event): event.preventDefault() self.send_wheel_event_to_iframe(event, f'margin-{which}') def send_wheel_event_to_iframe(self, event, location): evt = {'location': location} for attr in ('deltaX', 'deltaY', 'deltaMode', 'altKey', 'ctrlKey', 'shiftKey', 'metaKey'): evt[attr] = event[attr] self.iframe_wrapper.send_message('fake_wheel_event', evt=evt) def forward_gesture(self, gesture): self.iframe_wrapper.send_message('gesture_from_margin', gesture=gesture) def iframe_size(self): iframe = self.iframe l, r = document.getElementById('book-left-margin'), document.getElementById('book-right-margin') w = r.offsetLeft - l.offsetLeft - iframe.offsetLeft t, b = document.getElementById('book-top-margin'), document.getElementById('book-bottom-margin') h = b.offsetTop - t.offsetTop - iframe.offsetTop return w, h def on_request_size(self, data): # On iOS/Safari window.innerWidth/Height are incorrect inside an iframe window.scrollTo(0, 0) # ensure the window is at 0 because otherwise it sometimes moves down a bit on mobile thanks to the disappearing nav bar w, h = self.iframe_size() self.iframe_wrapper.send_message('window_size', width=w, height=h) 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) def on_handle_keypress(self, data): self.handle_keypress(data.evt) def handle_keypress(self, evt): if self.overlay.is_visible and evt.key is 'Escape': if self.overlay.handle_escape(): if evt.preventDefault: evt.preventDefault(), evt.stopPropagation() def overlay_visibility_changed(self, visible): if self.iframe_wrapper.send_message: self.iframe_wrapper.send_message('overlay_visibility_changed', visible=visible) if ui_operations.overlay_visibility_changed: ui_operations.overlay_visibility_changed(visible) if visible: for x in self.modal_overlays: x.hide() else: self.selection_bar.update_position() def on_handle_shortcut(self, data): if not data.name: return if data.name is 'back': window.history.back() elif data.name is 'forward': window.history.forward() elif data.name is 'show_chrome': self.show_chrome() elif data.name is 'show_chrome_force': self.show_chrome_force() elif data.name is 'toggle_toc': ui_operations.toggle_toc() elif data.name is 'toggle_bookmarks': if ui_operations.toggle_bookmarks: ui_operations.toggle_bookmarks() else: self.overlay.show_bookmarks() elif data.name is 'toggle_highlights': ui_operations.toggle_highlights() elif data.name is 'new_bookmark': self.new_bookmark() elif data.name is 'copy_to_clipboard': self.copy_to_clipboard() elif data.name is 'copy_location_to_clipboard' or data.name is 'copy_location_as_url_to_clipboard': self.copy_current_location_to_clipboard('url' in data.name) elif data.name is 'toggle_inspector': ui_operations.toggle_inspector() elif data.name is 'toggle_lookup': ui_operations.toggle_lookup() elif data.name is 'toggle_full_screen': ui_operations.toggle_full_screen() elif data.name is 'toggle_paged_mode': self.toggle_paged_mode() elif data.name is 'toggle_toolbar': self.toggle_toolbar() elif data.name is 'toggle_scrollbar': self.toggle_scrollbar() elif data.name is 'quit': ui_operations.quit() elif data.name is 'start_search': self.show_search() elif data.name is 'next_match': ui_operations.find_next() elif data.name is 'previous_match': ui_operations.find_next(True) elif data.name is 'increase_font_size': self.bump_font_size({'increase': True}) elif data.name is 'decrease_font_size': self.bump_font_size({'increase': False}) elif data.name is 'default_font_size': restore_default_font_size() elif data.name is 'toggle_full_screen': ui_operations.toggle_full_screen() elif data.name is 'toggle_reference_mode': self.toggle_reference_mode() elif data.name is 'read_aloud': self.start_read_aloud() elif data.name is 'toggle_hints': self.toggle_hints() elif data.name is 'toggle_read_aloud': self.toggle_read_aloud() elif data.name is 'reload_book': self.reload_book() elif data.name is 'sync_book': self.overlay.sync_book() elif data.name is 'next_section': self.on_next_section({'forward': True}) elif data.name is 'previous_section': self.on_next_section({'forward': False}) elif data.name is 'open_book': self.overlay.open_book() elif data.name is 'next': self.iframe_wrapper.send_message( 'next_screen', backwards=False, flip_if_rtl_page_progression=False, all_pages_on_screen=get_session_data().get('paged_margin_clicks_scroll_by_screen')) elif data.name is 'previous': self.iframe_wrapper.send_message( 'next_screen', backwards=True, flip_if_rtl_page_progression=False, all_pages_on_screen=get_session_data().get('paged_margin_clicks_scroll_by_screen')) elif data.name is 'clear_selection': self.iframe_wrapper.send_message('clear_selection') elif data.name is 'print': ui_operations.print_book() elif data.name is 'preferences': self.show_chrome({'initial_panel': 'show_prefs'}) elif data.name is 'metadata': self.overlay.show_metadata() elif data.name is 'edit_book': ui_operations.edit_book(current_spine_item(), self.current_file_progress_frac, self.currently_showing?.selection?.text) elif data.name is 'goto_location': self.overlay.show_ask_for_location() elif data.name is 'select_all': self.iframe_wrapper.send_message('modify_selection', granularity='all') elif data.name.startsWith('shrink_selection_by_'): self.iframe_wrapper.send_message('modify_selection', direction='backward', granularity=data.name.rpartition('_')[-1]) elif data.name.startsWith('extend_selection_by_'): self.iframe_wrapper.send_message('modify_selection', direction='forward', granularity=data.name.rpartition('_')[-1]) elif data.name is 'extend_selection_to_start_of_line': self.iframe_wrapper.send_message('modify_selection', direction='backward', granularity='lineboundary') elif data.name is 'extend_selection_to_end_of_line': self.iframe_wrapper.send_message('modify_selection', direction='forward', granularity='lineboundary') 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) elif data.name is 'toggle_autoscroll': self.toggle_autoscroll() elif data.name.startsWith('switch_color_scheme:'): self.switch_color_scheme(data.name.partition(':')[-1]) elif data.name is 'increase_number_of_columns': self.iframe_wrapper.send_message('change_number_of_columns', delta=1) elif data.name is 'decrease_number_of_columns': self.iframe_wrapper.send_message('change_number_of_columns', delta=-1) elif data.name is 'reset_number_of_columns': self.iframe_wrapper.send_message('change_number_of_columns', delta=0) else: self.iframe_wrapper.send_message('handle_navigation_shortcut', name=data.name) def on_selection_change(self, data): self.currently_showing.selection = { 'text': data.text, 'empty': data.empty, 'start': data.selection_extents.start, 'end': data.selection_extents.end, 'annot_id': data.annot_id, 'drag_mouse_position': data.drag_mouse_position, 'selection_change_caused_by_search': data.selection_change_caused_by_search, 'rtl': data.rtl, 'vertical': data.vertical, 'start_is_anchor': data.start_is_anchor } if ui_operations.selection_changed: ui_operations.selection_changed(self.currently_showing.selection.text, self.currently_showing.selection.annot_id) self.selection_bar.update_position() def new_bookmark(self): if ui_operations.new_bookmark: self.get_current_cfi('new-bookmark', ui_operations.new_bookmark) else: self.get_current_cfi('new-bookmark', def (req_id, data): create_new_bookmark(self.annotations_manager, data) ) def update_selection_position(self, data): sel = self.currently_showing.selection sel.start = data.selection_extents.start sel.end = data.selection_extents.end self.selection_bar.update_position() def on_columns_per_screen_changed(self, data): sd = get_session_data() cps = sd.get('columns_per_screen') or {} cps[data.which] = int(data.cps) sd.set('columns_per_screen', cps) def switch_color_scheme(self, name): get_session_data().set('current_color_scheme', name) ui_operations.redisplay_book() def toggle_paged_mode(self): sd = get_session_data() mode = sd.get('read_mode') new_mode = 'flow' if mode is 'paged' else 'paged' sd.set('read_mode', new_mode) ui_operations.redisplay_book() def toggle_autoscroll(self): self.iframe_wrapper.send_message('toggle_autoscroll') def toggle_toolbar(self): sd = get_session_data() misc = sd.get('standalone_misc_settings') misc.show_actions_toolbar = v'!misc.show_actions_toolbar' sd.set('standalone_misc_settings', misc) def on_invisible_text(self, data): warning_dialog( _('Not found'), _('The text: {} is present on this page but not visible').format(html_escape(data.text)), on_close=def(): self.search_overlay.show() ) def bump_font_size(self, data): mult = 1 if data.increase else -1 frac = 0.2 if runtime.is_standalone_viewer: frac = current_zoom_step_size() / 100 change_font_size_by(mult * frac) def on_show_footnote(self, data): self.show_content_popup() self.content_popup_overlay.show_footnote(data) def hide_overlays(self): self.overlay.hide() self.search_overlay.hide() self.content_popup_overlay.hide() self.focus_iframe() def focus_iframe(self): for x in self.modal_overlays: if x.is_visible: x.focus() return self.iframe.contentWindow.focus() def start_read_aloud(self, dont_start_talking): for x in self.modal_overlays: if x is not self.read_aloud: x.hide() self.read_aloud.show() if not dont_start_talking: self.read_aloud.play() def toggle_read_aloud(self): if self.read_aloud.is_visible: self.read_aloud.hide() else: self.start_read_aloud() def toggle_hints(self): if self.hints.is_visible: self.hints.hide() else: for x in self.modal_overlays: if x is not self.hints: x.hide() self.hints.show() def show_chrome(self, data): elements = {} if data and data.elements: elements = data.elements initial_panel = data?.initial_panel or None self.get_current_cfi('show-chrome', self.do_show_chrome.bind(None, elements, initial_panel)) def show_chrome_force(self): self.hide_overlays() self.show_chrome() def do_show_chrome(self, elements, initial_panel, request_id, cfi_data): self.hide_overlays() self.update_cfi_data(cfi_data) if initial_panel: getattr(self.overlay, initial_panel)() else: self.overlay.show(elements) def prepare_for_close(self): def close_prepared(request_id, cfi_data): ui_operations.close_prep_finished(cfi_data.cfi) self.get_current_cfi('prepare-close', close_prepared) def show_search(self, trigger): self.hide_overlays() text = self.currently_showing.selection.text if runtime.is_standalone_viewer: ui_operations.show_search(text or '', trigger) else: if text: self.search_overlay.set_text(text) self.search_overlay.show(text) if trigger and text: self.search_overlay.find_next() def show_content_popup(self): self.hide_overlays() self.content_popup_overlay.show() def set_margins(self): no_margins = self.currently_showing.name is self.book.manifest.title_page_name sd = get_session_data() margin_left = 0 if no_margins else sd.get('margin_left') margin_right = 0 if no_margins else sd.get('margin_right') margin_top = 0 if no_margins else sd.get('margin_top') margin_bottom = 0 if no_margins else sd.get('margin_bottom') max_text_height = sd.get('max_text_height') th = window.innerHeight - margin_top - margin_bottom if not no_margins and max_text_height > 100 and th > max_text_height: extra = (th - max_text_height) // 2 margin_top += extra margin_bottom += extra max_text_width = sd.get('max_text_width') tw = window.innerWidth - margin_left - margin_right if not no_margins and max_text_width > 100 and tw > max_text_width: extra = (tw - max_text_width) // 2 margin_left += extra margin_right += extra set_css(document.getElementById('book-top-margin'), height=margin_top + 'px', font_size=header_footer_font_size(margin_top) + 'px') set_css(document.getElementById('book-bottom-margin'), height=margin_bottom + 'px', font_size=header_footer_font_size(margin_bottom) + 'px') def side_margin(which, val): m = document.getElementById('book-{}-margin'.format(which)) if which is 'left': # Explicitly set the width of the central panel. This is needed # on small screens with chrome, without it sometimes the right # margin/scrollbar goes off the screen. m.nextSibling.style.maxWidth = 'calc(100vw - {}px)'.format( margin_left + margin_right + self.book_scrollbar.effective_width) set_css(m, width=val + 'px') val = min(val, 25) s = m.querySelector('.arrow').style s.width = val + 'px' s.height = val + 'px' side_margin('left', margin_left), side_margin('right', margin_right) def on_iframe_ready(self, data): data.width, data.height = self.iframe_size() if ui_operations.on_iframe_ready: ui_operations.on_iframe_ready() return self.do_pending_load def do_pending_load(self): if self.pending_load: data = self.pending_load self.pending_load = None self.show_spine_item_stage2(data) def on_iframe_error(self, data): title = data.title or _('There was an error processing the book') msg = _('Unknown error') if data.errkey: if data.errkey is 'no-auto-scroll-in-paged-mode': title = _('No auto scroll in paged mode') msg = _('Switch to flow mode (Viewer preferences->Page layout) to enable auto scrolling') elif data.errkey is 'changing-columns-in-flow-mode': title=_('In flow mode') msg=_('Cannot change number of pages per screen in flow mode, switch to paged mode first.') elif data.errkey = 'unhandled-error': title = _('Unhandled error') if data.is_non_critical: warning_dialog(title, msg, data.details, on_close=ui_operations.focus_iframe) return ui_operations.show_error(title, msg, data.details) def apply_color_scheme(self): self.current_color_scheme = ans = resolve_color_scheme() iframe = self.iframe if runtime.is_standalone_viewer: set_ui_colors(self.current_color_scheme.is_dark_theme) else: iframe.style.colorScheme = 'dark' if self.current_color_scheme.is_dark_theme else 'light' is_dark_theme(self.current_color_scheme.is_dark_theme) for which in 'left top right bottom'.split(' '): m = document.getElementById('book-{}-margin'.format(which)) s = m.style mc = ans[f'margin_{which}'] if mc: s.backgroundColor, s.color = mc.split(':') else: s.color = ans.foreground s.backgroundColor = ans.background sd = get_session_data() iframe.style.backgroundColor = ans.background or 'white' bg_image = sd.get('background_image') if bg_image: iframe.style.backgroundImage = f'url({READER_BACKGROUND_URL}?{Date().getTime()})' if runtime.is_standalone_viewer else f'url({bg_image})' else: iframe.style.backgroundImage = 'none' if sd.get('background_image_style') is 'scaled': iframe.style.backgroundSize = '100% 100%' iframe.style.backgroundRepeat = 'no-repeat' iframe.style.backgroundAttachment = 'scroll' iframe.style.backgroundPosition = 'center' else: iframe.style.backgroundSize = 'auto' iframe.style.backgroundRepeat = 'repeat' iframe.style.backgroundAttachment = 'scroll' iframe.style.backgroundPosition = '0 0' self.content_popup_overlay.apply_color_scheme(ans.background, ans.foreground) self.book_scrollbar.apply_color_scheme(ans) # this is needed on iOS where the bottom margin has its own margin, # so we dont want the body background color to bleed through iframe.parentNode.style.backgroundColor = ans.background iframe.parentNode.parentNode.style.backgroundColor = ans.background return ans def on_resize(self): if self.book and self.currently_showing.name: sd = get_session_data() if sd.get('max_text_width') or sd.get('max_text_height'): self.set_margins() def show_loading_message(self, msg): self.overlay.show_loading_message(msg) def show_loading(self): msg = _('Loading next section from {title}, please wait…').format(title=self.book.metadata.title or _('Unknown')) if self.show_loading_callback_timer is not None: clearTimeout(self.show_loading_callback_timer) self.show_loading_callback_timer = setTimeout(self.show_loading_message.bind(None, msg), 200) def hide_loading(self): if window.read_book_initial_open_search_text: q = window.read_book_initial_open_search_text v'delete window.read_book_initial_open_search_text' self.search_overlay.do_initial_search(q.text, q.query) self.show_loading_message(_('Searching for: {}').format(q.query)) return if self.show_loading_callback_timer is not None: clearTimeout(self.show_loading_callback_timer) self.show_loading_callback_timer = None self.iframe.style.visibility = 'visible' self.overlay.hide_loading_message() self.focus_iframe() def parse_cfi(self, encoded_cfi, book): name = cfi = None if encoded_cfi and encoded_cfi.startswith('epubcfi(/'): cfi = encoded_cfi[len('epubcfi(/'):-1] snum, rest = cfi.partition('/')[::2] try: snum = int(snum) except Exception: print('Invalid spine number in CFI:', snum) if jstype(snum) is 'number': name = book.manifest.spine[(int(snum) // 2) - 1] or name cfi = '/' + rest return name, cfi def open_book_page(self): # Open the page for the current book in a new tab if self.book and self.book.key: window.open(query_as_href({ 'library_id': self.book.key[0], 'book_id': self.book.key[1] + '', 'close_action': 'book_list', }, 'book_details')) def clear_book_resource_caches(self): self.loaded_resources = {} self.content_popup_overlay.loaded_resources = {} def reload_book(self): self.clear_book_resource_caches() ui_operations.reload_book() def display_book(self, book, initial_position, is_redisplay): self.hide_overlays() self.iframe.focus() is_current_book = self.book and self.book.key == book.key self.book_load_started = True if is_current_book: if not is_redisplay: self.search_overlay.clear_caches(book) # could be a reload else: if self.book: self.iframe_wrapper.reset() self.content_popup_overlay.reset() self.clear_book_resource_caches() self.timers.start_book(book) self.search_overlay.clear_caches(book) unkey = username_key(get_interface_data().username) self.book = current_book.book = book hl = None if not is_redisplay: if runtime.is_standalone_viewer: hl = book.highlights v'delete book.highlights' else: if unkey and book.annotations_map[unkey]: hl = book.annotations_map[unkey].highlight self.annotations_manager.set_bookmarks(book.annotations_map[unkey].bookmark or v'[]') self.annotations_manager.set_highlights(hl or v'[]') if runtime.is_standalone_viewer: add_book_to_recently_viewed(book) if ui_operations.update_last_read_time: ui_operations.update_last_read_time(book) pos = {'replace_history':True} if not book.manifest.spine.length: ui_operations.show_error(_('Invalid book'), _('This book is empty, with no items in the spine')) return name = book.manifest.spine[0] cfi = None if initial_position and initial_position.type is 'cfi' and initial_position.data.startswith('epubcfi(/'): cfi = initial_position.data else: q = parse_url_params() if q.bookpos and q.bookpos.startswith('epubcfi(/'): cfi = q.bookpos elif book.last_read_position and book.last_read_position[unkey]: cfi = book.last_read_position[unkey] cfiname, internal_cfi = self.parse_cfi(cfi, book) if cfiname and internal_cfi: name = cfiname pos.type, pos.cfi = 'cfi', internal_cfi navigated = False if initial_position: if initial_position.type is 'toc': navigated = self.goto_toc_node(initial_position.data) elif initial_position.type is 'bookpos': navigated = True self.goto_book_position(initial_position.data) elif initial_position.type is 'ref': navigated = self.goto_reference(initial_position.data) if navigated: self.hide_loading() else: self.show_name(name, initial_position=pos) sd = get_session_data() help_key = 'controls_help_shown_count' + ('_rtl_page_progression' if rtl_page_progression() else '') if not self[help_key]: self[help_key] = True c = sd.get(help_key, 0) if c < 2: show_controls_help() sd.set('controls_help_shown_count' + ('_rtl_page_progression' if rtl_page_progression() else ''), c + 1) def preferences_changed(self): self.set_margins() ui_operations.update_url_state(True) ui_operations.redisplay_book() def redisplay_book(self): # redisplay_book() is called when settings are changed if not self.book: if runtime.is_standalone_viewer: self.overlay.open_book() return sd = get_session_data() self.keyboard_shortcut_map = create_shortcut_map(sd.get('keyboard_shortcuts')) if ui_operations.export_shortcut_map: ui_operations.export_shortcut_map(self.keyboard_shortcut_map) self.book_scrollbar.apply_visibility() self.display_book(self.book, None, True) def iframe_settings(self, name): sd = get_session_data() bg_image_fade = 'transparent' cs = self.apply_color_scheme() fade = int(sd.get('background_image_fade')) rgba = cached_color_to_rgba(cs.background) if self.iframe.style.backgroundImage is not 'none' and fade > 0: bg_image_fade = f'rgba({rgba[0]}, {rgba[1]}, {rgba[2]}, {fade/100})' return { 'margin_left': 0 if name is self.book.manifest.title_page_name else sd.get('margin_left'), 'margin_right': 0 if name is self.book.manifest.title_page_name else sd.get('margin_right'), 'margin_top': 0 if name is self.book.manifest.title_page_name else sd.get('margin_top'), 'margin_bottom': 0 if name is self.book.manifest.title_page_name else sd.get('margin_bottom'), 'read_mode': sd.get('read_mode'), 'columns_per_screen': sd.get('columns_per_screen'), 'color_scheme': cs, 'override_book_colors': sd.get('override_book_colors'), 'is_dark_theme': cs.is_dark_theme, 'bg_image_fade': bg_image_fade, 'base_font_size': sd.get('base_font_size'), 'user_stylesheet': sd.get('user_stylesheet'), 'keyboard_shortcuts': sd.get('keyboard_shortcuts'), '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'), 'paged_wheel_section_jumps': sd.get('paged_wheel_section_jumps'), 'paged_pixel_scroll_threshold': sd.get('paged_pixel_scroll_threshold'), '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'), 'reverse_page_turn_zones': sd.get('reverse_page_turn_zones'), 'gesture_overrides': sd.get('gesture_overrides'), } def show_name(self, name, initial_position=None): if self.currently_showing.loading: return self.processing_spine_item_display = False initial_position = initial_position or {'replace_history':False} spine = self.book.manifest.spine idx = spine.indexOf(name) self.currently_showing = { 'name':name, 'settings':self.iframe_settings(name), 'initial_position':initial_position, 'loading':True, 'spine_index': idx, 'selection': {'empty': True}, } self.show_loading() set_current_spine_item(name) if idx > -1: self.currently_showing.bookpos = 'epubcfi(/{})'.format(2 * (idx +1)) self.set_margins() self.load_doc(name, self.show_spine_item) def load_doc(self, name, done_callback): def cb(resource_data): self.loaded_resources = resource_data done_callback(resource_data) load_resources(self.book, name, self.loaded_resources, cb) def goto_doc_boundary(self, start): name = self.book.manifest.spine[0 if start else self.book.manifest.spine.length - 1] self.show_name(name, initial_position={'type':'frac', 'frac':0 if start else 1, 'replace_history':False}) def goto_frac(self, frac): if not self.book or not self.book.manifest: return chapter_start_page = 0 total_length = self.book.manifest.spine_length page = total_length * frac chapter_frac = 0 chapter_name = None for name in self.book.manifest.spine: chapter_length = self.book.manifest.files[name]?.length or 0 chapter_end_page = chapter_start_page + chapter_length if chapter_start_page <= page <= chapter_end_page: num_pages = chapter_end_page - chapter_start_page - 1 if num_pages > 0: chapter_frac = (page - chapter_start_page) / num_pages else: chapter_frac = 0 chapter_name = name break chapter_start_page = chapter_end_page if not chapter_name: chapter_name = self.book.manifest.spine[-1] chapter_frac = max(0, min(chapter_frac, 1)) if self.currently_showing.name is chapter_name: self.iframe_wrapper.send_message('scroll_to_frac', frac=chapter_frac) else: self.show_name(chapter_name, initial_position={'type':'frac', 'frac':chapter_frac, 'replace_history':True}) def goto_book_position(self, bpos): val = max(0, min(1000 * float(bpos) / self.current_position_data.book_length, 1)) return self.goto_frac(val) def on_scroll_to_anchor(self, data): self.show_name(data.name, initial_position={'type':'anchor', 'anchor':data.frag, 'replace_history':False}) def link_in_content_popup_activated(self, name, frag, is_popup, title): self.content_popup_overlay.hide() if is_popup: self.iframe_wrapper.send_message('fake_popup_activation', name=name, frag=frag, title=title) else: self.goto_named_destination(name, frag) def goto_cfi(self, bookpos, add_to_history): cfiname, internal_cfi = self.parse_cfi(bookpos, self.book) if cfiname and internal_cfi: # Note that goto_cfi is used by back() so it must not add to # history by default, otherwise forward() will not work pos = {'replace_history': not add_to_history} name = cfiname pos.type, pos.cfi = 'cfi', internal_cfi self.show_name(name, initial_position=pos) return True return False def goto_reference(self, reference): if not self.book or not self.book.manifest: return index, refnum = reference.split('.') index, refnum = int(index), (int(refnum) if refnum else 1) chapter_name = self.book.manifest.spine[index] if not chapter_name: return False if self.currently_showing.name is chapter_name: self.iframe_wrapper.send_message('scroll_to_ref', refnum=refnum) else: self.show_name(chapter_name, initial_position={'type':'ref', 'refnum':refnum, 'replace_history':True}) return True def goto_named_destination(self, name, frag): if self.currently_showing.name is name: self.iframe_wrapper.send_message('scroll_to_anchor', frag=frag) else: spine = self.book.manifest.spine idx = spine.indexOf(name) if idx is -1: error_dialog(_('Destination does not exist'), _( 'The file {} does not exist in this book').format(name), on_close=def(): ui_operations.focus_iframe() ) return False self.show_name(name, initial_position={'type':'anchor', 'anchor':frag, 'replace_history':False}) return True def goto_toc_node(self, node_id): toc = self.book.manifest.toc found = False def process_node(x): nonlocal found if x.id is node_id: self.goto_named_destination(x.dest or '', x.frag or '') found = True return for c in x.children: process_node(c) if toc: process_node(toc) return found def sync_data_received(self, reading_pos_cfi, annotations_map): if annotations_map: ui_operations.annotations_synced(annotations_map) if annotations_map.highlight: if self.annotations_manager.merge_highlights(annotations_map.highlight): hl = self.annotations_manager.highlights_for_currently_showing() self.iframe_wrapper.send_message('replace_highlights', highlights=hl) if reading_pos_cfi: self.goto_cfi(reading_pos_cfi) def set_notes_for_highlight(self, uuid, notes): if self.annotations_manager.set_notes_for_highlight(uuid, notes): self.selection_bar.notes_edited(uuid) self.selection_bar.update_position() def show_next_spine_item(self, previous): spine = self.book.manifest.spine idx = spine.indexOf(self.currently_showing.name) if previous: if idx is 0: return False idx = min(spine.length - 1, max(idx - 1, 0)) self.show_name(spine[idx], initial_position={'type':'frac', 'frac':1, 'replace_history':True}) else: if idx is spine.length - 1: return False idx = max(0, min(spine.length - 1, idx + 1)) self.show_name(spine[idx], initial_position={'type':'frac', 'frac':0, 'replace_history':True}) return True def on_next_spine_item(self, data): self.show_next_spine_item(data.previous) def on_next_section(self, data): toc_node = get_next_section(data.forward) if toc_node: self.goto_named_destination(toc_node.dest, toc_node.frag) def get_current_cfi(self, request_id, callback): self.get_cfi_counter += 1 request_id += ':' + self.get_cfi_counter self.report_cfi_callbacks[request_id] = callback self.iframe_wrapper.send_message('get_current_cfi', request_id=request_id) def update_cfi_data(self, data): username = get_interface_data().username if self.book: self.currently_showing.bookpos = data.cfi unkey = username_key(username) if not self.book.last_read_position: self.book.last_read_position = {} self.book.last_read_position[unkey] = data.cfi self.set_progress_frac(data.progress_frac, data.file_progress_frac, data.page_counts) self.update_header_footer() if ui_operations.update_last_read_time: ui_operations.update_last_read_time(self.book) return username def on_report_cfi(self, data): cb = self.report_cfi_callbacks[data.request_id] if cb: cb(data.request_id.rpartition(':')[0], { 'cfi': data.cfi, 'progress_frac': data.progress_frac, 'file_progress_frac': data.file_progress_frac, 'selected_text': data.selected_text, 'selection_bounds': data.selection_bounds, 'page_counts': data.page_counts }) v'delete self.report_cfi_callbacks[data.request_id]' def on_update_progress_frac(self, data): self.set_progress_frac(data.progress_frac, data.file_progress_frac, data.page_counts) self.update_header_footer() def on_update_cfi(self, data): overlay_shown = not self.processing_spine_item_display and self.overlay.is_visible if overlay_shown or self.search_overlay.is_visible or self.content_popup_overlay.is_visible: # Chrome on Android stupidly resizes the viewport when the on # screen keyboard is displayed. This means that the push_state() # below causes the overlay to be closed, making it impossible to # type anything into text boxes. # See https://bugs.chromium.org/p/chromium/issues/detail?id=404315 return username = self.update_cfi_data(data) ui_operations.update_url_state(data.replace_history) if username: key = self.book.key lrd = {'device':get_device_uuid(), 'cfi':data.cfi, 'pos_frac':data.progress_frac} ajax_send('book-set-last-read-position/{library_id}/{book_id}/{fmt}'.format( library_id=key[0], book_id=key[1], fmt=key[2]), lrd, def(end_type, xhr, ev): if end_type is not 'load': print('Failed to update last read position, AJAX call did not succeed') ) update_book_in_recently_read_by_user_on_home_page(key[0], key[1], key[2], data.cfi) @property def current_position_data(self): if self.book?.manifest: book_length = self.book.manifest.spine_length or 0 name = self.currently_showing.name chapter_length = self.book.manifest.files[name]?.length or 0 else: book_length = chapter_length = 0 pos = { 'progress_frac': self.current_progress_frac, 'book_length': book_length, 'chapter_length': chapter_length, 'file_progress_frac': self.current_file_progress_frac, 'cfi': self.currently_showing?.bookpos, 'page_counts': self.current_page_counts, } return pos def show_status_message(self, msg, timeout): self.current_status_message = msg or '' self.update_header_footer() if self.current_status_message: if not timeout?: timeout = 10000 window.setTimeout(def(): self.show_status_message();, timeout) def create_template_renderer(self): if not self.book: return pos = self.current_position_data book_length = pos.book_length * max(0, 1 - pos.progress_frac) chapter_length = pos.chapter_length * max(0, 1 - pos.file_progress_frac) book_time = self.timers.time_for(book_length) chapter_time = self.timers.time_for(chapter_length) mi = self.book.metadata def render(div, name, which, override): return render_head_foot(div, name, which, mi, self.current_toc_node, self.current_toc_toplevel_node, book_time, chapter_time, pos, override) return render def update_header_footer(self): renderer = self.create_template_renderer() if not renderer: return sd = get_session_data() has_clock = False def render_template(div, edge, name): nonlocal has_clock c = div.lastChild b = c.previousSibling a = b.previousSibling if sd.get(f'margin_{edge}', 20) > 5: override = self.current_status_message if edge is 'bottom' else '' hca = renderer(a, name, 'left', override) hcb = renderer(b, name, 'middle', '') hcc = renderer(c, name, 'right', '') if hca or hcb or hcc: has_clock = True else: clear(a), clear(b), clear(c) for edge in ('left', 'right', 'top', 'bottom'): div = document.getElementById(f'book-{edge}-margin') if div: tname = {'left':'left-margin', 'right': 'right-margin', 'top': 'header', 'bottom': 'footer'}[edge] render_template(div, edge, tname) if has_clock: if not self.timer_ids.clock: self.timer_ids.clock = window.setInterval(self.update_header_footer, 60000) else: if self.timer_ids.clock: window.clearInterval(self.timer_ids.clock) self.timer_ids.clock = 0 def on_update_toc_position(self, data): update_visible_toc_nodes(data.visible_anchors) self.current_toc_families = get_current_toc_nodes() if self.current_toc_families.length: first = self.current_toc_families[0] self.current_toc_node = first[-1] self.current_toc_toplevel_node = first[0] else: self.current_toc_node = self.current_toc_toplevel_node = None if runtime.is_standalone_viewer: r = v'[]' for fam in self.current_toc_families: if fam.length: r.push(fam[-1].id) ui_operations.update_current_toc_nodes(r) self.update_header_footer() def show_spine_item(self, resource_data): self.pending_load = resource_data if self.iframe_wrapper.ready: self.do_pending_load() else: self.iframe_wrapper.init() def show_spine_item_stage2(self, resource_data): # We cannot encrypt this message because the resource data contains # Blob objects which do not survive encryption self.processing_spine_item_display = True self.current_status_message = '' self.iframe.style.visibility = 'hidden' self.iframe_wrapper.send_unencrypted_message('display', resource_data=resource_data, book=self.book, name=self.currently_showing.name, initial_position=self.currently_showing.initial_position, settings=self.currently_showing.settings, reference_mode_enabled=self.reference_mode_enabled, is_titlepage=self.currently_showing.name is self.book.manifest.title_page_name, highlights=self.annotations_manager.highlights_for_currently_showing(), ) def on_content_loaded(self, data): for x in self.modal_overlays: if not x.dont_hide_on_content_loaded: x.hide() self.processing_spine_item_display = False self.currently_showing.loading = False self.hide_loading() self.set_progress_frac(data.progress_frac, data.file_progress_frac, data.page_counts) self.update_header_footer() window.scrollTo(0, 0) # ensure window is at 0 on mobile where the navbar causes issues if self.book_load_started: self.book_load_started = False if ui_operations.clear_history: ui_operations.clear_history() if ui_operations.content_file_changed: ui_operations.content_file_changed(self.currently_showing.name) if self.read_aloud.is_visible: self.read_aloud.play() def set_progress_frac(self, progress_frac, file_progress_frac, page_counts): self.current_progress_frac = progress_frac or 0 self.current_file_progress_frac = file_progress_frac or 0 self.current_page_counts = page_counts self.book_scrollbar.sync_to_contents(self.current_progress_frac) def update_font_size(self): self.iframe_wrapper.send_message('change_font_size', base_font_size=get_session_data().get('base_font_size')) def viewer_font_size_changed(self): self.iframe_wrapper.send_message('viewer_font_size_changed', 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.apply_color_scheme() self.iframe_wrapper.send_message('change_color_scheme', color_scheme=cs) def toggle_reference_mode(self): self.reference_mode_enabled = not self.reference_mode_enabled self.iframe_wrapper.send_message('set_reference_mode', enabled=self.reference_mode_enabled) if ui_operations.reference_mode_changed: ui_operations.reference_mode_changed(self.reference_mode_enabled) def discover_search_result(self, sr): if sr.search_finished: if self.search_result_discovery: self.search_result_discovery.finished = True if not self.search_result_discovery.discovered and self.search_result_discovery.first_search_result and self.search_result_discovery.queue.length is 0: sr = self.search_result_discovery.first_search_result sr.force_jump_to = True self.search_result_discovery.jump_forced = True self.show_search_result(sr) return if sr.result_num is 1: self.search_result_discovery = { 'queue': v'[]', 'on_discovery': sr.on_discovery, 'in_flight': None, 'discovered': False, 'first_search_result': sr, 'finished': False, 'jump_forced': False, } if not self.search_result_discovery or self.search_result_discovery.discovered or self.search_result_discovery.on_discovery is not sr.on_discovery: return self.search_result_discovery.queue.push(sr) if not self.search_result_discovery.in_flight: self.show_search_result(self.search_result_discovery.queue.shift()) def handle_search_result_discovery(self, sr, discovered): if self.search_result_discovery?.on_discovery is sr.on_discovery: self.search_result_discovery.in_flight = None if discovered: if not self.search_result_discovery.discovered: self.search_result_discovery.discovered = True ui_operations.search_result_discovered(sr) elif not self.search_result_discovery.discovered and self.search_result_discovery.queue.length: self.show_search_result(self.search_result_discovery.queue.shift()) elif not self.search_result_discovery.discovered and self.search_result_discovery.finished and not self.search_result_discovery.jump_forced: sr = self.search_result_discovery.first_search_result sr.force_jump_to = True self.search_result_discovery.jump_forced = True self.show_search_result(sr) def search_result_discovered(self, data): self.handle_search_result_discovery(data.search_result, data.discovered) def search_result_not_found(self, data): if ui_operations.search_result_not_found: ui_operations.search_result_not_found(data.search_result) self.handle_search_result_discovery(data.search_result, False) def show_search_result(self, sr): if self.currently_showing.name is sr.file_name: self.iframe_wrapper.send_message('show_search_result', search_result=sr) else: self.show_name(sr.file_name, initial_position={'type':'search_result', 'search_result':sr, 'replace_history':True}) if self.search_result_discovery?.on_discovery is sr.on_discovery: self.search_result_discovery.in_flight = sr.result_num def highlight_action(self, uuid, which): spine = self.book.manifest.spine spine_index = self.annotations_manager.spine_index_for_highlight(uuid, spine) if spine_index < 0 or spine_index >= spine.length: if which is 'edit': self.selection_bar.report_failed_edit_highlight(uuid) return if which is 'edit': if self.currently_showing.spine_index is spine_index: self.selection_bar.edit_highlight(uuid) else: self.show_name(spine[spine_index], initial_position={'type':'edit_annotation', 'uuid': uuid, 'replace_history':True}) elif which is 'delete': self.selection_bar.remove_highlight_with_id(uuid) elif which is 'goto': cfi = self.annotations_manager.cfi_for_highlight(uuid, spine_index) if cfi: self.goto_cfi(cfi, True)