Start work on section tracking for the new viewer

This commit is contained in:
Kovid Goyal 2016-12-23 07:35:42 +05:30
parent 1782fbae51
commit f5c1f90dfa
9 changed files with 206 additions and 20 deletions

View File

@ -99,18 +99,23 @@ class TOC(object):
def __str__(self): def __str__(self):
return b'\n'.join([x.encode('utf-8') for x in self.get_lines()]) return b'\n'.join([x.encode('utf-8') for x in self.get_lines()])
@property def to_dict(self, node_counter=None):
def as_dict(self):
ans = { ans = {
'title':self.title, 'dest':self.dest, 'frag':self.frag, 'title':self.title, 'dest':self.dest, 'frag':self.frag,
'children':[c.as_dict for c in self.children] 'children':[c.to_dict(node_counter) for c in self.children]
} }
if self.dest_exists is not None: if self.dest_exists is not None:
ans['dest_exists'] = self.dest_exists ans['dest_exists'] = self.dest_exists
if self.dest_error is not None: if self.dest_error is not None:
ans['dest_error'] = self.dest_error ans['dest_error'] = self.dest_error
if node_counter is not None:
ans['id'] = next(node_counter)
return ans return ans
@property
def as_dict(self):
return self.to_dict()
def child_xpath(tag, name): def child_xpath(tag, name):
return tag.xpath('./*[calibre:lower-case(local-name()) = "%s"]'%name) return tag.xpath('./*[calibre:lower-case(local-name()) = "%s"]'%name)
@ -748,4 +753,3 @@ def create_inline_toc(container, title=None):
f.write(raw) f.write(raw)
set_guide_item(container, 'toc', title, name, frag='calibre_generated_inline_toc') set_guide_item(container, 'toc', title, name, frag='calibre_generated_inline_toc')
return name return name

View File

@ -31,6 +31,7 @@ def abspath(x):
x = '\\\\?\\' + os.path.abspath(x) x = '\\\\?\\' + os.path.abspath(x)
return x return x
_books_cache_dir = None _books_cache_dir = None
@ -50,9 +51,10 @@ def books_cache_dir():
def book_hash(library_uuid, book_id, fmt, size, mtime): def book_hash(library_uuid, book_id, fmt, size, mtime):
raw = dumps((library_uuid, book_id, fmt.upper(), size, mtime), RENDER_VERSION) raw = dumps((library_uuid, book_id, fmt.upper(), size, mtime, RENDER_VERSION))
return sha1(raw).hexdigest().decode('ascii') return sha1(raw).hexdigest().decode('ascii')
staging_cleaned = False staging_cleaned = False
@ -82,6 +84,7 @@ def queue_job(ctx, copy_format_to, bhash, fmt, book_id, size, mtime):
queued_jobs[bhash] = job_id queued_jobs[bhash] = job_id
return job_id return job_id
last_final_clean_time = 0 last_final_clean_time = 0
@ -175,6 +178,7 @@ def book_file(ctx, rd, book_id, fmt, size, mtime, name):
raise raise
raise HTTPNotFound('No book file with hash: %s and name: %s' % (bhash, name)) raise HTTPNotFound('No book file with hash: %s and name: %s' % (bhash, name))
mathjax_lock = Lock() mathjax_lock = Lock()
mathjax_manifest = None mathjax_manifest = None

View File

@ -55,6 +55,7 @@ def decode_url(x):
parts = x.split('#', 1) parts = x.split('#', 1)
return decode_component(parts[0]), (parts[1] if len(parts) > 1 else '') return decode_component(parts[0]), (parts[1] if len(parts) > 1 else '')
absolute_units = frozenset('px mm cm pt in pc q'.split()) absolute_units = frozenset('px mm cm pt in pc q'.split())
length_factors = {'mm':2.8346456693, 'cm':28.346456693, 'in': 72, 'pc': 12, 'q':0.708661417325} length_factors = {'mm':2.8346456693, 'cm':28.346456693, 'in': 72, 'pc': 12, 'q':0.708661417325}
@ -114,6 +115,21 @@ def has_ancestor(elem, q):
return False return False
def anchor_map(root):
ans = []
seen = set()
for elem in root.xpath('//*[@id or @name]'):
eid = elem.get('id')
if not eid and elem.tag.endswith('}a'):
eid = elem.get('name')
if eid:
elem.set('id', eid)
if eid and eid not in seen:
ans.append(eid)
seen.add(eid)
return ans
def get_length(root): def get_length(root):
strip_space = re.compile(r'\s+') strip_space = re.compile(r'\s+')
ans = 0 ans = 0
@ -136,6 +152,19 @@ def get_length(root):
return ans return ans
def toc_anchor_map(toc):
ans = defaultdict(list)
def process_node(node):
name = node['dest']
if name:
ans[name].append({'id':node['id'], 'frag':node['frag']})
tuple(map(process_node, node['children']))
process_node(toc)
return dict(ans)
class Container(ContainerBase): class Container(ContainerBase):
tweak_mode = True tweak_mode = True
@ -150,10 +179,11 @@ class Container(ContainerBase):
name == 'mimetype' name == 'mimetype'
} }
raster_cover_name, titlepage_name = self.create_cover_page(input_fmt.lower()) raster_cover_name, titlepage_name = self.create_cover_page(input_fmt.lower())
toc = get_toc(self).to_dict(count())
self.book_render_data = data = { self.book_render_data = data = {
'version': RENDER_VERSION, 'version': RENDER_VERSION,
'toc':get_toc(self).as_dict, 'toc':toc,
'spine':[name for name, is_linear in self.spine_names], 'spine':[name for name, is_linear in self.spine_names],
'link_uid': uuid4(), 'link_uid': uuid4(),
'book_hash': book_hash, 'book_hash': book_hash,
@ -163,6 +193,7 @@ class Container(ContainerBase):
'has_maths': False, 'has_maths': False,
'total_length': 0, 'total_length': 0,
'spine_length': 0, 'spine_length': 0,
'toc_anchor_map': toc_anchor_map(toc),
} }
# Mark the spine as dirty since we have to ensure it is normalized # Mark the spine as dirty since we have to ensure it is normalized
for name in data['spine']: for name in data['spine']:
@ -188,6 +219,7 @@ class Container(ContainerBase):
ans['has_maths'] = hm = check_for_maths(root) ans['has_maths'] = hm = check_for_maths(root)
if hm: if hm:
self.book_render_data['has_maths'] = True self.book_render_data['has_maths'] = True
ans['anchor_map'] = anchor_map(root)
return ans return ans
data['files'] = {name:manifest_data(name) for name in set(self.name_path_map) - excluded_names} data['files'] = {name:manifest_data(name) for name in set(self.name_path_map) - excluded_names}
self.commit() self.commit()
@ -467,5 +499,6 @@ def html_as_dict(root):
def render(pathtoebook, output_dir, book_hash=None): def render(pathtoebook, output_dir, book_hash=None):
Container(pathtoebook, output_dir, book_hash=book_hash) Container(pathtoebook, output_dir, book_hash=book_hash)
if __name__ == '__main__': if __name__ == '__main__':
render(sys.argv[-2], sys.argv[-1]) render(sys.argv[-2], sys.argv[-1])

View File

@ -5,7 +5,7 @@ from __python__ import hash_literals, bound_methods
from dom import set_css from dom import set_css
from read_book.globals import get_boss from read_book.globals import get_boss
from keycodes import get_key from keycodes import get_key
from utils import document_height, document_width from utils import document_height, document_width, viewport_to_document
def flow_to_scroll_fraction(frac): def flow_to_scroll_fraction(frac):
window.scrollTo(0, document_height() * frac) window.scrollTo(0, document_height() * frac)
@ -162,3 +162,27 @@ def handle_gesture(gesture):
scroll_by_page(True) scroll_by_page(True)
elif gesture.type is 'next-page': elif gesture.type is 'next-page':
scroll_by_page(False) scroll_by_page(False)
anchor_funcs = {
'pos_for_elem': def pos_for_elem(elem):
if not elem:
return 0, 0
br = elem.getBoundingClientRect()
x, y = viewport_to_document(br.left, br.top, elem.ownerDocument)
return y, x
,
'visibility': def visibility(pos):
y, x = pos
if y < window.pageYOffset:
return -1
if y < window.pageYOffset + window.innerHeight:
if x < window.pageXOffset:
return -1
if x < window.pageXOffset + window.innerWidth:
return 0
return 1
,
'cmp': def cmp(a, b):
return (a[0] - b[0]) or (a[1] - b[1])
,
}

View File

@ -35,22 +35,25 @@ messenger = Messenger()
iframe_id = 'read-book-iframe' iframe_id = 'read-book-iframe'
uid = 'calibre-' + hexlify(random_bytes(12)) uid = 'calibre-' + hexlify(random_bytes(12))
_layout_mode = 'flow'
def current_layout_mode(): def current_layout_mode():
return _layout_mode return current_layout_mode.value
current_layout_mode.value = 'flow'
def set_layout_mode(val): def set_layout_mode(val):
nonlocal _layout_mode current_layout_mode.value = val
_layout_mode = val
_current_spine_item = None
def current_spine_item(): def current_spine_item():
return _current_spine_item return current_spine_item.value
current_spine_item.value = None
def set_current_spine_item(val): def set_current_spine_item(val):
nonlocal _current_spine_item current_spine_item.value = val
_current_spine_item = val
def toc_anchor_map():
return toc_anchor_map.value
def set_toc_anchor_map(val):
toc_anchor_map.value = val
default_color_schemes = { default_color_schemes = {
'white':{'foreground':'#000000', 'background':'#ffffff', 'name':_('White')}, 'white':{'foreground':'#000000', 'background':'#ffffff', 'name':_('White')},

View File

@ -8,16 +8,17 @@ from gettext import install, gettext as _
from read_book.cfi import at_current, scroll_to as scroll_to_cfi 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.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.mathjax import apply_mathjax
from read_book.toc import update_visible_toc_anchors
from read_book.resources import finalize_resources, unserialize_html from read_book.resources import finalize_resources, unserialize_html
from read_book.flow_mode import ( from read_book.flow_mode import (
flow_to_scroll_fraction, flow_onwheel, flow_onkeydown, layout as flow_layout, handle_gesture as flow_handle_gesture, flow_to_scroll_fraction, flow_onwheel, flow_onkeydown, layout as flow_layout, handle_gesture as flow_handle_gesture,
scroll_by_page as flow_scroll_by_page scroll_by_page as flow_scroll_by_page, anchor_funcs as flow_anchor_funcs
) )
from read_book.paged_mode import ( from read_book.paged_mode import (
layout as paged_layout, scroll_to_fraction as paged_scroll_to_fraction, layout as paged_layout, scroll_to_fraction as paged_scroll_to_fraction,
onwheel as paged_onwheel, onkeydown as paged_onkeydown, scroll_to_elem, onwheel as paged_onwheel, onkeydown as paged_onkeydown, scroll_to_elem,
jump_to_cfi as paged_jump_to_cfi, handle_gesture as paged_handle_gesture, jump_to_cfi as paged_jump_to_cfi, handle_gesture as paged_handle_gesture,
scroll_by_page as paged_scroll_by_page scroll_by_page as paged_scroll_by_page, anchor_funcs as paged_anchor_funcs
) )
from read_book.settings import apply_settings, opts from read_book.settings import apply_settings, opts
from read_book.touch import create_handlers as create_touch_handlers from read_book.touch import create_handlers as create_touch_handlers
@ -104,6 +105,7 @@ class IframeBoss:
self._handle_gesture = flow_handle_gesture self._handle_gesture = flow_handle_gesture
self.to_scroll_fraction = flow_to_scroll_fraction self.to_scroll_fraction = flow_to_scroll_fraction
self.jump_to_cfi = scroll_to_cfi self.jump_to_cfi = scroll_to_cfi
self.anchor_funcs = flow_anchor_funcs
else: else:
self.do_layout = paged_layout self.do_layout = paged_layout
self.handle_wheel = paged_onwheel self.handle_wheel = paged_onwheel
@ -111,6 +113,7 @@ class IframeBoss:
self.to_scroll_fraction = paged_scroll_to_fraction self.to_scroll_fraction = paged_scroll_to_fraction
self.jump_to_cfi = paged_jump_to_cfi self.jump_to_cfi = paged_jump_to_cfi
self._handle_gesture = paged_handle_gesture self._handle_gesture = paged_handle_gesture
self.anchor_funcs = paged_anchor_funcs
apply_settings(data.settings) 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}) 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 self.last_cfi = None
@ -174,7 +177,7 @@ class IframeBoss:
def content_loaded_stage2(self): def content_loaded_stage2(self):
self.connect_links() self.connect_links()
window.addEventListener('scroll', debounce(self.update_cfi, 1000)) window.addEventListener('scroll', debounce(self.onscroll, 1000))
window.addEventListener('resize', debounce(self.onresize, 500)) window.addEventListener('resize', debounce(self.onresize, 500))
window.addEventListener('wheel', self.onwheel) window.addEventListener('wheel', self.onwheel)
window.addEventListener('keydown', self.onkeydown) window.addEventListener('keydown', self.onkeydown)
@ -188,7 +191,7 @@ class IframeBoss:
self.scroll_to_anchor(ipos.anchor) self.scroll_to_anchor(ipos.anchor)
elif ipos.type is 'cfi': elif ipos.type is 'cfi':
self.jump_to_cfi(ipos.cfi) self.jump_to_cfi(ipos.cfi)
self.update_cfi() self.onscroll()
self.send_message('content_loaded') self.send_message('content_loaded')
def update_cfi(self): def update_cfi(self):
@ -203,6 +206,14 @@ class IframeBoss:
self.send_message('update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update) self.send_message('update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update)
self.replace_history_on_next_cfi_update = True self.replace_history_on_next_cfi_update = True
def update_toc_position(self):
visible_anchors = update_visible_toc_anchors(self.book.manifest.toc_anchor_map, self.anchor_funcs)
self.send_message('update_toc_position', visible_anchors=visible_anchors)
def onscroll(self):
self.update_cfi()
self.update_toc_position()
def onresize(self): def onresize(self):
if current_layout_mode() is not 'flow': if current_layout_mode() is not 'flow':
self.do_layout() self.do_layout()
@ -211,6 +222,7 @@ class IframeBoss:
if cfi: if cfi:
paged_jump_to_cfi('/' + cfi) paged_jump_to_cfi('/' + cfi)
self.update_cfi() self.update_cfi()
self.update_toc_position()
def onwheel(self, evt): def onwheel(self, evt):
evt.preventDefault() evt.preventDefault()

View File

@ -513,3 +513,25 @@ def handle_gesture(gesture):
scroll_by_page(True, False) scroll_by_page(True, False)
elif gesture.type is 'next-page': elif gesture.type is 'next-page':
scroll_by_page(False, False) scroll_by_page(False, False)
anchor_funcs = {
'pos_for_elem': def pos_for_elem(elem):
if not elem:
return 0
br = elem.getBoundingClientRect()
x = viewport_to_document(br.left, br.top, elem.ownerDocument)[0]
return column_at(x)
,
'visibility': def visibility(pos):
first = column_at(window.pageXOffset + 10)
if pos < first:
return -1
if pos < first + cols_per_screen:
return 0
return 1
,
'cmp': def cmp(a, b):
return a - b
,
}

View File

@ -8,13 +8,50 @@ from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from modals import error_dialog from modals import error_dialog
from widgets import create_tree, find_text_in_tree, scroll_tree_item_into_view from widgets import create_tree, find_text_in_tree, scroll_tree_item_into_view
from read_book.globals import toc_anchor_map, set_toc_anchor_map, current_spine_item, current_layout_mode
def update_visible_toc_nodes(visible_anchors):
update_visible_toc_nodes.data = visible_anchors
update_visible_toc_nodes.data = {}
def get_highlighted_toc_nodes(toc, parent_map, id_map):
data = update_visible_toc_nodes.data
ans = {}
if data.has_visible:
ans = data.visible_anchors
elif data.before:
ans[data.before] = True
for node_id in Object.keys(ans):
pid = parent_map[node_id]
while pid:
ans[pid] = True
pid = parent_map[pid]
return ans
def create_toc_tree(toc, onclick): def create_toc_tree(toc, onclick):
parent_map, id_map = {}, {}
def process_node(node, parent):
id_map[node.id] = node
parent_map[node.id] = parent
for c in node.children:
process_node(c, node)
process_node(toc)
highlighted_toc_nodes = get_highlighted_toc_nodes(toc, parent_map, id_map)
def populate_data(node, li, a): def populate_data(node, li, a):
li.dataset.tocDest = node.dest or '' li.dataset.tocDest = node.dest or ''
li.dataset.tocFrag = node.frag or '' li.dataset.tocFrag = node.frag or ''
a.textContent = node.title or '' title = node.title or ''
if highlighted_toc_nodes[node.id]:
a.appendChild(E.b(E.i(title)))
else:
a.textContent = title
return create_tree(toc, populate_data, onclick) return create_tree(toc, populate_data, onclick)
@ -47,3 +84,45 @@ def create_toc_panel(book, container, onclick, onclose):
search_bar = create_search_bar(do_search.bind(toc_panel_id), 'search-book-toc', button=search_button, placeholder=t) search_bar = create_search_bar(do_search.bind(toc_panel_id), 'search-book-toc', button=search_button, placeholder=t)
set_css(search_bar, flex_grow='10', margin_right='1em') set_css(search_bar, flex_grow='10', margin_right='1em')
container.appendChild(E.div(style='margin: 1ex 1em; display: flex;', search_bar, search_button)) container.appendChild(E.div(style='margin: 1ex 1em; display: flex;', search_bar, search_button))
def current_toc_anchor_map(tam, anchor_funcs):
current_map = toc_anchor_map()
if not (current_map and current_map.layout_mode is current_layout_mode() and current_map.width is window.innerWidth and current_map.height is window.innerHeight):
name = current_spine_item().name
am = {}
anchors = v'[]'
for anchor in (tam[name] or v'[]'):
val = anchor_funcs.pos_for_elem()
if anchor.frag:
elem = document.getElementById(anchor.frag)
if elem:
val = anchor_funcs.pos_for_elem(elem)
am[anchor.id] = val
anchors.push(anchor.id)
anchors.sort(def (a, b): anchor_funcs.cmp(am[a], am[b]);)
sort_map = {aid: i for i, aid in enumerate(anchors)}
current_map = {'layout_mode': current_layout_mode, 'width': window.innerWidth, 'height': window.innerHeight, 'pos_map': am, 'sort_map':sort_map}
set_toc_anchor_map(current_map)
return current_map
def update_visible_toc_anchors(toc_anchor_map, anchor_funcs):
tam = current_toc_anchor_map(toc_anchor_map, anchor_funcs)
prev = before = after = None
visible_anchors = {}
has_visible = False
for anchor_id in tam.pos_map:
pos = tam.pos_map[anchor_id]
visibility = anchor_funcs.visibility(pos)
if visibility is 0:
before = prev
has_visible = True
visible_anchors[anchor_id] = True
elif visibility > 0:
after = anchor_id
break
prev = anchor_id
return {'visible_anchors':visible_anchors, 'has_visible':has_visible, 'before':before, 'after':after}

View File

@ -12,6 +12,7 @@ from read_book.overlay import Overlay
from read_book.prefs.colors import resolve_color_scheme from read_book.prefs.colors import resolve_color_scheme
from read_book.prefs.font_size import change_font_size_by from read_book.prefs.font_size import change_font_size_by
from read_book.touch import set_left_margin_handler, set_right_margin_handler from read_book.touch import set_left_margin_handler, set_right_margin_handler
from read_book.toc import update_visible_toc_nodes
from book_list.theme import get_color from book_list.theme import get_color
from utils import parse_url_params, username_key from utils import parse_url_params, username_key
@ -82,6 +83,7 @@ class View:
'goto_doc_boundary': self.goto_doc_boundary, 'goto_doc_boundary': self.goto_doc_boundary,
'scroll_to_anchor': self.on_scroll_to_anchor, 'scroll_to_anchor': self.on_scroll_to_anchor,
'update_cfi': self.on_update_cfi, 'update_cfi': self.on_update_cfi,
'update_toc_position': self.on_update_toc_position,
'content_loaded': self.on_content_loaded, 'content_loaded': self.on_content_loaded,
'show_chrome': self.show_chrome, 'show_chrome': self.show_chrome,
'bump_font_size': self.bump_font_size, 'bump_font_size': self.bump_font_size,
@ -314,6 +316,9 @@ class View:
self.book.last_read_position[unkey] = data.cfi self.book.last_read_position[unkey] = data.cfi
self.ui.db.update_last_read_time(self.book) self.ui.db.update_last_read_time(self.book)
def on_update_toc_position(self, data):
update_visible_toc_nodes(data.visible_anchors)
def show_spine_item(self, resource_data): def show_spine_item(self, resource_data):
self.loaded_resources = 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 # Re-init the iframe to ensure any changes made to the environment by the last spine item are lost