calibre/src/pyj/viewer-main.pyj

416 lines
14 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals
import traceback
from elementmaker import E
from gettext import gettext as _, install
import initialize # noqa: unused-import
from ajax import ajax, workaround_qt_bug
from book_list.globals import get_session_data, set_session_data
from book_list.library_data import library_data
from book_list.theme import get_color, css_for_variables
from dom import get_widget_css, set_css
from modals import create_modal_container
from qt import from_python, to_python
from read_book.db import new_book
from read_book.footnotes import main as footnotes_main
from read_book.globals import runtime, set_system_colors, ui_operations, default_color_schemes
from read_book.iframe import main as iframe_main
from read_book.shortcuts import add_standalone_viewer_shortcuts
from read_book.view import View
from session import local_storage, session_defaults
from utils import debounce, encode_query_with_path, parse_url_params
from viewer.constants import FAKE_HOST, FAKE_PROTOCOL, READER_BACKGROUND_URL
runtime.is_standalone_viewer = True
runtime.FAKE_HOST = FAKE_HOST
runtime.SANDBOX_HOST = FAKE_HOST.rpartition('.')[0] + '.sandbox'
runtime.FAKE_PROTOCOL = FAKE_PROTOCOL
book = None
view = None
def file_received(name, file_data, proceed, end_type, xhr, ev):
end_type = workaround_qt_bug(xhr, end_type)
if end_type is 'abort':
return
if end_type is not 'load':
show_error(_('Failed to load file from book'), _(
'Could not load the file: {} with error: {}').format(name, xhr.error_html))
return
if not xhr.responseType or xhr.responseType is 'text':
result = xhr.responseText
else if xhr.responseType is 'blob':
result = xhr.response
else:
show_error(_('Failed to load file from book'), _(
'Could not load the file: {} unknown response type: {}').format(name, xhr.responseType))
return
proceed(result, name, file_data.mimetype, book)
def get_file(book, name, proceed):
entry = book.manifest.files[name]
if not entry:
raise ValueError(f'No file named {name} in the book manifest')
xhr = ajax('book/' + name, file_received.bind(None, name, entry, proceed), ok_code=0)
if entry.is_html or entry.mimetype.startswith('text/') or entry.mimetype is 'application/javascript':
xhr.responseType = 'text'
else:
xhr.responseType = 'blob'
xhr.send()
def get_mathjax_files(proceed):
proceed({})
def update_url_state(replace):
if view and view.currently_showing:
bookpos = view.currently_showing.bookpos
if bookpos:
query = {'bookpos': bookpos}
query = encode_query_with_path(query)
if replace:
window.history.replaceState(None, '', query)
else:
window.history.pushState(None, '', query)
def on_pop_state():
if view and view.currently_showing:
data = parse_url_params()
if data.bookpos and data.bookpos.startswith('epubcfi(/'):
view.goto_cfi(data.bookpos)
def show_error(title, msg, details):
to_python.show_error(title, msg, details)
def manifest_received(key, initial_position, pathtoebook, highlights, end_type, xhr, ev):
nonlocal book
end_type = workaround_qt_bug(xhr, end_type)
if end_type is 'load':
book = new_book(key, {})
data = xhr.response
book.manifest = data[0]
book.metadata = book.manifest.metadata = data[1]
book.manifest.pathtoebook = pathtoebook
book.highlights = highlights
book.stored_files = {}
book.is_complete = True
v'delete book.manifest["metadata"]'
v'delete book.manifest["last_read_positions"]'
view.display_book(book, initial_position)
else:
show_error(_('Could not open book'), _(
'Failed to load book manifest, click "Show details" for more info'),
xhr.error_html or None)
class SessionData:
def __init__(self, prefs):
defaults = session_defaults()
self.data = {k: defaults[k] if prefs[k] is undefined else prefs[k] for k in defaults}
def get(self, key, defval):
ans = self.data[key]
if ans is undefined or ans is None:
if defval is undefined:
defval = None
return defval
return ans
def set(self, key, val):
if val is None:
self.data[key] = session_defaults()[key]
else:
self.data[key] = val
to_python.set_session_data(key, val)
def clear(self):
defaults = session_defaults()
self.data = {k: defaults[k] for k in defaults}
to_python.set_session_data('*', None)
class LocalStorage:
def __init__(self, data):
self.data = data
def get(self, key, defval):
ans = self.data[key]
if ans is undefined or ans is None:
if defval is undefined:
defval = None
return defval
return ans
def set(self, key, val):
self.data[key] = val
to_python.set_local_storage(key, val)
def clear(self):
self.data = {}
to_python.set_local_storage('*', None)
def create_session_data(prefs, local_storage_data):
sd = SessionData(prefs)
set_session_data(sd)
local_storage.storage = LocalStorage(local_storage_data)
@from_python
def create_view(prefs, local_storage, field_metadata, ui_data):
nonlocal view
set_system_colors(ui_data.system_colors)
runtime.all_font_families = ui_data.all_font_families
library_data.field_metadata = field_metadata
document.documentElement.style.fontFamily = f'"{ui_data.ui_font_family}", sans-serif'
document.documentElement.style.fontSize = ui_data.ui_font_sz
runtime.QT_VERSION = ui_data.QT_VERSION
if view is None:
create_session_data(prefs, local_storage)
view = View(document.getElementById('view'))
window.addEventListener('resize', debounce(view.on_resize.bind(self), 250))
to_python.view_created({'default_color_schemes': default_color_schemes})
if ui_data.show_home_page_on_ready:
view.overlay.open_book(False)
@from_python
def set_system_palette(system_colors):
set_system_colors(system_colors)
if view:
view.update_color_scheme()
@from_python
def highlight_action(uuid, which):
if view:
view.highlight_action(uuid, which)
@from_python
def generic_action(which, data):
if which is 'set-notes-in-highlight':
view.set_notes_for_highlight(data.uuid, data.notes or '')
@from_python
def show_home_page():
view.overlay.open_book(False)
@from_python
def start_book_load(key, initial_position, pathtoebook, highlights):
xhr = ajax('manifest', manifest_received.bind(None, key, initial_position, pathtoebook, highlights), ok_code=0)
xhr.responseType = 'json'
xhr.send()
@from_python
def goto_toc_node(node_id):
view.goto_toc_node(node_id)
@from_python
def goto_cfi(cfi):
view.goto_cfi(cfi)
@from_python
def full_screen_state_changed(viewer_in_full_screen):
runtime.viewer_in_full_screen = viewer_in_full_screen
@from_python
def get_current_cfi(request_id):
view.get_current_cfi(request_id, ui_operations.report_cfi)
@from_python
def goto_frac(frac):
if view:
view.goto_frac(frac)
@from_python
def background_image_changed(img_id):
img = document.getElementById(img_id)
if img:
img.src = READER_BACKGROUND_URL + '?' + Date().getTime()
@from_python
def trigger_shortcut(which):
if view:
view.on_handle_shortcut({'name': which})
@from_python
def show_search_result(sr):
if view:
view.show_search_result(sr)
@from_python
def prepare_for_close():
if view:
view.prepare_for_close()
else:
ui_operations.close_prep_finished(None)
@from_python
def viewer_font_size_changed():
if view:
view.viewer_font_size_changed()
def onerror(msg, script_url, line_number, column_number, error_object):
if not error_object:
# cross domain error
return False
fname = script_url.rpartition('/')[-1] or script_url
msg += '<br><span style="font-size:smaller">' + 'Error at {}:{}:{}'.format(fname, line_number, column_number or '') + '</span>'
details = ''
console.log(error_object)
details = traceback.format_exception(error_object).join('')
show_error(_('Unhandled error'), msg, details)
return True
if window is window.top:
# main
TRANSLATIONS_DATA = v'__TRANSLATIONS_DATA__'
if TRANSLATIONS_DATA:
install(TRANSLATIONS_DATA)
add_standalone_viewer_shortcuts()
ui_operations.get_file = get_file
ui_operations.get_mathjax_files = get_mathjax_files
ui_operations.update_url_state = update_url_state
ui_operations.show_error = show_error
ui_operations.redisplay_book = def():
view.redisplay_book()
ui_operations.reload_book = def():
to_python.reload_book()
ui_operations.forward_gesture = def(gesture):
view.forward_gesture(gesture)
ui_operations.update_color_scheme = def():
view.update_color_scheme()
ui_operations.update_font_size = def():
view.update_font_size()
ui_operations.focus_iframe = def():
view.focus_iframe()
ui_operations.goto_cfi = def(cfi):
return view.goto_cfi(cfi)
ui_operations.goto_frac = def(frac):
return view.goto_frac(frac)
ui_operations.goto_book_position = def(bpos):
return view.goto_book_position(bpos)
ui_operations.goto_reference = def(ref):
return view.goto_reference(ref)
ui_operations.toggle_toc = def():
to_python.toggle_toc()
ui_operations.toggle_bookmarks = def():
to_python.toggle_bookmarks()
ui_operations.toggle_highlights = def():
to_python.toggle_highlights()
ui_operations.new_bookmark = def(request_id, data):
if request_id is 'new-bookmark':
to_python.new_bookmark(data)
ui_operations.toggle_inspector = def():
to_python.toggle_inspector()
ui_operations.content_file_changed = def(name):
to_python.content_file_changed(name)
ui_operations.show_search = def(text):
to_python.show_search(text)
ui_operations.find_next = def(previous):
to_python.find_next(previous)
ui_operations.reset_interface = def():
sd = get_session_data()
defaults = session_defaults()
m = sd.get('standalone_misc_settings', {})
v'delete m.show_actions_toolbar'
sd.set('standalone_misc_settings', m)
sd.set('book_scrollbar', False)
view.book_scrollbar.apply_visibility()
sd.set('header', defaults.header)
sd.set('footer', defaults.footer)
view.update_header_footer()
to_python.reset_interface()
ui_operations.open_url = def(url):
to_python.open_url(url)
ui_operations.quit = def():
to_python.quit()
ui_operations.toggle_lookup = def(force_show):
to_python.toggle_lookup(v'!!force_show')
ui_operations.selection_changed = def(selected_text, annot_id):
to_python.selection_changed(selected_text, annot_id or None)
ui_operations.update_current_toc_nodes = def(current_node_id, top_level_node_id):
to_python.update_current_toc_nodes(current_node_id, top_level_node_id)
ui_operations.toggle_full_screen = def():
to_python.toggle_full_screen()
ui_operations.report_cfi = def(request_id, data):
to_python.report_cfi(request_id, data)
ui_operations.ask_for_open = def(path):
to_python.ask_for_open(path)
ui_operations.copy_selection = def(text):
to_python.copy_selection(text or None)
ui_operations.view_image = def(name):
to_python.view_image(name)
ui_operations.copy_image = def(name):
to_python.copy_image(name)
ui_operations.change_background_image = def(img_id):
to_python.change_background_image(img_id)
ui_operations.quit = def():
to_python.quit()
ui_operations.overlay_visibility_changed = def(visible):
to_python.overlay_visibility_changed(visible)
ui_operations.reference_mode_changed = def(enabled):
to_python.reference_mode_changed(enabled)
ui_operations.show_loading_message = def(msg):
to_python.show_loading_message(msg)
ui_operations.export_shortcut_map = def(smap):
to_python.export_shortcut_map(smap)
ui_operations.print_book = def():
to_python.print_book()
ui_operations.clear_history = def():
to_python.clear_history()
ui_operations.customize_toolbar = def():
to_python.customize_toolbar()
ui_operations.autoscroll_state_changed = def(active):
to_python.autoscroll_state_changed(active)
ui_operations.search_result_not_found = def(sr):
to_python.search_result_not_found(sr)
ui_operations.scrollbar_context_menu = def(x, y, frac):
to_python.scrollbar_context_menu(x, y, frac)
ui_operations.close_prep_finished = def(cfi):
to_python.close_prep_finished(cfi)
ui_operations.highlights_changed = def(highlights):
to_python.highlights_changed(highlights)
document.body.appendChild(E.div(id='view'))
window.onerror = onerror
create_modal_container()
document.head.appendChild(E.style(css_for_variables() + '\n\n' + get_widget_css()))
set_css(document.body, background_color=get_color('window-background'), color=get_color('window-foreground'))
setTimeout(def():
window.onpopstate = on_pop_state
, 0) # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load
else:
# iframe
div = document.getElementById('calibre-viewer-footnote-iframe')
if div:
footnotes_main()
else:
iframe_main()
add_standalone_viewer_shortcuts()