From a171dfc99d8ecfe630d4b6873f5bcbc145e188af Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 5 Apr 2016 20:17:18 +0530 Subject: [PATCH] Implement previous and next page buttons for when scrolling reaches top/bottom in flow mode --- src/pyj/read_book/flow_mode.pyj | 60 +++++++++++++++++++++++++++++++++ src/pyj/read_book/globals.pyj | 20 ++++++++++- src/pyj/read_book/iframe.pyj | 44 ++++++++++++++++++------ src/pyj/read_book/overlay.pyj | 14 ++++++++ src/pyj/read_book/resources.pyj | 2 +- src/pyj/read_book/view.pyj | 59 +++++++++++++++++++++----------- src/pyj/utils.pyj | 2 +- 7 files changed, 169 insertions(+), 32 deletions(-) create mode 100644 src/pyj/read_book/flow_mode.pyj create mode 100644 src/pyj/read_book/overlay.pyj diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj new file mode 100644 index 0000000000..d5247cecfe --- /dev/null +++ b/src/pyj/read_book/flow_mode.pyj @@ -0,0 +1,60 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +from dom import build_rule +from elementmaker import E +from gettext import gettext as _ +from read_book.globals import current_layout_mode, current_spine_item, uid, get_boss + +flow_previous_indicator = flow_next_indicator = None + +def document_height(): + html = document.documentElement + return max(document.body.scrollHeight, document.body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight) + +def setup_flow_indicators(style): + nonlocal flow_previous_indicator, flow_next_indicator + sel = 'flow-indicator-' + uid + flow_previous_indicator = E.div( + style='left:0; top:0;', '⬅ ' + _('prev page'), title=_('Go to the previous page'), + onclick=def(): + get_boss().send_message('next_spine_item', previous=True) + ) + flow_next_indicator = E.div( + style='right:0; bottom:0;', _('next page') + ' ➡', title=_('Go to the next page'), + onclick=def(): + get_boss().send_message('next_spine_item', previous=False) + ) + for c in [flow_next_indicator, flow_previous_indicator]: + c.setAttribute('class', sel) + document.body.appendChild(c) + document.body.appendChild(flow_previous_indicator) + document.body.appendChild(flow_next_indicator) + update_flow_mode_scroll_indicators() + style.push(build_rule( + '.' + sel, position='fixed', z_index='2147483647', padding='1ex 1em', margin='1em', border_radius='8px', + background_color='rgba(255, 255, 255, 0.7)', color='blue', cursor='pointer', font_size='larger', font_family='sans-serif', + transition='opacity 0.5s ease-in', opacity='0' + )) + style.push(build_rule('.' + sel + ':hover', color='red')) + +def update_flow_mode_scroll_indicators(): + if not flow_previous_indicator or current_layout_mode() is not 'flow': + return + near_top = window.pageYOffset < 25 + near_bottom = abs(window.pageYOffset + window.innerHeight - document_height()) < 25 + csi = current_spine_item() + p = near_top and not csi.is_first + n = near_bottom and not csi.is_last + flow_previous_indicator.style.visibility = 'visible' if p else 'hidden' + flow_next_indicator.style.visibility = 'visible' if n else 'hidden' + flow_previous_indicator.style.opacity = '1' if p else '0' + flow_next_indicator.style.opacity = '1' if n else '0' + +def flow_change_mode(): + if flow_previous_indicator: + d = 'block' if current_layout_mode() is 'flow' else 'none' + flow_previous_indicator.style.display = flow_next_indicator.style.display = d + +def flow_to_scroll_fraction(frac): + window.scrollTo(0, document_height() * frac) diff --git a/src/pyj/read_book/globals.pyj b/src/pyj/read_book/globals.pyj index 72cd6e7a69..f3ff4330c1 100644 --- a/src/pyj/read_book/globals.pyj +++ b/src/pyj/read_book/globals.pyj @@ -1,7 +1,8 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -from aes import GCM +from aes import GCM, random_bytes +from encodings import hexlify _boss = None @@ -30,3 +31,20 @@ class Messenger: messenger = Messenger() iframe_id = 'read-book-iframe' +uid = 'calibre-' + hexlify(random_bytes(12)) + +_layout_mode = 'flow' +def current_layout_mode(): + return _layout_mode + +def set_layout_mode(val): + nonlocal _layout_mode + _layout_mode = val + +_current_spine_item = None +def current_spine_item(): + return _current_spine_item + +def set_current_spine_item(val): + nonlocal _current_spine_item + _current_spine_item = val diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index cc645ffef5..bbf02783a4 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -1,27 +1,32 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal +from __python__ import bound_methods from aes import GCM +from elementmaker import E from gettext import install -from read_book.globals import set_boss +from read_book.globals import set_boss, set_current_spine_item, current_layout_mode, current_spine_item from read_book.resources import finalize_resources, unserialize_html +from read_book.flow_mode import setup_flow_indicators, update_flow_mode_scroll_indicators, flow_change_mode, flow_to_scroll_fraction +from utils import debounce class Boss: def __init__(self): self.ready_sent = False self.encrypted_communications = False - window.addEventListener('message', self.handle_message.bind(self), False) + window.addEventListener('message', self.handle_message, False) window.addEventListener('load', def(): if not self.ready_sent: - self.send_message({'action':'ready'}) + self.send_message('ready') self.ready_sent = True ) set_boss(self) self.handlers = { - 'initialize':self.initialize.bind(self), - 'display': self.display.bind(self), + 'initialize':self.initialize, + 'display': self.display, } + self.last_window_ypos = 0 def handle_message(self, event): if event.source is not window.parent: @@ -41,24 +46,43 @@ class Boss: except Exception as e: console.log('Error in iframe message handler:') console.log(e) - self.send_message({'action':'error', 'details':e.stack, 'msg':e.toString()}) + self.send_message('error', details=e.stack, msg=e.toString()) else: print('Unknown action in message to iframe from parent: ' + data.action) def initialize(self, data): self.gcm_from_parent, self.gcm_to_parent = GCM(data.secret.subarray(0, 32)), GCM(data.secret.subarray(32)) - install(data.translations) + if data.translations: + install(data.translations) def display(self, data): self.encrypted_communications = True self.book = data.book + spine = self.book.manifest.spine + index = spine.indexOf(data.name) + set_current_spine_item({'name':data.name, 'is_first':index is 0, 'is_last':index is spine.length - 1, 'initial_scroll_fraction':data.initial_scroll_fraction}) root_data = finalize_resources(self.book, data.name, data.resource_data) - unserialize_html(root_data, self.content_loaded.bind(self)) + unserialize_html(root_data, self.content_loaded) def content_loaded(self): - print('Content loaded') + window.addEventListener('scroll', debounce(self.onscroll, 15)) + style = v'[]' + setup_flow_indicators(style) + document.head.appendChild(E.style(type='text/css', style.join('\n'))) + flow_change_mode() + csi = current_spine_item() + if csi.initial_scroll_fraction: + if current_layout_mode() is 'flow': + flow_to_scroll_fraction(csi.initial_scroll_fraction) - def send_message(self, data): + def onscroll(self, evt): + if evt.view and evt.view is not window.top: + return + self.last_window_ypos = window.pageYOffset + update_flow_mode_scroll_indicators() + + def send_message(self, action, **data): + data.action = action if self.encrypted_communications: data = self.gcm_to_parent.encrypt(JSON.stringify(data)) window.parent.postMessage(data, '*') diff --git a/src/pyj/read_book/overlay.pyj b/src/pyj/read_book/overlay.pyj new file mode 100644 index 0000000000..ca4edbb685 --- /dev/null +++ b/src/pyj/read_book/overlay.pyj @@ -0,0 +1,14 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +from read_book.globals import iframe_id + +class Overlay: + + def __init__(self, view): + self.view = view + + @property + def container(self): + return document.getElementById(iframe_id).nextSibling + diff --git a/src/pyj/read_book/resources.pyj b/src/pyj/read_book/resources.pyj index a39d7ba0df..d5963b2cc1 100644 --- a/src/pyj/read_book/resources.pyj +++ b/src/pyj/read_book/resources.pyj @@ -232,5 +232,5 @@ def unserialize_html(serialized_data, proceed): if load_required.length: setTimeout(hangcheck, 5000) else: - proceed = True + proceeded = True proceed() diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index eda23534d8..80ecde3936 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -1,10 +1,12 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal +from __python__ import bound_methods from elementmaker import E from gettext import gettext as _ from read_book.globals import messenger, iframe_id from read_book.resources import load_resources +from read_book.overlay import Overlay LOADING_DOC = ''' @@ -29,27 +31,28 @@ class View: self.ui = ui self.loaded_resources = {} container.appendChild( - E.div(style='width: 100vw; height: 100vh; overflow: hidden; display: flex; align-items: stretch', - E.div(style='display: flex; flex-direction: column; align-items: stretch; flex-grow:2', - E.iframe( - id=iframe_id, - seamless=True, - sandbox='allow-popups allow-scripts', - style='flex-grow: 2', + E.div(style='width: 100vw; height: 100vh; overflow: hidden; display: flex; align-items: stretch', # container for horizontally aligned panels + E.div(style='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='flex-grow: 2; display:flex; align-items: stretch', # container for iframe and its overlay + E.iframe(id=iframe_id, seamless=True, sandbox='allow-popups allow-scripts', style='flex-grow: 2'), + E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none'), # overlay container ) ) ) ) + self.overlay = Overlay(self) self.src_doc = None self.iframe_ready = False self.pending_spine_load = None self.encrypted_communications = False self.create_src_doc() - window.addEventListener('message', self.handle_message.bind(self), False) + window.addEventListener('message', self.handle_message, False) self.handlers = { - 'ready': self.on_iframe_ready.bind(self), - 'error': self.on_iframe_error.bind(self), + 'ready': self.on_iframe_ready, + 'error': self.on_iframe_error, + 'next_spine_item': self.on_next_spine_item, } + self.currently_showing = {'spine':0, 'cfi':None} @property def iframe(self): @@ -66,7 +69,8 @@ class View: self.encrypted_communications = False self.iframe.srcdoc = self.src_doc - def send_message(self, data): + def send_message(self, action, **data): + data.action = action if self.encrypted_communications: data = messenger.encrypt(data) self.iframe.contentWindow.postMessage(data, '*') @@ -90,7 +94,7 @@ class View: def on_iframe_ready(self, data): messenger.reset() - self.send_message({'action':'initialize', 'secret':messenger.secret, 'translations':self.ui.interface_data.translations}) + self.send_message('initialize', 'secret'=messenger.secret, 'translations'=self.ui.interface_data.translations) self.iframe_ready = True if self.pending_spine_load: data = self.pending_spine_load @@ -111,17 +115,34 @@ class View: self.show_loading(book.metadata.title) self.ui.db.update_last_read_time(book) # TODO: Check for last open position of book - name = book.manifest.spine[0] - load_resources(self.ui.db, book, name, self.loaded_resources, self.show_spine_item.bind(self, name)) + self.show_name(book.manifest.spine[0]) - def show_spine_item(self, name, resource_data): + def show_name(self, name, initial_scroll_fraction=0): + self.currently_showing = {'name':name, 'cfi':None, 'initial_scroll_fraction':initial_scroll_fraction} + load_resources(self.ui.db, self.book, name, self.loaded_resources, self.show_spine_item) + + def on_next_spine_item(self, data): + spine = self.book.manifest.spine + idx = spine.indexOf(self.currently_showing.name) + if data.previous: + if idx is 0: + return + idx = max(idx - 1, 0) + self.show_name(spine[idx], initial_scroll_fraction=1) + else: + if idx is spine.length - 1: + return + idx = min(spine.length - 1, idx + 1) + self.show_name(spine[idx]) + + def show_spine_item(self, resource_data): self.loaded_resources = resource_data # Re-init the iframe to ensure any changes made to the environment by the last spine item are lost self.init_iframe() # Now wait for frame to message that it is ready - self.pending_spine_load = [name, resource_data] + self.pending_spine_load = resource_data - def show_spine_item_stage2(self, x): - name, resource_data = x - self.send_message({'action':'display', 'resource_data':resource_data, 'book':self.book, 'name':name}) + def show_spine_item_stage2(self, resource_data): + self.send_message('display', resource_data=resource_data, book=self.book, name=self.currently_showing.name, + initial_scroll_fraction=self.currently_showing.initial_scroll_fraction) self.encrypted_communications = True diff --git a/src/pyj/utils.pyj b/src/pyj/utils.pyj index 7372d3c5fd..280c743977 100644 --- a/src/pyj/utils.pyj +++ b/src/pyj/utils.pyj @@ -4,7 +4,7 @@ def debounce(func, wait, immediate=False): # Returns a function, that, as long as it continues to be invoked, will not # be triggered. The function will be called after it stops being called for - # N milliseconds. If `immediate` is True, trigger the function on the + # wait milliseconds. If `immediate` is True, trigger the function on the # leading edge, instead of the trailing. timeout = None return def debounce_inner(): # noqa: unused-local