Encode the current book position in the URL

Allows direct linking to arbitrary locations in a book hosted in the
calibre server
This commit is contained in:
Kovid Goyal 2016-05-20 12:36:50 +05:30
parent 49d1ea8bb2
commit 3ca4670ce9
7 changed files with 114 additions and 22 deletions

View File

@ -4,6 +4,17 @@ from __python__ import hash_literals
from gettext import gettext as _
def encode_query_component(x):
ans = encodeURIComponent(x)
# The following exceptions are to make epubcfi() look better
ans = ans.replace(/%2[fF]/g, '/')
ans = ans.replace(/%40/g, '@')
ans = ans.replace(/%5[bB]/g, '[')
ans = ans.replace(/%5[dD]/g, ']')
ans = ans.replace(/%5[eE]/g, '^')
ans = ans.replace(/%3[aA]/g, ':')
return ans
def encode_query(query):
if not query:
return ''
@ -15,7 +26,7 @@ def encode_query(query):
val = query[k]
if val is undefined or val is None:
continue
path += ('&' if has_query else '?') + encodeURIComponent(k) + '=' + encodeURIComponent(val.toString())
path += ('&' if has_query else '?') + encodeURIComponent(k) + '=' + encode_query_component(val.toString())
has_query = True
return path

View File

@ -42,10 +42,16 @@ class Boss:
if not data.mode or data.mode is 'book_list':
if data.panel is not self.ui.current_panel:
self.ui.show_panel(data.panel, push_state=False)
elif data.mode is 'read_book':
self.current_mode = data.mode
self.apply_mode()
self.read_book(int(data.book_id), data.fmt)
if data.mode is 'read_book':
try:
book_id = int(data.book_id)
except Exception:
book_id = None
if book_id is None:
if data.panel is not self.ui.current_panel:
self.ui.show_panel(data.panel, push_state=False)
else:
self.read_book(book_id, data.fmt)
setTimeout(def():
window.onpopstate = self.onpopstate.bind(self)
, 0) # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load
@ -103,7 +109,6 @@ class Boss:
def read_book(self, book_id, fmt, metadata):
self.current_mode = 'read_book'
self.apply_mode()
self.push_state(extra_query_data={'book_id':book_id, 'fmt':fmt})
self.read_ui.load_book(book_id, fmt, metadata)
def change_books(self, data):
@ -131,6 +136,10 @@ class Boss:
query.search = sq
else:
query.mode = self.current_mode
if self.current_mode is 'read_book':
eqd = self.read_ui.url_data
for k in eqd:
query[k] = eqd[k]
if idata.library_id is not idata.default_library:
query.library_id = idata.library_id
set_current_query(query)

View File

@ -94,7 +94,7 @@ class DB:
'manifest': None,
'cover_width': None,
'cover_height': None,
'last_read_position': None,
'last_read_position': {},
})
)

View File

@ -5,20 +5,29 @@ from __python__ import bound_methods, hash_literals
import traceback
from aes import GCM
from gettext import install, gettext as _
from read_book.cfi import at_current, scroll_to as scroll_to_cfi
from read_book.globals import set_boss, set_current_spine_item, current_layout_mode, current_spine_item, set_layout_mode
from read_book.mathjax import apply_mathjax
from read_book.resources import finalize_resources, unserialize_html
from read_book.flow_mode import flow_to_scroll_fraction, flow_onwheel, flow_onkeydown, layout as flow_layout
from read_book.paged_mode import layout as paged_layout, scroll_to_fraction as paged_scroll_to_fraction, onwheel as paged_onwheel, onkeydown as paged_onkeydown, scroll_to_elem
from read_book.flow_mode import (
flow_to_scroll_fraction, flow_onwheel, flow_onkeydown, layout as flow_layout
)
from read_book.paged_mode import (
layout as paged_layout, scroll_to_fraction as paged_scroll_to_fraction,
onwheel as paged_onwheel, onkeydown as paged_onkeydown, scroll_to_elem,
jump_to_cfi as paged_jump_to_cfi
)
from read_book.settings import apply_settings
from utils import debounce
FORCE_FLOW_MODE = False
class Boss:
class IframeBoss:
def __init__(self):
self.ready_sent = False
self.last_cfi = None
self.replace_history_on_next_cfi_update = True
self.encrypted_communications = False
window.addEventListener('message', self.handle_message, False)
window.addEventListener('load', def():
@ -83,13 +92,16 @@ class Boss:
self.handle_wheel = flow_onwheel
self.handle_keydown = flow_onkeydown
self.to_scroll_fraction = flow_to_scroll_fraction
self.jump_to_cfi = scroll_to_cfi
else:
self.do_layout = paged_layout
self.handle_wheel = paged_onwheel
self.handle_keydown = paged_onkeydown
self.to_scroll_fraction = paged_scroll_to_fraction
self.jump_to_cfi = paged_jump_to_cfi
apply_settings(data.settings)
set_current_spine_item({'name':data.name, 'is_first':index is 0, 'is_last':index is spine.length - 1, 'initial_position':data.initial_position})
self.last_cfi = None
root_data, self.mathjax = finalize_resources(self.book, data.name, data.resource_data)
unserialize_html(root_data, self.content_loaded)
@ -109,13 +121,26 @@ class Boss:
csi = current_spine_item()
if csi.initial_position:
ipos = csi.initial_position
self.replace_history_on_next_cfi_update = ipos.replace_history
if ipos.type is 'frac':
self.to_scroll_fraction(ipos.frac)
elif ipos.type is 'anchor':
self.scroll_to_anchor(ipos.anchor)
elif ipos.type is 'cfi':
self.jump_to_cfi(ipos.cfi)
self.update_cfi()
def update_cfi(self):
pass # TODO: Update CFI
cfi = at_current()
if cfi:
spine = self.book.manifest.spine
index = spine.indexOf(current_spine_item().name)
if index > -1:
cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi)
if cfi != self.last_cfi:
self.last_cfi = cfi
self.send_message('update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update)
self.replace_history_on_next_cfi_update = True
def onresize(self):
if current_layout_mode() is not 'flow':
@ -151,6 +176,7 @@ class Boss:
if not name:
name = current_spine_item().name
if name is current_spine_item().name:
self.replace_history_on_next_cfi_update = False
self.scroll_to_anchor(frag)
else:
self.send_message('scroll_to_anchor', name=name, frag=frag)
@ -168,4 +194,4 @@ class Boss:
def init():
script = document.getElementById('bootstrap')
script.parentNode.removeChild(script) # free up some memory
Boss()
IframeBoss()

View File

@ -96,11 +96,20 @@ class ReadUI:
div.lastChild.textContent = msg or ''
def load_book(self, book_id, fmt, metadata):
self.base_url_data = {'book_id':book_id, 'fmt':fmt}
if self.db is None:
self.pending_load = [book_id, fmt, metadata]
return
self.start_load(book_id, fmt, metadata)
@property
def url_data(self):
ans = {'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, db):
self.db = db
if self.pending_load is not None:

View File

@ -2,13 +2,14 @@
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals
from book_list.globals import get_session_data
from book_list.globals import get_session_data, get_boss
from dom import set_css
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
from utils import parse_url_params, username_key
LOADING_DOC = '''
<!DOCTYPE html>
@ -63,8 +64,9 @@ class View:
'next_spine_item': self.on_next_spine_item,
'goto_doc_boundary': self.goto_doc_boundary,
'scroll_to_anchor': self.on_scroll_to_anchor,
'update_cfi': self.on_update_cfi,
}
self.currently_showing = {'spine':0, 'cfi':None}
self.currently_showing = {}
@property
def iframe(self):
@ -149,10 +151,28 @@ class View:
self.book = book
self.show_loading(book.metadata.title)
self.ui.db.update_last_read_time(book)
# TODO: Check for last open position of book
self.show_name(book.manifest.spine[1])
pos = {'replace_history':True}
unkey = username_key(self.ui.interface_data.username)
name = book.manifest.spine[0]
cfi = None
q = parse_url_params()
if q.bookpos and q.bookpos.startswith('epubcfi(/'):
cfi = q.bookpos
elif book.last_read_position and book.last_read_position[unkey]:
cfi = book.last_read_position[unkey]
if cfi and cfi.startswith('epubcfi(/'):
cfi = cfi[len('epubcfi(/'):-1]
snum, rest = cfi.partition('/')[::2]
try:
snum = int(snum)
except Exception:
print('Invalid spine number in CFI:', snum)
if type(snum) == 'number':
name = book.manifest.spine[(int(snum) // 2) - 1] or name
pos.type, pos.cfi = 'cfi', '/' + rest
self.show_name(name, initial_position=pos)
def show_name(self, name, initial_position=None, cfi=None):
def show_name(self, name, initial_position=None):
if self.currently_showing.loading:
return
sd = get_session_data()
@ -162,16 +182,21 @@ class View:
'read_mode': sd.get('read_mode'),
'cols_per_screen': sd.get('cols_per_screen'),
}
self.currently_showing = {'name':name, 'cfi':cfi, 'settings':settings, 'initial_position':initial_position, 'loading':True}
initial_position = initial_position or {'replace_history':False}
self.currently_showing = {'name':name, 'settings':settings, 'initial_position':initial_position, 'loading':True}
spine = self.book.manifest.spine
idx = spine.indexOf(name)
if idx > -1:
self.currently_showing.bookpos = 'epubcfi(/{})'.format(2 * (idx +1))
self.set_margins(name is self.book.manifest.title_page_name)
load_resources(self.ui.db, self.book, name, self.loaded_resources, self.show_spine_item)
def goto_doc_boundary(self, data):
name = self.book.manifest.spine[0 if data.start else self.book.manifest.spine.length - 1]
self.show_name(name, initial_position={'type':'frac', 'frac':0 if data.start else 1})
self.show_name(name, initial_position={'type':'frac', 'frac':0 if data.start else 1, 'replace_history':False})
def on_scroll_to_anchor(self, data):
self.show_name(data.name, initial_position={'type':'anchor', 'anchor':data.frag})
self.show_name(data.name, initial_position={'type':'anchor', 'anchor':data.frag, 'replace_history':False})
def on_next_spine_item(self, data):
spine = self.book.manifest.spine
@ -180,12 +205,21 @@ class View:
if idx is 0:
return
idx = min(spine.length - 1, max(idx - 1, 0))
self.show_name(spine[idx], initial_position={'type':'frac', 'frac':1})
self.show_name(spine[idx], initial_position={'type':'frac', 'frac':1, 'replace_history':True})
else:
if idx is spine.length - 1:
return
idx = max(0, min(spine.length - 1, idx + 1))
self.show_name(spine[idx])
self.show_name(spine[idx], initial_position={'type':'frac', 'frac':0, 'replace_history':True})
def on_update_cfi(self, data):
self.currently_showing.bookpos = data.cfi
get_boss().push_state(replace=data.replace_history)
unkey = username_key(self.ui.interface_data.username)
if not self.book.last_read_position:
self.book.last_read_position = {}
self.book.last_read_position[unkey] = data.cfi
self.ui.db.update_last_read_time(self.book)
def show_spine_item(self, resource_data):
self.loaded_resources = resource_data

View File

@ -128,6 +128,9 @@ def viewport_to_document(x, y, doc):
y += wy
return x, y
def username_key(username):
return ('u' if username else 'n') + username
if __name__ is '__main__':
print(fmt_sidx(10), fmt_sidx(1.2))
print(list(map(human_readable, [1, 1024.0, 1025, 1024*1024*2.3])))