mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement previous and next page buttons for when scrolling reaches top/bottom in flow mode
This commit is contained in:
parent
eaeff825eb
commit
a171dfc99d
60
src/pyj/read_book/flow_mode.pyj
Normal file
60
src/pyj/read_book/flow_mode.pyj
Normal 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)
|
@ -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
|
||||
|
@ -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))
|
||||
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, '*')
|
||||
|
14
src/pyj/read_book/overlay.pyj
Normal file
14
src/pyj/read_book/overlay.pyj
Normal 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
|
||||
|
@ -232,5 +232,5 @@ def unserialize_html(serialized_data, proceed):
|
||||
if load_required.length:
|
||||
setTimeout(hangcheck, 5000)
|
||||
else:
|
||||
proceed = True
|
||||
proceeded = True
|
||||
proceed()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user