Implement previous and next page buttons for when scrolling reaches top/bottom in flow mode

This commit is contained in:
Kovid Goyal 2016-04-05 20:17:18 +05:30
parent eaeff825eb
commit a171dfc99d
7 changed files with 169 additions and 32 deletions

View File

@ -0,0 +1,60 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@ -1,7 +1,8 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

@ -1,27 +1,32 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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, '*')

View File

@ -0,0 +1,14 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

@ -232,5 +232,5 @@ def unserialize_html(serialized_data, proceed):
if load_required.length:
setTimeout(hangcheck, 5000)
else:
proceed = True
proceeded = True
proceed()

View File

@ -1,10 +1,12 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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 = '''
<!DOCTYPE html>
@ -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

View File

@ -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