Kovid Goyal 337bbb3c90
...
2023-10-17 09:05:15 +05:30

1481 lines
67 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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 <b>calibre E-book viewer</b>!'))
div = div.nextSibling
safe_set_inner_html(div, _('Use the <b>PageUp/PageDn</b> or <b>Arrow keys</b> to turn pages'))
div = div.nextSibling
safe_set_inner_html(div, _('Press the <b>Esc</b> key or <b>{}</b> or <b>tap on the top third</b> 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: <i>{}</i> 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 <i>{title}</i>, 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)