mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Start work on section tracking for the new viewer
This commit is contained in:
parent
1782fbae51
commit
f5c1f90dfa
@ -99,18 +99,23 @@ class TOC(object):
|
||||
def __str__(self):
|
||||
return b'\n'.join([x.encode('utf-8') for x in self.get_lines()])
|
||||
|
||||
@property
|
||||
def as_dict(self):
|
||||
def to_dict(self, node_counter=None):
|
||||
ans = {
|
||||
'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:
|
||||
ans['dest_exists'] = self.dest_exists
|
||||
if self.dest_error is not None:
|
||||
ans['dest_error'] = self.dest_error
|
||||
if node_counter is not None:
|
||||
ans['id'] = next(node_counter)
|
||||
return ans
|
||||
|
||||
@property
|
||||
def as_dict(self):
|
||||
return self.to_dict()
|
||||
|
||||
|
||||
def child_xpath(tag, 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)
|
||||
set_guide_item(container, 'toc', title, name, frag='calibre_generated_inline_toc')
|
||||
return name
|
||||
|
||||
|
@ -31,6 +31,7 @@ def abspath(x):
|
||||
x = '\\\\?\\' + os.path.abspath(x)
|
||||
return x
|
||||
|
||||
|
||||
_books_cache_dir = None
|
||||
|
||||
|
||||
@ -50,9 +51,10 @@ def books_cache_dir():
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
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
|
||||
return job_id
|
||||
|
||||
|
||||
last_final_clean_time = 0
|
||||
|
||||
|
||||
@ -175,6 +178,7 @@ def book_file(ctx, rd, book_id, fmt, size, mtime, name):
|
||||
raise
|
||||
raise HTTPNotFound('No book file with hash: %s and name: %s' % (bhash, name))
|
||||
|
||||
|
||||
mathjax_lock = Lock()
|
||||
mathjax_manifest = None
|
||||
|
||||
|
@ -55,6 +55,7 @@ def decode_url(x):
|
||||
parts = x.split('#', 1)
|
||||
return decode_component(parts[0]), (parts[1] if len(parts) > 1 else '')
|
||||
|
||||
|
||||
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}
|
||||
|
||||
@ -114,6 +115,21 @@ def has_ancestor(elem, q):
|
||||
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):
|
||||
strip_space = re.compile(r'\s+')
|
||||
ans = 0
|
||||
@ -136,6 +152,19 @@ def get_length(root):
|
||||
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):
|
||||
|
||||
tweak_mode = True
|
||||
@ -150,10 +179,11 @@ class Container(ContainerBase):
|
||||
name == 'mimetype'
|
||||
}
|
||||
raster_cover_name, titlepage_name = self.create_cover_page(input_fmt.lower())
|
||||
toc = get_toc(self).to_dict(count())
|
||||
|
||||
self.book_render_data = data = {
|
||||
'version': RENDER_VERSION,
|
||||
'toc':get_toc(self).as_dict,
|
||||
'toc':toc,
|
||||
'spine':[name for name, is_linear in self.spine_names],
|
||||
'link_uid': uuid4(),
|
||||
'book_hash': book_hash,
|
||||
@ -163,6 +193,7 @@ class Container(ContainerBase):
|
||||
'has_maths': False,
|
||||
'total_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
|
||||
for name in data['spine']:
|
||||
@ -188,6 +219,7 @@ class Container(ContainerBase):
|
||||
ans['has_maths'] = hm = check_for_maths(root)
|
||||
if hm:
|
||||
self.book_render_data['has_maths'] = True
|
||||
ans['anchor_map'] = anchor_map(root)
|
||||
return ans
|
||||
data['files'] = {name:manifest_data(name) for name in set(self.name_path_map) - excluded_names}
|
||||
self.commit()
|
||||
@ -467,5 +499,6 @@ def html_as_dict(root):
|
||||
def render(pathtoebook, output_dir, book_hash=None):
|
||||
Container(pathtoebook, output_dir, book_hash=book_hash)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
render(sys.argv[-2], sys.argv[-1])
|
||||
|
@ -5,7 +5,7 @@ from __python__ import hash_literals, bound_methods
|
||||
from dom import set_css
|
||||
from read_book.globals import get_boss
|
||||
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):
|
||||
window.scrollTo(0, document_height() * frac)
|
||||
@ -162,3 +162,27 @@ def handle_gesture(gesture):
|
||||
scroll_by_page(True)
|
||||
elif gesture.type is 'next-page':
|
||||
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])
|
||||
,
|
||||
}
|
||||
|
@ -35,22 +35,25 @@ messenger = Messenger()
|
||||
iframe_id = 'read-book-iframe'
|
||||
uid = 'calibre-' + hexlify(random_bytes(12))
|
||||
|
||||
_layout_mode = 'flow'
|
||||
def current_layout_mode():
|
||||
return _layout_mode
|
||||
return current_layout_mode.value
|
||||
current_layout_mode.value = 'flow'
|
||||
|
||||
def set_layout_mode(val):
|
||||
nonlocal _layout_mode
|
||||
_layout_mode = val
|
||||
current_layout_mode.value = val
|
||||
|
||||
_current_spine_item = None
|
||||
def current_spine_item():
|
||||
return _current_spine_item
|
||||
return current_spine_item.value
|
||||
current_spine_item.value = None
|
||||
|
||||
def set_current_spine_item(val):
|
||||
nonlocal _current_spine_item
|
||||
_current_spine_item = val
|
||||
current_spine_item.value = val
|
||||
|
||||
def toc_anchor_map():
|
||||
return toc_anchor_map.value
|
||||
|
||||
def set_toc_anchor_map(val):
|
||||
toc_anchor_map.value = val
|
||||
|
||||
default_color_schemes = {
|
||||
'white':{'foreground':'#000000', 'background':'#ffffff', 'name':_('White')},
|
||||
|
@ -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.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.toc import update_visible_toc_anchors
|
||||
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, 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 (
|
||||
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, 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.touch import create_handlers as create_touch_handlers
|
||||
@ -104,6 +105,7 @@ class IframeBoss:
|
||||
self._handle_gesture = flow_handle_gesture
|
||||
self.to_scroll_fraction = flow_to_scroll_fraction
|
||||
self.jump_to_cfi = scroll_to_cfi
|
||||
self.anchor_funcs = flow_anchor_funcs
|
||||
else:
|
||||
self.do_layout = paged_layout
|
||||
self.handle_wheel = paged_onwheel
|
||||
@ -111,6 +113,7 @@ class IframeBoss:
|
||||
self.to_scroll_fraction = paged_scroll_to_fraction
|
||||
self.jump_to_cfi = paged_jump_to_cfi
|
||||
self._handle_gesture = paged_handle_gesture
|
||||
self.anchor_funcs = paged_anchor_funcs
|
||||
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
|
||||
@ -174,7 +177,7 @@ class IframeBoss:
|
||||
|
||||
def content_loaded_stage2(self):
|
||||
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('wheel', self.onwheel)
|
||||
window.addEventListener('keydown', self.onkeydown)
|
||||
@ -188,7 +191,7 @@ class IframeBoss:
|
||||
self.scroll_to_anchor(ipos.anchor)
|
||||
elif ipos.type is 'cfi':
|
||||
self.jump_to_cfi(ipos.cfi)
|
||||
self.update_cfi()
|
||||
self.onscroll()
|
||||
self.send_message('content_loaded')
|
||||
|
||||
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.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):
|
||||
if current_layout_mode() is not 'flow':
|
||||
self.do_layout()
|
||||
@ -211,6 +222,7 @@ class IframeBoss:
|
||||
if cfi:
|
||||
paged_jump_to_cfi('/' + cfi)
|
||||
self.update_cfi()
|
||||
self.update_toc_position()
|
||||
|
||||
def onwheel(self, evt):
|
||||
evt.preventDefault()
|
||||
|
@ -513,3 +513,25 @@ def handle_gesture(gesture):
|
||||
scroll_by_page(True, False)
|
||||
elif gesture.type is 'next-page':
|
||||
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
|
||||
,
|
||||
}
|
||||
|
@ -8,13 +8,50 @@ from elementmaker import E
|
||||
from gettext import gettext as _
|
||||
from modals import error_dialog
|
||||
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):
|
||||
|
||||
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):
|
||||
li.dataset.tocDest = node.dest 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)
|
||||
|
||||
@ -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)
|
||||
set_css(search_bar, flex_grow='10', margin_right='1em')
|
||||
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}
|
||||
|
@ -12,6 +12,7 @@ from read_book.overlay import Overlay
|
||||
from read_book.prefs.colors import resolve_color_scheme
|
||||
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.toc import update_visible_toc_nodes
|
||||
from book_list.theme import get_color
|
||||
from utils import parse_url_params, username_key
|
||||
|
||||
@ -82,6 +83,7 @@ class View:
|
||||
'goto_doc_boundary': self.goto_doc_boundary,
|
||||
'scroll_to_anchor': self.on_scroll_to_anchor,
|
||||
'update_cfi': self.on_update_cfi,
|
||||
'update_toc_position': self.on_update_toc_position,
|
||||
'content_loaded': self.on_content_loaded,
|
||||
'show_chrome': self.show_chrome,
|
||||
'bump_font_size': self.bump_font_size,
|
||||
@ -314,6 +316,9 @@ class View:
|
||||
self.book.last_read_position[unkey] = data.cfi
|
||||
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):
|
||||
self.loaded_resources = resource_data
|
||||
# Re-init the iframe to ensure any changes made to the environment by the last spine item are lost
|
||||
|
Loading…
x
Reference in New Issue
Block a user