mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Allow tap/click on word to skip to speaking from it
This commit is contained in:
parent
65445427fd
commit
fbeb0230ff
@ -28,25 +28,6 @@ def text_nodes_in_range(r):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def first_non_empty_text_node_in_range(r):
|
|
||||||
parent = r.commonAncestorContainer
|
|
||||||
doc = parent.ownerDocument or document
|
|
||||||
iterator = doc.createNodeIterator(parent)
|
|
||||||
in_range = False
|
|
||||||
while True:
|
|
||||||
node = iterator.nextNode()
|
|
||||||
if not node:
|
|
||||||
break
|
|
||||||
if not in_range and node.isSameNode(r.startContainer):
|
|
||||||
in_range = True
|
|
||||||
if in_range:
|
|
||||||
if is_non_empty_text_node(node):
|
|
||||||
return node
|
|
||||||
if node.isSameNode(r.endContainer):
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def first_annot_in_range(r, annot_id_uuid_map):
|
def first_annot_in_range(r, annot_id_uuid_map):
|
||||||
parent = r.commonAncestorContainer
|
parent = r.commonAncestorContainer
|
||||||
doc = parent.ownerDocument or document
|
doc = parent.ownerDocument or document
|
||||||
|
@ -7,7 +7,7 @@ from fs_images import fix_fullscreen_svg_images
|
|||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from iframe_comm import IframeClient
|
from iframe_comm import IframeClient
|
||||||
from range_utils import (
|
from range_utils import (
|
||||||
first_non_empty_text_node_in_range, highlight_associated_with_selection,
|
highlight_associated_with_selection,
|
||||||
last_span_for_crw, reset_highlight_counter, select_crw, unwrap_all_crw,
|
last_span_for_crw, reset_highlight_counter, select_crw, unwrap_all_crw,
|
||||||
unwrap_crw, wrap_text_in_range
|
unwrap_crw, wrap_text_in_range
|
||||||
)
|
)
|
||||||
@ -64,7 +64,7 @@ from read_book.touch import (
|
|||||||
)
|
)
|
||||||
from read_book.viewport import scroll_viewport
|
from read_book.viewport import scroll_viewport
|
||||||
from select import (
|
from select import (
|
||||||
move_end_of_selection, range_for_tts, selection_extents, word_at_point
|
move_end_of_selection, selection_extents, word_at_point
|
||||||
)
|
)
|
||||||
from utils import debounce, is_ios
|
from utils import debounce, is_ios
|
||||||
|
|
||||||
@ -900,15 +900,16 @@ class IframeBoss:
|
|||||||
if data.type is 'mark':
|
if data.type is 'mark':
|
||||||
self.mark_word_being_spoken(data.num)
|
self.mark_word_being_spoken(data.num)
|
||||||
elif data.type is 'play':
|
elif data.type is 'play':
|
||||||
if data.x? and data.y?:
|
text_node, offset = None, 0
|
||||||
r = range_for_tts(data.x, data.y)
|
if data.pos:
|
||||||
text_node, offset = first_non_empty_text_node_in_range(r)
|
r = word_at_point(data.pos.x, data.pos.y)
|
||||||
else:
|
if r:
|
||||||
text_node, offset = None, 0
|
if r.startContainer?.nodeType is Node.TEXT_NODE:
|
||||||
|
text_node, offset = r.startContainer, r.startOffset
|
||||||
marked_text = tts_data(text_node, offset)
|
marked_text = tts_data(text_node, offset)
|
||||||
sel = window.getSelection()
|
sel = window.getSelection()
|
||||||
sel.removeAllRanges()
|
sel.removeAllRanges()
|
||||||
self.send_message('tts', type='text-extracted', marked_text=marked_text)
|
self.send_message('tts', type='text-extracted', marked_text=marked_text, pos=data.pos)
|
||||||
|
|
||||||
def mark_word_being_spoken(self, occurrence_number):
|
def mark_word_being_spoken(self, occurrence_number):
|
||||||
self.last_search_at = window.performance.now()
|
self.last_search_at = window.performance.now()
|
||||||
|
@ -9,7 +9,7 @@ from dom import clear, svgicon, unique_id
|
|||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from read_book.globals import runtime, ui_operations
|
from read_book.globals import runtime, ui_operations
|
||||||
from read_book.highlights import ICON_SIZE
|
from read_book.highlights import ICON_SIZE
|
||||||
from read_book.selection_bar import BUTTON_MARGIN
|
from read_book.selection_bar import BUTTON_MARGIN, get_margins, map_to_iframe_coords
|
||||||
from read_book.shortcuts import shortcut_for_key_event
|
from read_book.shortcuts import shortcut_for_key_event
|
||||||
|
|
||||||
HIDDEN = 0
|
HIDDEN = 0
|
||||||
@ -35,6 +35,7 @@ class ReadAloud:
|
|||||||
'display: inline-flex; flex-direction: column; margin: 1rem;'
|
'display: inline-flex; flex-direction: column; margin: 1rem;'
|
||||||
))
|
))
|
||||||
container.addEventListener('keydown', self.on_keydown, {'passive': False})
|
container.addEventListener('keydown', self.on_keydown, {'passive': False})
|
||||||
|
container.addEventListener('click', self.container_clicked, {'passive': False})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def container(self):
|
def container(self):
|
||||||
@ -83,17 +84,16 @@ class ReadAloud:
|
|||||||
return
|
return
|
||||||
bar_container = self.bar
|
bar_container = self.bar
|
||||||
clear(bar_container)
|
clear(bar_container)
|
||||||
bar_container.style.maxWidth = 'min(50rem, 90vw)' if self.supports_css_min_max else '50rem'
|
bar_container.style.maxWidth = 'min(40rem, 80vw)' if self.supports_css_min_max else '40rem'
|
||||||
bar_container.style.backgroundColor = get_color("window-background")
|
bar_container.style.backgroundColor = get_color("window-background")
|
||||||
notes_container = E.div()
|
|
||||||
for x in [
|
for x in [
|
||||||
E.div(style='height: 4ex; display: flex; align-items: center; padding: 5px; justify-content: center'),
|
E.div(style='height: 4ex; display: flex; align-items: center; padding: 5px; justify-content: center'),
|
||||||
|
|
||||||
E.hr(style='border-top: solid 1px; margin: 0; padding: 0; display: none'),
|
E.hr(style='border-top: solid 1px; margin: 0; padding: 0; display: none'),
|
||||||
|
|
||||||
E.div(
|
E.div(
|
||||||
style='display: none; padding: 5px;',
|
style='display: none; padding: 5px; font-size: smaller',
|
||||||
notes_container,
|
E.div()
|
||||||
)
|
)
|
||||||
]:
|
]:
|
||||||
bar_container.appendChild(x)
|
bar_container.appendChild(x)
|
||||||
@ -118,6 +118,16 @@ class ReadAloud:
|
|||||||
else:
|
else:
|
||||||
bar.appendChild(cb('play', 'play', _('Start reading') if self.state is STOPPED else _('Resume reading')))
|
bar.appendChild(cb('play', 'play', _('Start reading') if self.state is STOPPED else _('Resume reading')))
|
||||||
bar.appendChild(cb('hide', 'close', _('Close Read aloud')))
|
bar.appendChild(cb('hide', 'close', _('Close Read aloud')))
|
||||||
|
if self.state is not WAITING_FOR_PLAY_TO_START:
|
||||||
|
notes_container = bar_container.lastChild
|
||||||
|
notes_container.style.display = notes_container.previousSibling.style.display = 'block'
|
||||||
|
notes_container = notes_container.lastChild
|
||||||
|
if self.state is STOPPED:
|
||||||
|
notes_container.textContent = _('Tap/click on a word to start from there')
|
||||||
|
elif self.state is PLAYING:
|
||||||
|
notes_container.textContent = _('Tap/click on a word to skip to it')
|
||||||
|
else:
|
||||||
|
notes_container.textContent = _('Tap/click on a word to continue from there')
|
||||||
|
|
||||||
def play(self):
|
def play(self):
|
||||||
if self.state is PAUSED:
|
if self.state is PAUSED:
|
||||||
@ -143,6 +153,15 @@ class ReadAloud:
|
|||||||
elif self.state is PAUSED or self.state is STOPPED:
|
elif self.state is PAUSED or self.state is STOPPED:
|
||||||
self.play()
|
self.play()
|
||||||
|
|
||||||
|
def container_clicked(self, ev):
|
||||||
|
if ev.button is not 0:
|
||||||
|
return
|
||||||
|
ev.stopPropagation(), ev.preventDefault()
|
||||||
|
margins = get_margins()
|
||||||
|
pos = {'x': ev.clientX, 'y': ev.clientY}
|
||||||
|
pos = map_to_iframe_coords(pos, margins)
|
||||||
|
self.send_message('play', pos=pos)
|
||||||
|
|
||||||
def on_keydown(self, ev):
|
def on_keydown(self, ev):
|
||||||
ev.stopPropagation(), ev.preventDefault()
|
ev.stopPropagation(), ev.preventDefault()
|
||||||
if ev.key is 'Escape':
|
if ev.key is 'Escape':
|
||||||
@ -183,4 +202,6 @@ class ReadAloud:
|
|||||||
|
|
||||||
def handle_message(self, msg):
|
def handle_message(self, msg):
|
||||||
if msg.type is 'text-extracted':
|
if msg.type is 'text-extracted':
|
||||||
|
if msg.pos:
|
||||||
|
self.stop()
|
||||||
ui_operations.tts('play', {'marked_text': msg.marked_text})
|
ui_operations.tts('play', {'marked_text': msg.marked_text})
|
||||||
|
@ -191,17 +191,3 @@ def move_end_of_selection(pos, start):
|
|||||||
else:
|
else:
|
||||||
if r.endContainer is not p.offsetNode or r.endOffset is not p.offset:
|
if r.endContainer is not p.offsetNode or r.endOffset is not p.offset:
|
||||||
r.setEnd(p.offsetNode, p.offset)
|
r.setEnd(p.offsetNode, p.offset)
|
||||||
|
|
||||||
|
|
||||||
def range_for_tts(x, y):
|
|
||||||
p = None
|
|
||||||
if x? and y?:
|
|
||||||
p = caret_position_from_point(x, y)
|
|
||||||
if not p:
|
|
||||||
p = caret_position_from_point(0, 0)
|
|
||||||
if not p:
|
|
||||||
p = {'offsetNode': document.body, 'offset': 0}
|
|
||||||
r = document.createRange()
|
|
||||||
r.setStart(p.offsetNode, p.offset)
|
|
||||||
r.setEndAfter(document.body)
|
|
||||||
return r
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user