# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal from __python__ import bound_methods, hash_literals from elementmaker import E from gettext import gettext as _ from book_list.globals import get_read_ui from book_list.library_data import sync_library_books from book_list.router import home from book_list.theme import get_color from dom import add_extra_css, build_rule, clear, set_css, svgicon, unique_id from modals import error_dialog from utils import full_screen_element, request_full_screen, safe_set_inner_html, is_ios from read_book.goto import create_goto_panel from read_book.prefs.font_size import create_font_size_panel from read_book.prefs.main import create_prefs_panel from read_book.toc import create_toc_panel from read_book.word_actions import create_word_actions_panel from session import get_device_uuid from widgets import create_button, create_spinner class LoadingMessage: # {{{ def __init__(self, msg): self.msg = msg or '' def show(self, container): self.container_id = container.getAttribute('id') container.style.backgroundColor = get_color('window-background') container.appendChild( E.div( style='text-align:center', E.div(create_spinner('100px', '100px')), E.h2() )) safe_set_inner_html(container.firstChild.lastChild, self.msg) set_css(container.firstChild, position='relative', top='50%', transform='translateY(-50%)') def set_msg(self, msg): self.msg = msg container = document.getElementById(self.container_id) safe_set_inner_html(container.firstChild.lastChild, self.msg) def on_container_click(self, evt): pass # Dont allow panel to be closed by a click # }}} class DeleteBook: # {{{ def __init__(self, overlay, question, ok_icon, ok_text, reload_book): self.overlay = overlay self.question = question or _( 'Are you sure you want to remove this book from local storage? You will have to re-download it from calibre if you want to read it again.') self.ok_icon = ok_icon or 'trash' self.ok_text = ok_text or _('Delete book') self.reload_book = reload_book def show(self, container): self.container_id = container.getAttribute('id') set_css(container, display='flex', justify_content='center', flex_direction='column', background_color=get_color('window-background')) container.appendChild( E.div(style='margin:1ex 1em', E.h2(self.question), E.div(style='display:flex; justify-content:flex-end', create_button(self.ok_text, self.ok_icon, action=self.delete_book, highlight=True), E.span('\xa0'), create_button(_('Cancel'), action=self.cancel), ) ) ) def show_working(self): container = document.getElementById(self.container_id) clear(container) container.appendChild( E.div( style='text-align:center', E.div(create_spinner('100px', '100px')), E.h2() )) safe_set_inner_html(container.lastChild.lastChild, _('Deleting local book copy, please wait...')) def on_container_click(self, evt): pass # Dont allow panel to be closed by a click def delete_book(self): self.show_working() view = self.overlay.view view.ui.db.delete_book(view.book, def(book, errmsg): self.overlay.hide_current_panel() if errmsg: view.ui.show_error(_('Failed to delete book'), _('Failed to delete book from local storage, click "Show details" for more information.'), errmsg) else: if self.reload_book: get_read_ui().reload_book() else: home() ) def cancel(self): self.overlay.hide_current_panel() # }}} class SyncBook: # {{{ def __init__(self, overlay): self.overlay = overlay self.canceled = False def show(self, container): self.container_id = container.getAttribute('id') set_css(container, display='flex', justify_content='center', flex_direction='column', background_color=get_color('window-background')) book = self.overlay.view.book to_sync = v'[[book.key, new Date(0)]]' sync_library_books(book.key[0], to_sync, self.sync_data_received) container.appendChild( E.div(style='margin:1ex 1em', E.h2(_('Syncing to last read position')), E.p(_('Downloading last read data from server, please wait...')), E.div(style='display:flex; justify-content:flex-end', create_button(_('Cancel'), action=self.cancel), ) ) ) def on_container_click(self, evt): pass # Dont allow panel to be closed by a click def cancel(self): self.canceled = True self.overlay.hide_current_panel() def sync_data_received(self, library_id, lrmap, load_type, xhr, ev): if self.canceled: return self.overlay.hide() if load_type is not 'load': error_dialog(_('Failed to fetch sync data'), _('Failed to download last read data from server, click "Show details" for more information.'), xhr.error_html) return data = JSON.parse(xhr.responseText) book = self.overlay.view.book dev = get_device_uuid() epoch = 0 ans = None for key in data: book_id, fmt = key.partition(':')[::2] if book_id is str(book.key[1]) and fmt.upper() is book.key[2].upper(): last_read_positions = data[key] for d in last_read_positions: if d.device is not dev and d.epoch > epoch: epoch = d.epoch ans = d if ans is not None: cfi = ans.cfi if cfi: self.overlay.view.goto_bookpos(cfi) # }}} # MainOverlay {{{ MAIN_OVERLAY_TS_CLASS = 'read-book-main-overlay-top-section' MAIN_OVERLAY_ACTIONS_CLASS = 'read-book-main-overlay-actions' add_extra_css(def(): style = '' sel = '.' + MAIN_OVERLAY_TS_CLASS + ' > .' + MAIN_OVERLAY_ACTIONS_CLASS + ' ' style += build_rule(sel, overflow='hidden') sel += '> ul ' style += build_rule(sel, display='flex', flex_wrap='wrap', list_style='none', border_bottom='1px solid currentColor') sel += '> li' style += build_rule(sel, border_right='1px solid currentColor', padding='0.5em 1ex', display='flex', align_items='center', cursor='pointer') style += build_rule(sel + ':last-child', border_right_style='none') style += build_rule(sel + ':hover > *:first-child', color=get_color('window-hover-foreground')) style += build_rule(sel + ':active > *:first-child', transform='scale(1.8)') return style ) class MainOverlay: def __init__(self, overlay): self.overlay = overlay self.timer = None self.timer_id = unique_id() if window.Intl?.DateTimeFormat: self.date_formatter = window.Intl.DateTimeFormat(undefined, {'hour':'numeric', 'minute':'numeric'}) else: self.date_formatter = {'format': def(date): return '{}:{}'.format(date.getHours(), date.getMinutes()) } def show(self, container): self.container_id = container.getAttribute('id') icon_size = '3.5ex' def ac(text, tooltip, action, icon, is_text_button): if is_text_button: icon = E.span(icon, style='font-size: 175%; font-weight: bold') else: icon = svgicon(icon, icon_size, icon_size) if icon else '' return E.li(icon, '\xa0', text, onclick=action, title=tooltip) actions_div = E.div( # actions E.ul( ac(_('Home'), _('Return to list of books'), def(): home();, 'home'), ac(_('Back'), None, self.back, 'arrow-left'), ac(_('Forward'), None, self.forward, 'arrow-right'), ), E.ul( ac(_('Search'), _('Search for text in this book'), self.overlay.show_search, 'search'), ac(_('Go to'), _('Go to a specific location in the book'), self.overlay.show_goto, 'chevron-right'), ), E.ul( ac(_('Sync'), _('Get last read position and annotations from the server'), self.overlay.sync_book, 'cloud-download'), ac(_('Delete'), _('Delete this book from the device'), self.overlay.delete_book, 'trash'), ac(_('Reload'), _('Reload this book from the server'), self.overlay.reload_book, 'refresh') ), E.ul( ac(_('Table of Contents'), None, self.overlay.show_toc, 'TC', True), # ac(_('Bookmarks'), None, None, 'bookmark'), ), E.ul( ac(_('Font size'), _('Change text size'), self.overlay.show_font_size_chooser, 'Aa', True), ac(_('Preferences'), _('Configure the book reader'), self.overlay.show_prefs, 'cogs'), ), class_=MAIN_OVERLAY_ACTIONS_CLASS ) if not full_screen_element() and not is_ios: # No fullscreen on iOS, see http://caniuse.com/#search=fullscreen actions_div.appendChild( E.ul( ac(_('Full screen'), _('Enter full screen mode'), def(): request_full_screen(), self.overlay.hide();, 'full-screen'), )) container.appendChild(set_css(E.div(class_=MAIN_OVERLAY_TS_CLASS, # top section onclick=def (evt):evt.stopPropagation();, set_css(E.div( # top row E.div(self.overlay.view.book.metadata.title, style='max-width: 90%; text-overflow: ellipsis; font-weight: bold'), E.div(self.date_formatter.format(Date()), id=self.timer_id, style='max-width: 9%; text-overflow: ellipsis'), ), display='flex', justify_content='space-between', align_items='baseline', font_size='smaller', padding='0.5ex 1rem', border_bottom='solid 1px currentColor' ), actions_div, ), user_select='none', background_color=get_color('window-background'))) container.appendChild( set_css(E.div( # bottom bar svgicon('close', icon_size, icon_size), '\xa0', _('Close'), ), cursor='pointer', position='fixed', width='100vw', bottom='0', display='flex', justify_content='center', align_items='center', padding='0.5ex 1em', user_select='none', background_color=get_color('window-background'), ) ) self.on_hide() self.timer = setInterval(self.update_time, 1000) def update_time(self): tm = document.getElementById(self.timer_id) if tm: tm.textContent = self.date_formatter.format(Date()) def on_hide(self): if self.timer is not None: clearInterval(self.timer) self.timer = None def back(self): window.history.back() def forward(self): window.history.forward() # }}} class TOCOverlay: # {{{ def __init__(self, overlay, create_func, title): self.overlay = overlay self.create_func = create_func or create_toc_panel self.title = title or _('Table of Contents') def on_container_click(self, evt): pass # Dont allow panel to be closed by a click def show(self, container): container.style.backgroundColor = get_color('window-background') container.appendChild(E.div( style='padding: 1ex 1em; border-bottom: solid 1px currentColor; display:flex; justify-content: space-between', E.h2(self.title), E.div(svgicon('close'), style='cursor:pointer', onclick=def(event):event.preventDefault(), event.stopPropagation(), self.overlay.hide_current_panel(event);, class_='simple-link'), )) self.create_func(self.overlay.view.book, container, self.handle_activate) def handle_activate(self, dest, frag): self.overlay.hide() if jstype(dest) is 'function': dest(self.overlay.view, frag) else: self.overlay.view.goto_named_destination(dest, frag) # }}} class WordActionsOverlay: # {{{ def __init__(self, word, overlay): self.word = word self.overlay = overlay def on_container_click(self, evt): pass # Dont allow panel to be closed by a click def show(self, container): container.style.backgroundColor = get_color('window-background') container.appendChild(E.div( style='padding: 1ex 1em; border-bottom: solid 1px currentColor; display:flex; justify-content: space-between', E.h2(_('Lookup: {}').format(self.word)), E.div(svgicon('close'), style='cursor:pointer', onclick=def(event):event.preventDefault(), event.stopPropagation(), self.overlay.hide_current_panel(event);, class_='simple-link'), )) container.appendChild(E.div()) create_word_actions_panel(container, self.word, self.overlay.hide) # }}} class PrefsOverlay: # {{{ def __init__(self, overlay): self.overlay = overlay self.changes_occurred = False def on_container_click(self, evt): pass # Dont allow panel to be closed by a click def show(self, container): self.changes_occurred = False container.style.backgroundColor = get_color('window-background') create_prefs_panel(container, self.overlay.hide_current_panel, def():self.changes_occurred=True;) def on_hide(self): if self.changes_occurred: self.changes_occurred = False get_read_ui().redisplay_book() # }}} class FontSizeOverlay: # {{{ def __init__(self, overlay): self.overlay = overlay def show(self, container): create_font_size_panel(container, self.overlay.hide_current_panel) # }}} class Overlay: def __init__(self, view): self.view = view c = self.clear_container() c.addEventListener('click', self.container_clicked) self.panels = [] def clear_container(self): c = self.container clear(c) c.style.backgroundColor = 'transparent' c.style.color = get_color('window-foreground') c.style.display = 'block' return c @property def container(self): return document.getElementById('book-overlay') @property def is_visible(self): return self.container.style.display is not 'none' def update_visibility(self): self.container.style.display = 'block' if self.panels.length else 'none' def container_clicked(self, evt): if self.panels.length and jstype(self.panels[-1].on_container_click) is 'function': self.panels[-1].on_container_click(evt) else: self.hide_current_panel() def show_loading_message(self, msg): lm = LoadingMessage(msg) self.panels.push(lm) self.show_current_panel() def hide_loading_message(self): self.panels = [p for p in self.panels if not isinstance(p, LoadingMessage)] self.show_current_panel() def hide_current_panel(self): p = self.panels.pop() if p and callable(p.on_hide): p.on_hide() self.show_current_panel() def show_current_panel(self): if self.panels.length: c = self.clear_container() self.panels[-1].show(c) self.update_visibility() def show(self): self.panels = [MainOverlay(self)] self.show_current_panel() def hide(self): while self.panels.length: self.hide_current_panel() self.update_visibility() def delete_book(self): self.hide_current_panel() self.panels = [DeleteBook(self)] self.show_current_panel() def reload_book(self): self.hide_current_panel() self.panels = [DeleteBook(self, _('Are you sure you want to reload this book?'), 'refresh', _('Reload book'), True)] self.show_current_panel() def sync_book(self): self.hide_current_panel() self.panels = [SyncBook(self)] self.show_current_panel() def show_toc(self): self.hide_current_panel() self.panels.push(TOCOverlay(self)) self.show_current_panel() def show_goto(self): self.hide_current_panel() self.panels.push(TOCOverlay(self, create_goto_panel, _('Go to…'))) self.show_current_panel() def show_search(self): self.hide() self.view.show_search() def show_prefs(self): self.hide_current_panel() self.panels.push(PrefsOverlay(self)) self.show_current_panel() def show_font_size_chooser(self): self.hide_current_panel() self.panels.push(FontSizeOverlay(self)) self.show_current_panel() def show_word_actions(self, word): self.hide_current_panel() self.panels.push(WordActionsOverlay(word, self)) self.show_current_panel()