diff --git a/src/pyj/ajax.pyj b/src/pyj/ajax.pyj index f6cead2cf3..ec2d8db838 100644 --- a/src/pyj/ajax.pyj +++ b/src/pyj/ajax.pyj @@ -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 diff --git a/src/pyj/book_list/boss.pyj b/src/pyj/book_list/boss.pyj index b8c85433ac..400a4531f3 100644 --- a/src/pyj/book_list/boss.pyj +++ b/src/pyj/book_list/boss.pyj @@ -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) diff --git a/src/pyj/read_book/db.pyj b/src/pyj/read_book/db.pyj index dc4d3b0059..66a8960e75 100644 --- a/src/pyj/read_book/db.pyj +++ b/src/pyj/read_book/db.pyj @@ -94,7 +94,7 @@ class DB: 'manifest': None, 'cover_width': None, 'cover_height': None, - 'last_read_position': None, + 'last_read_position': {}, }) ) diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 5dabb0dfe1..7670cd25ab 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -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() diff --git a/src/pyj/read_book/ui.pyj b/src/pyj/read_book/ui.pyj index e7c992d290..a5120805f2 100644 --- a/src/pyj/read_book/ui.pyj +++ b/src/pyj/read_book/ui.pyj @@ -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: diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index ccd0280c84..068bf3c41b 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -2,13 +2,14 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal 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 = ''' @@ -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 diff --git a/src/pyj/utils.pyj b/src/pyj/utils.pyj index 5593321cdc..2e895796c7 100644 --- a/src/pyj/utils.pyj +++ b/src/pyj/utils.pyj @@ -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])))