mirror of
git://github.com/kovidgoyal/calibre.git
synced 2025-12-06 08:15:41 +01:00
664 lines
28 KiB
Text
664 lines
28 KiB
Text
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
# globals: __RENDER_VERSION__
|
|
from __python__ import hash_literals
|
|
|
|
from elementmaker import E
|
|
|
|
import traceback
|
|
from ajax import ajax, ajax_send
|
|
from book_list.constants import read_book_container_id
|
|
from book_list.library_data import current_library_id, library_data
|
|
from book_list.router import home, push_state, read_book_mode, update_window_title
|
|
from book_list.ui import show_panel
|
|
from dom import clear
|
|
from gettext import gettext as _
|
|
from modals import create_simple_dialog_markup, error_dialog
|
|
from read_book.db import get_db
|
|
from read_book.globals import ui_operations
|
|
from read_book.tts import Client
|
|
from read_book.view import View
|
|
from session import get_interface_data
|
|
from utils import debounce, full_screen_element, human_readable, request_full_screen
|
|
from widgets import create_button
|
|
|
|
RENDER_VERSION = __RENDER_VERSION__
|
|
MATHJAX_VERSION = "__MATHJAX_VERSION__"
|
|
|
|
class ReadUI:
|
|
|
|
def __init__(self):
|
|
self.base_url_data = {}
|
|
self.current_metadata = {'title': _('Unknown book')}
|
|
self.current_book_id = None
|
|
self.manifest_xhr = None
|
|
self.pending_load = None
|
|
self.downloads_in_progress = []
|
|
self.progress_id = 'book-load-progress'
|
|
self.display_id = 'book-iframe-container'
|
|
self.error_id = 'book-global-error-container'
|
|
self.stacked_widgets = [self.progress_id, self.display_id, self.error_id]
|
|
container = document.getElementById(read_book_container_id)
|
|
|
|
container.appendChild(E.div(
|
|
id=self.progress_id, style='display:none; text-align: center',
|
|
E.h3(style='margin-top:30vh; margin-bottom: 1ex;'),
|
|
E.progress(style='margin: 1ex'),
|
|
E.div(style='margin: 1ex')
|
|
))
|
|
|
|
container.appendChild(E.div(
|
|
id=self.error_id, style='display:none;',
|
|
))
|
|
|
|
container.appendChild(E.div(
|
|
id=self.display_id, style='display:none',
|
|
))
|
|
self.view = View(container.lastChild)
|
|
self.tts_client = Client()
|
|
self.windows_to_listen_for_messages_from = []
|
|
window.addEventListener('resize', debounce(self.on_resize.bind(self), 250))
|
|
window.addEventListener('message', self.message_from_other_window.bind(self))
|
|
self.db = get_db(self.db_initialized.bind(self), self.show_error.bind(self))
|
|
ui_operations.get_file = self.db.get_file
|
|
ui_operations.get_mathjax_files = self.db.get_mathjax_files
|
|
ui_operations.update_url_state = self.update_url_state.bind(self)
|
|
ui_operations.update_last_read_time = self.db.update_last_read_time
|
|
ui_operations.show_error = self.show_error.bind(self)
|
|
ui_operations.update_reading_rates = self.update_reading_rates.bind(self)
|
|
ui_operations.redisplay_book = self.redisplay_book.bind(self)
|
|
ui_operations.reload_book = self.reload_book.bind(self)
|
|
ui_operations.forward_gesture = self.forward_gesture.bind(self)
|
|
ui_operations.update_color_scheme = self.update_color_scheme.bind(self)
|
|
ui_operations.update_font_size = self.update_font_size.bind(self)
|
|
ui_operations.goto_cfi = self.goto_cfi.bind(self)
|
|
ui_operations.goto_frac = self.goto_frac.bind(self)
|
|
ui_operations.goto_book_position = self.goto_book_position.bind(self)
|
|
ui_operations.goto_reference = self.goto_reference.bind(self)
|
|
ui_operations.delete_book = self.delete_book.bind(self)
|
|
ui_operations.focus_iframe = self.focus_iframe.bind(self)
|
|
ui_operations.toggle_toc = self.toggle_toc.bind(self)
|
|
ui_operations.toggle_full_screen = self.toggle_full_screen.bind(self)
|
|
ui_operations.annots_changed = self.annots_changed.bind(self)
|
|
ui_operations.annotations_synced = self.annotations_synced.bind(self)
|
|
ui_operations.wait_for_messages_from = self.wait_for_messages_from.bind(self)
|
|
ui_operations.stop_waiting_for_messages_from = self.stop_waiting_for_messages_from.bind(self)
|
|
ui_operations.update_metadata = self.update_metadata.bind(self)
|
|
ui_operations.close_book = self.close_book.bind(self)
|
|
ui_operations.copy_image = self.copy_image.bind(self)
|
|
ui_operations.view_image = self.view_image.bind(self)
|
|
ui_operations.speak_simple_text = self.speak_simple_text.bind(self)
|
|
ui_operations.tts = self.tts.bind(self)
|
|
ui_operations.search_result_discovered = self.view.search_overlay.search_result_discovered
|
|
ui_operations.search_result_not_found = self.view.search_overlay.search_result_not_found
|
|
ui_operations.find_next = self.view.search_overlay.find_next
|
|
ui_operations.open_url = def(url):
|
|
window.open(url, '_blank')
|
|
ui_operations.show_help = def(which):
|
|
if which is 'viewer':
|
|
path = '/viewer.html'
|
|
else:
|
|
return
|
|
if get_interface_data().lang_code_for_user_manual:
|
|
path = f'/{get_interface_data().lang_code_for_user_manual}{path}'
|
|
url = 'https://manual.calibre-ebook.com' + path
|
|
window.open(url, '_blank')
|
|
|
|
ui_operations.copy_selection = def(text, html):
|
|
# try using document.execCommand which on chrome allows the
|
|
# copy on non-secure origin if this is close to a user
|
|
# interaction event
|
|
active_elem = document.activeElement
|
|
ta = document.createElement("textarea")
|
|
ta.value = text
|
|
ta.style.position = 'absolute'
|
|
ta.style.top = window.innerHeight + 'px'
|
|
ta.style.left = window.innerWidth + 'px'
|
|
document.body.appendChild(ta)
|
|
ta.focus()
|
|
ta.select()
|
|
ok = False
|
|
try:
|
|
ok = document.execCommand('copy')
|
|
except:
|
|
pass
|
|
document.body.removeChild(ta)
|
|
if active_elem:
|
|
active_elem.focus()
|
|
if ok:
|
|
return
|
|
if not window.navigator.clipboard:
|
|
return error_dialog(_('No clipboard access'), _(
|
|
'Your browser requires you to access the Content server over an HTTPS connection'
|
|
' to be able to copy to clipboard.'))
|
|
window.navigator.clipboard.writeText(text or '').then(def (): pass;, def():
|
|
error_dialog(_('Could not copy to clipboard'), _('No permission to write to clipboard'))
|
|
)
|
|
|
|
def on_resize(self):
|
|
self.view.on_resize()
|
|
|
|
def show_stack(self, name):
|
|
ans = None
|
|
for w in self.stacked_widgets:
|
|
d = document.getElementById(w)
|
|
v = 'none'
|
|
if name is w:
|
|
ans = d
|
|
v = 'block'
|
|
d.style.display = v
|
|
return ans
|
|
|
|
def show_error(self, title, msg, details):
|
|
div = self.show_stack(self.error_id)
|
|
clear(div)
|
|
if self.current_metadata:
|
|
book = self.current_metadata.title
|
|
elif self.current_book_id:
|
|
book = _('Book id #{}').format(self.current_book_id)
|
|
else:
|
|
book = _('book')
|
|
|
|
div.appendChild(E.div(style='padding: 2ex 2rem; display:table; margin: auto'))
|
|
div = div.lastChild
|
|
dp = E.div()
|
|
create_simple_dialog_markup(title, _('Could not open {}. {}').format(book, msg), details, 'bug', '', dp)
|
|
div.appendChild(dp)
|
|
div.appendChild(E.div(
|
|
style='margin-top: 1ex; padding-top: 1ex; border-top: solid 1px currentColor',
|
|
_('Go back to:'), E.br(), E.br(),
|
|
create_button(_('Home'), 'home', def(): home(True);),
|
|
))
|
|
q = {}
|
|
if self.current_book_id:
|
|
q.book_id = self.current_book_id + ''
|
|
q.close_action = 'home'
|
|
div.lastChild.appendChild(E.span(
|
|
'\xa0',
|
|
create_button(_('Book list'), 'library', def(): show_panel('book_list', q, True);),
|
|
))
|
|
if self.current_book_id:
|
|
q.close_action = 'book_list'
|
|
div.lastChild.appendChild(E.span(
|
|
'\xa0',
|
|
create_button(_('Book details'), 'book', def(): show_panel('book_details', q, True);),
|
|
))
|
|
|
|
def init_ui(self):
|
|
div = self.show_stack(self.progress_id)
|
|
if self.current_metadata:
|
|
div.firstChild.textContent = _(
|
|
'Downloading {0} for offline reading, please wait...').format(self.current_metadata.title)
|
|
else:
|
|
div.firstChild.textContent = ''
|
|
pr = div.firstChild.nextSibling
|
|
pr.removeAttribute('value'), pr.removeAttribute('max')
|
|
div.lastChild.textContent = _('Downloading book manifest...')
|
|
|
|
def show_progress_message(self, msg):
|
|
div = document.getElementById(self.progress_id)
|
|
div.lastChild.textContent = msg or ''
|
|
|
|
def close_book(self):
|
|
self.base_url_data = {}
|
|
|
|
def copy_image(self, image_file_name):
|
|
if not self.view?.book:
|
|
return
|
|
ui_operations.get_file(
|
|
self.view.book, image_file_name, def(blob, name, mimetype):
|
|
try:
|
|
window.navigator.clipboard.write([new ClipboardItem({mimetype: blob})]) # noqa
|
|
except Exception as err:
|
|
error_dialog(_('Could not copy image'), str(err))
|
|
)
|
|
|
|
def view_image(self, image_file_name):
|
|
if not self.view?.book:
|
|
return
|
|
ui_operations.get_file(
|
|
self.view.book, image_file_name, def(blob, name, mimetype):
|
|
url = window.URL.createObjectURL(blob)
|
|
window.open(url)
|
|
)
|
|
|
|
def load_book(self, library_id, book_id, fmt, metadata, force_reload):
|
|
self.base_url_data = {'library_id': library_id, 'book_id':book_id, 'fmt':fmt}
|
|
if not self.db.initialized:
|
|
self.pending_load = [book_id, fmt, metadata, force_reload]
|
|
return
|
|
if not self.db.is_ok:
|
|
self.show_error(_('Cannot read books'), self.db.initialize_error_msg)
|
|
return
|
|
self.start_load(book_id, fmt, metadata, force_reload)
|
|
|
|
def reload_book(self):
|
|
library_id, book_id, fmt = self.base_url_data.library_id, self.base_url_data.book_id, self.base_url_data.fmt
|
|
metadata = self.metadata or library_data.metadata[book_id]
|
|
self.load_book(library_id, book_id, fmt, metadata, True)
|
|
|
|
def redisplay_book(self):
|
|
self.view.redisplay_book()
|
|
|
|
def forward_gesture(self, gesture):
|
|
self.view.forward_gesture(gesture)
|
|
|
|
def update_font_size(self):
|
|
self.view.update_font_size()
|
|
|
|
def goto_cfi(self, bookpos):
|
|
return self.view.goto_cfi(bookpos)
|
|
|
|
def goto_frac(self, frac):
|
|
return self.view.goto_frac(frac)
|
|
|
|
def goto_book_position(self, bpos):
|
|
return self.view.goto_book_position(bpos)
|
|
|
|
def goto_reference(self, ref):
|
|
return self.view.goto_reference(ref)
|
|
|
|
def delete_book(self, book, proceed):
|
|
self.db.delete_book(book, proceed)
|
|
|
|
def focus_iframe(self):
|
|
self.view.focus_iframe()
|
|
|
|
def toggle_toc(self):
|
|
self.view.overlay.show_toc()
|
|
|
|
def toggle_full_screen(self):
|
|
if full_screen_element():
|
|
document.exitFullscreen()
|
|
else:
|
|
request_full_screen(document.documentElement)
|
|
|
|
def update_color_scheme(self):
|
|
self.view.update_color_scheme()
|
|
|
|
def annots_changed(self, amap):
|
|
library_id = self.base_url_data.library_id
|
|
book_id = self.base_url_data.book_id
|
|
fmt = self.base_url_data.fmt
|
|
self.db.update_annotations_data_from_key(library_id, book_id, fmt, {'annotations_map': amap})
|
|
ajax_send(f'book-update-annotations/{library_id}/{book_id}/{fmt}', amap, def (): pass;)
|
|
|
|
def annotations_synced(self, amap):
|
|
library_id = self.base_url_data.library_id
|
|
book_id = self.base_url_data.book_id
|
|
fmt = self.base_url_data.fmt
|
|
self.db.update_annotations_data_from_key(library_id, book_id, fmt, {'annotations_map': amap})
|
|
|
|
@property
|
|
def url_data(self):
|
|
ans = {'library_id':self.base_url_data.library_id, 'book_id':self.base_url_data.book_id, 'fmt': self.base_url_data.fmt}
|
|
bookpos = self.view.currently_showing.bookpos
|
|
if bookpos:
|
|
ans.bookpos = bookpos
|
|
return ans
|
|
|
|
def db_initialized(self):
|
|
if not self.db.is_ok:
|
|
error_dialog(_('Could not initialize database'), self.db.initialize_error_msg)
|
|
return
|
|
if self.pending_load is not None:
|
|
pl, self.pending_load = self.pending_load, None
|
|
if self.db.initialize_error_msg:
|
|
self.show_error(_('Failed to initialize IndexedDB'), self.db.initialize_error_msg)
|
|
else:
|
|
self.start_load(*pl)
|
|
|
|
def start_load(self, book_id, fmt, metadata, force_reload):
|
|
self.current_book_id = book_id
|
|
metadata = metadata or library_data.metadata[book_id]
|
|
self.current_metadata = metadata or {'title':_('Book id #') + book_id}
|
|
update_window_title('', self.current_metadata.title)
|
|
self.init_ui()
|
|
if jstype(self.db) is 'string':
|
|
self.show_error(_('Cannot read book'), self.db)
|
|
return
|
|
self.db.get_book(current_library_id(), book_id, fmt, metadata, self.got_book.bind(self, force_reload))
|
|
|
|
def got_book(self, force_reload, book):
|
|
if not book.manifest or book.manifest.version is not RENDER_VERSION or not book.is_complete:
|
|
# We re-download the manifest when the book is not complete to ensure we have the
|
|
# correct manifest, even though doing so is not strictly necessary
|
|
self.get_manifest(book, force_reload)
|
|
else:
|
|
self.display_book(book)
|
|
|
|
def get_manifest(self, book, force_reload):
|
|
library_id, book_id, fmt = book.key
|
|
if self.manifest_xhr:
|
|
self.manifest_xhr.abort()
|
|
query = {'library_id': library_id}
|
|
if force_reload:
|
|
query.force_reload = '1'
|
|
self.manifest_xhr = ajax(('book-manifest/' + encodeURIComponent(book_id) + '/' + encodeURIComponent(fmt)),
|
|
self.got_manifest.bind(self, book), query=query)
|
|
self.manifest_xhr.send()
|
|
|
|
def got_manifest(self, book, end_type, xhr, ev):
|
|
self.manifest_xhr = None
|
|
if end_type is 'abort':
|
|
return
|
|
if end_type is not 'load':
|
|
return self.show_error(_('Failed to load book manifest'),
|
|
_('The book manifest failed to load, click "Show details" for more information.').format(title=self.current_metadata.title),
|
|
xhr.error_html)
|
|
try:
|
|
manifest = JSON.parse(xhr.responseText)
|
|
except Exception:
|
|
return self.show_error(_('Failed to load book manifest'),
|
|
_('The manifest for {title} is not valid').format(title=self.current_metadata.title),
|
|
traceback.format_exc())
|
|
if manifest.version is not undefined:
|
|
if manifest.version is not RENDER_VERSION:
|
|
print('calibre upgraded: RENDER_VERSION={} manifest.version={}'.format(RENDER_VERSION, manifest.version))
|
|
return self.show_error(_('calibre upgraded!'), _(
|
|
'A newer version of calibre is available, please click the Reload button in your browser.'))
|
|
self.current_metadata = manifest.metadata
|
|
self.db.save_manifest(book, manifest, self.download_book.bind(self, book))
|
|
return
|
|
# Book is still being processed
|
|
msg = _('Downloading book manifest...')
|
|
if manifest.job_status is 'finished':
|
|
if manifest.aborted:
|
|
return self.show_error(_('Failed to prepare book for reading'), _('Preparation of book for reading was aborted because it took too long'))
|
|
if manifest.traceback:
|
|
return self.show_error(_('Failed to prepare book for reading'), _(
|
|
'There was an error processing the book, click "Show details" for more information'), manifest.traceback or '')
|
|
elif manifest.job_status is 'waiting':
|
|
msg = _('Book is queued for processing on the server...')
|
|
elif manifest.job_status is 'running':
|
|
msg = _('Book is being prepared for reading on the server...')
|
|
self.show_progress_message(msg)
|
|
setTimeout(self.get_manifest.bind(self, book), 100)
|
|
|
|
def download_book(self, book):
|
|
files = book.manifest.files
|
|
total = 0
|
|
cover_total_updated = False
|
|
for name in files:
|
|
total += files[name].size
|
|
files_left = set(book.manifest.files)
|
|
failed_files = []
|
|
for xhr in self.downloads_in_progress:
|
|
xhr.abort()
|
|
self.downloads_in_progress = []
|
|
progress = document.getElementById(self.progress_id)
|
|
pbar = progress.firstChild.nextSibling
|
|
library_id, book_id, fmt = book.key
|
|
base_path = 'book-file/{}/{}/{}/{}/'.format(encodeURIComponent(book_id), encodeURIComponent(fmt),
|
|
encodeURIComponent(book.manifest.book_hash.size), encodeURIComponent(book.manifest.book_hash.mtime))
|
|
query = {'library_id': library_id}
|
|
progress_track = {}
|
|
pbar.setAttribute('max', total + '')
|
|
raster_cover_name = book.manifest.raster_cover_name
|
|
if not raster_cover_name:
|
|
nnum = 1
|
|
base = 'raster-cover-image-{}.jpg'
|
|
inserted_name = base.format(nnum)
|
|
while inserted_name in files_left:
|
|
nnum += 1
|
|
inserted_name = base.format(nnum)
|
|
raster_cover_name = inserted_name
|
|
files_left.add(raster_cover_name)
|
|
|
|
raster_cover_size = 0
|
|
|
|
def update_progress():
|
|
x = 0
|
|
for name in progress_track:
|
|
x += progress_track[name]
|
|
pbar.setAttribute('value', x + '')
|
|
if x is total:
|
|
msg = _('Downloaded {}, saving to disk. This may take a few seconds...').format(human_readable(total))
|
|
else:
|
|
msg = _('Downloaded {0}, {1} left').format(human_readable(x), human_readable(total - x))
|
|
progress.lastChild.textContent = msg
|
|
|
|
def show_failure():
|
|
det = ['<h4>{}</h4><div>{}</div><hr>'.format(fname, err_html) for fname, err_html in failed_files].join('')
|
|
self.show_error(_('Could not download book'), _(
|
|
'Failed to download some book data, click "Show details" for more information'), det)
|
|
|
|
def on_stored(err):
|
|
files_left.discard(this)
|
|
if err:
|
|
failed_files.append([this, err])
|
|
if len(files_left):
|
|
return
|
|
if failed_files.length:
|
|
return show_failure()
|
|
self.db.finish_book(book, self.display_book.bind(self, book))
|
|
|
|
def on_complete(end_type, xhr, ev):
|
|
self.downloads_in_progress.remove(xhr)
|
|
progress_track[this] = raster_cover_size if this is raster_cover_name else files[this].size
|
|
update_progress()
|
|
if len(queued):
|
|
for fname in queued:
|
|
start_download(fname, base_path + encodeURIComponent(fname).replace(/%2[fF]/g, '/'))
|
|
queued.discard(fname)
|
|
break
|
|
if end_type is 'abort':
|
|
files_left.discard(this)
|
|
return
|
|
if end_type is 'load':
|
|
self.db.store_file(book, this, xhr, on_stored.bind(this), this is raster_cover_name)
|
|
else:
|
|
failed_files.append([this, xhr.error_html])
|
|
files_left.discard(this)
|
|
if not len(files_left):
|
|
show_failure()
|
|
|
|
def on_progress(loaded, ftotal):
|
|
nonlocal total, cover_total_updated, raster_cover_size
|
|
if this is raster_cover_name and not cover_total_updated:
|
|
raster_cover_size = ftotal
|
|
cover_total_updated = True
|
|
total = total - (files[raster_cover_name]?.size or 0) + raster_cover_size
|
|
pbar.setAttribute('max', total + '')
|
|
progress_track[this] = loaded
|
|
update_progress()
|
|
|
|
def start_download(fname, path):
|
|
xhr = ajax(path, on_complete.bind(fname), on_progress=on_progress.bind(fname), query=query, progress_totals_needed=fname is raster_cover_name)
|
|
xhr.responseType = 'text'
|
|
if not book.manifest.files[fname]?.is_virtualized:
|
|
xhr.responseType = 'blob' if self.db.supports_blobs else 'arraybuffer'
|
|
xhr.send()
|
|
self.downloads_in_progress.append(xhr)
|
|
|
|
if raster_cover_name:
|
|
start_download(raster_cover_name, 'get/cover/' + book_id + '/' + encodeURIComponent(library_id))
|
|
|
|
count = 0
|
|
queued = set()
|
|
for fname in files_left:
|
|
if fname is not raster_cover_name:
|
|
count += 1
|
|
# Chrome starts killing AJAX requests if there are too many in flight, unlike Firefox
|
|
# which is smart enough to queue them
|
|
if count < 20:
|
|
start_download(fname, base_path + encodeURIComponent(fname).replace(/%2[fF]/g, '/'))
|
|
else:
|
|
queued.add(fname)
|
|
|
|
def ensure_maths(self, proceed):
|
|
self.db.get_mathjax_info(def(mathjax_info):
|
|
if mathjax_info.version is MATHJAX_VERSION:
|
|
return proceed()
|
|
print('Upgrading MathJax, previous version:', mathjax_info.version)
|
|
self.db.clear_mathjax(def():
|
|
self.get_mathjax_manifest(mathjax_info, proceed)
|
|
)
|
|
)
|
|
|
|
def get_mathjax_manifest(self, mathjax_info, proceed):
|
|
ajax('mathjax', def(end_type, xhr, event):
|
|
if end_type is 'abort':
|
|
return
|
|
if end_type is not 'load':
|
|
return self.show_error(_('Failed to load MathJax manifest'),
|
|
_('The MathJax manifest failed to load, click "Show details" for more information.').format(title=self.current_metadata.title),
|
|
xhr.error_html)
|
|
try:
|
|
manifest = JSON.parse(xhr.responseText)
|
|
except Exception:
|
|
return self.show_error(_('Failed to load MathJax manifest'),
|
|
_('The MathJax manifest is not valid'), traceback.format_exc())
|
|
if manifest.etag is not MATHJAX_VERSION:
|
|
print('calibre upgraded: MATHJAX_VERSION={} manifest.etag={}'.format(MATHJAX_VERSION, manifest.etag))
|
|
return self.show_error(_('calibre upgraded!'), _(
|
|
'A newer version of calibre is available, please click the Reload button in your browser.'))
|
|
mathjax_info.version = manifest.etag
|
|
mathjax_info.files = manifest.files
|
|
self.download_mathjax(mathjax_info, proceed)
|
|
).send()
|
|
|
|
def download_mathjax(self, mathjax_info, proceed):
|
|
files = mathjax_info.files
|
|
total = 0
|
|
progress_track = {}
|
|
files_left = set()
|
|
failed_files = []
|
|
for key in files:
|
|
total += files[key]
|
|
progress_track[key] = 0
|
|
files_left.add(key)
|
|
progress = document.getElementById(self.progress_id)
|
|
progress.firstChild.textContent = _(
|
|
'Downloading MathJax to render mathematics in this book...')
|
|
pbar = progress.firstChild.nextSibling
|
|
pbar.setAttribute('max', total + '')
|
|
for xhr in self.downloads_in_progress:
|
|
xhr.abort()
|
|
self.downloads_in_progress = []
|
|
|
|
def update_progress():
|
|
x = 0
|
|
for name in progress_track:
|
|
x += progress_track[name]
|
|
pbar.setAttribute('value', x + '')
|
|
if x is total:
|
|
msg = _('Downloaded {}, saving to disk. This may take a few seconds...').format(human_readable(total))
|
|
else:
|
|
msg = _('Downloaded {0}, {1} left').format(human_readable(x), human_readable(total - x))
|
|
progress.lastChild.textContent = msg
|
|
|
|
def on_progress(loaded, ftotal):
|
|
progress_track[this] = loaded
|
|
update_progress()
|
|
|
|
def show_failure():
|
|
det = ['<h4>{}</h4><div>{}</div><hr>'.format(fname, err_html) for fname, err_html in failed_files].join('')
|
|
self.show_error(_('Could not download MathJax'), _(
|
|
'Failed to download some MathJax data, click "Show details" for more information'), det)
|
|
|
|
def on_complete(end_type, xhr, ev):
|
|
self.downloads_in_progress.remove(xhr)
|
|
progress_track[this] = files[this]
|
|
update_progress()
|
|
if end_type is 'abort':
|
|
files_left.discard(this)
|
|
return
|
|
if end_type is 'load':
|
|
self.db.store_mathjax_file(this, xhr, on_stored.bind(this))
|
|
else:
|
|
failed_files.append([this, xhr.error_html])
|
|
files_left.discard(this)
|
|
if not len(files_left):
|
|
show_failure()
|
|
|
|
def on_stored(err):
|
|
files_left.discard(this)
|
|
if err:
|
|
failed_files.append([this, err])
|
|
if len(files_left):
|
|
return
|
|
if failed_files.length:
|
|
return show_failure()
|
|
self.db.finish_mathjax(mathjax_info, proceed)
|
|
|
|
def start_download(name):
|
|
path = 'mathjax/' + name
|
|
xhr = ajax(path, on_complete.bind(name), on_progress=on_progress.bind(name), progress_totals_needed=False)
|
|
xhr.responseType = 'blob' if self.db.supports_blobs else 'arraybuffer'
|
|
xhr.send()
|
|
self.downloads_in_progress.push(xhr)
|
|
|
|
for fname in files_left:
|
|
start_download(fname)
|
|
|
|
def display_book(self, book):
|
|
if book.manifest.has_maths:
|
|
self.ensure_maths(self.display_book_stage2.bind(self, book))
|
|
else:
|
|
self.display_book_stage2(book)
|
|
|
|
def display_book_stage2(self, book):
|
|
self.current_metadata = book.metadata
|
|
update_window_title('', self.current_metadata.title)
|
|
self.show_stack(self.display_id)
|
|
self.view.display_book(book)
|
|
|
|
def apply_url_state(self, current_query):
|
|
same = True
|
|
current_state = self.url_data
|
|
same = current_query.library_id is current_state.library_id and str(current_query.book_id) is str(current_state.book_id) and current_query.fmt is current_state.fmt
|
|
self.view.overlay.hide()
|
|
window.scrollTo(0, 0) # Ensure we are at the top of the window
|
|
if same:
|
|
if current_state.bookpos is not current_query.bookpos and current_query.bookpos:
|
|
self.view.goto_cfi(current_query.bookpos)
|
|
else:
|
|
self.load_book(current_query.library_id, int(current_query.book_id), current_query.fmt, library_data.metadata[current_query.book_id])
|
|
|
|
def update_url_state(self, replace):
|
|
push_state(self.url_data, replace=replace, mode=read_book_mode)
|
|
|
|
def update_metadata(self, book, metadata):
|
|
self.db.update_metadata(book, metadata)
|
|
|
|
def wait_for_messages_from(self, w, callback):
|
|
self.windows_to_listen_for_messages_from.push(v'[w, callback]')
|
|
|
|
def stop_waiting_for_messages_from(self, w):
|
|
self.windows_to_listen_for_messages_from = [x for x in self.windows_to_listen_for_messages_from if x[0] is not w]
|
|
|
|
def message_from_other_window(self, msg):
|
|
if not self.windows_to_listen_for_messages_from.length:
|
|
return
|
|
old = self.windows_to_listen_for_messages_from
|
|
for x in old:
|
|
w, callback = x
|
|
if w is msg.source:
|
|
callback(msg)
|
|
|
|
def check_for_speech_capability(self):
|
|
if not window.speechSynthesis:
|
|
error_dialog(_('No speech support'), _(
|
|
'Your browser does not have support for Text-to-Speech'))
|
|
return False
|
|
return True
|
|
|
|
def speak_simple_text(self, text):
|
|
if not self.check_for_speech_capability():
|
|
return
|
|
self.tts_client.speak_simple_text(text)
|
|
|
|
def tts(self, event, data):
|
|
if not self.check_for_speech_capability():
|
|
return
|
|
if event is 'play':
|
|
self.tts_client.speak_marked_text(data.marked_text, self.view.read_aloud.handle_tts_event)
|
|
else:
|
|
getattr(self.tts_client, event)()
|
|
|
|
def update_reading_rates(self, rates):
|
|
if not self.view?.book:
|
|
return
|
|
book = self.view.book
|
|
self.db.save_reading_rates(book, rates)
|