mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
More work on Read Aloud
This commit is contained in:
parent
4fd521a88b
commit
eaccc523af
1
imgsrc/srv/hourglass.svg
Normal file
1
imgsrc/srv/hourglass.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1536 128q0 261-106.5 461.5t-266.5 306.5q160 106 266.5 306.5t106.5 461.5h96q14 0 23 9t9 23v64q0 14-9 23t-23 9h-1472q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h96q0-261 106.5-461.5t266.5-306.5q-160-106-266.5-306.5t-106.5-461.5h-96q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h1472q14 0 23 9t9 23v64q0 14-9 23t-23 9h-96zm-128 0h-1024q0 206 85 384h854q85-178 85-384zm-57 1216q-54-141-145.5-241.5t-194.5-142.5h-230q-103 42-194.5 142.5t-145.5 241.5h910z"/></svg>
|
After Width: | Height: | Size: 541 B |
1
imgsrc/srv/pause.svg
Normal file
1
imgsrc/srv/pause.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1664 192v1408q0 26-19 45t-45 19h-512q-26 0-45-19t-19-45v-1408q0-26 19-45t45-19h512q26 0 45 19t19 45zm-896 0v1408q0 26-19 45t-45 19h-512q-26 0-45-19t-19-45v-1408q0-26 19-45t45-19h512q26 0 45 19t19 45z"/></svg>
|
After Width: | Height: | Size: 309 B |
1
imgsrc/srv/play.svg
Normal file
1
imgsrc/srv/play.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1576 927l-1328 738q-23 13-39.5 3t-16.5-36v-1472q0-26 16.5-36t39.5 3l1328 738q23 13 23 31t-23 31z"/></svg>
|
After Width: | Height: | Size: 206 B |
@ -269,6 +269,7 @@ class ViewerBridge(Bridge):
|
||||
highlights_changed = from_js(object)
|
||||
open_url = from_js(object)
|
||||
speak_simple_text = from_js(object)
|
||||
tts = from_js(object, object)
|
||||
|
||||
create_view = to_js()
|
||||
start_book_load = to_js()
|
||||
@ -519,6 +520,7 @@ class WebView(RestartingWebEngineView):
|
||||
self.bridge.highlights_changed.connect(self.highlights_changed)
|
||||
self.bridge.open_url.connect(safe_open_url)
|
||||
self.bridge.speak_simple_text.connect(self.speak_simple_text)
|
||||
self.bridge.tts.connect(self.tts_action)
|
||||
self.bridge.export_shortcut_map.connect(self.set_shortcut_map)
|
||||
self.shortcut_map = {}
|
||||
self.bridge.report_cfi.connect(self.call_callback)
|
||||
@ -553,6 +555,9 @@ class WebView(RestartingWebEngineView):
|
||||
except TTSSystemUnavailable as err:
|
||||
return error_dialog(self, _('Text-to-Speech unavailable'), str(err), show=True)
|
||||
|
||||
def tts_action(self, action, data):
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
if self._tts_client is not None:
|
||||
self._tts_client.shutdown()
|
||||
|
@ -4,7 +4,7 @@ from __python__ import bound_methods, hash_literals
|
||||
|
||||
import traceback
|
||||
from gettext import gettext as _
|
||||
from select import move_end_of_selection, selection_extents, word_at_point
|
||||
from select import move_end_of_selection, selection_extents, word_at_point, range_for_tts
|
||||
|
||||
from fs_images import fix_fullscreen_svg_images
|
||||
from iframe_comm import IframeClient
|
||||
@ -143,6 +143,7 @@ class IframeBoss:
|
||||
'show_search_result': self.show_search_result,
|
||||
'handle_navigation_shortcut': self.on_handle_navigation_shortcut,
|
||||
'annotations': self.annotations_msg_received,
|
||||
'tts': self.tts_msg_received,
|
||||
'copy_selection': self.copy_selection,
|
||||
'replace_highlights': self.replace_highlights,
|
||||
'clear_selection': def(): window.getSelection().removeAllRanges();,
|
||||
@ -546,7 +547,8 @@ class IframeBoss:
|
||||
annot_id = highlight_associated_with_selection(sel, annot_id_uuid_map)
|
||||
r = sel.getRangeAt(0)
|
||||
start_is_anchor = r.startContainer is sel.anchorNode and r.startOffset is sel.anchorOffset
|
||||
by_search = window.performance.now() - self.last_search_at < 1000
|
||||
now = window.performance.now()
|
||||
by_search = now - self.last_search_at < 1000
|
||||
self.send_message(
|
||||
'selectionchange', text=text, empty=v'!!collapsed', annot_id=annot_id,
|
||||
drag_mouse_position=drag_mouse_position, selection_change_caused_by_search=by_search,
|
||||
@ -890,5 +892,11 @@ class IframeBoss:
|
||||
container.appendChild(s.getRangeAt(i).cloneContents())
|
||||
self.send_message('copy_text_to_clipboard', text=text, html=container.innerHTML)
|
||||
|
||||
def tts_msg_received(self, data):
|
||||
if data.type is 'play':
|
||||
r = range_for_tts()
|
||||
text = r.toString()
|
||||
self.send_message('tts', type='text-extracted', text=text)
|
||||
|
||||
def main():
|
||||
main.boss = IframeBoss()
|
||||
|
@ -3,29 +3,35 @@
|
||||
from __python__ import bound_methods, hash_literals
|
||||
|
||||
from elementmaker import E
|
||||
from gettext import gettext as _
|
||||
|
||||
from book_list.theme import get_color
|
||||
from dom import clear, unique_id
|
||||
from read_book.globals import runtime
|
||||
from dom import clear, unique_id, svgicon
|
||||
from read_book.globals import runtime, ui_operations
|
||||
from read_book.selection_bar import BUTTON_MARGIN
|
||||
from read_book.highlights import ICON_SIZE
|
||||
|
||||
HIDDEN = 0
|
||||
PAUSED = 1
|
||||
PLAYING = 2
|
||||
WAITING_FOR_PLAY_TO_START = 1
|
||||
PAUSED = 2
|
||||
PLAYING = 3
|
||||
STOPPED = 4
|
||||
|
||||
|
||||
class ReadAloud:
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
self.state = HIDDEN
|
||||
self._state = HIDDEN
|
||||
self.bar_id = unique_id('bar')
|
||||
container = self.container
|
||||
container.setAttribute('tabindex', '0')
|
||||
container.style.overflow = 'hidden'
|
||||
container.style.textAlign = 'right'
|
||||
container.appendChild(E.div(
|
||||
id=self.bar_id,
|
||||
style='position: fixed; border: solid 1px currentColor; border-radius: 5px;'
|
||||
'right: 95vw; top: 0; display: flex; flex-direction: column;'
|
||||
style='position: static; border: solid 1px currentColor; border-radius: 5px;'
|
||||
'display: inline-flex; flex-direction: column; margin: 1rem;'
|
||||
))
|
||||
|
||||
@property
|
||||
@ -44,21 +50,35 @@ class ReadAloud:
|
||||
def is_visible(self):
|
||||
return self.container.style.display is not 'none'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, val):
|
||||
if val is not self._state:
|
||||
self._state = val
|
||||
self.build_bar()
|
||||
|
||||
def hide(self):
|
||||
self.state = HIDDEN
|
||||
self.container.style.display = 'none'
|
||||
self.view.focus_iframe()
|
||||
if self.state is not HIDDEN:
|
||||
ui_operations.tts('stop')
|
||||
self.state = HIDDEN
|
||||
self.container.style.display = 'none'
|
||||
self.view.focus_iframe()
|
||||
|
||||
def show(self):
|
||||
if self.state is HIDDEN:
|
||||
self.container.style.display = 'block'
|
||||
self.state = PAUSED
|
||||
self.state = STOPPED
|
||||
self.focus()
|
||||
|
||||
def focus(self):
|
||||
self.container.focus()
|
||||
|
||||
def build_bar(self, annot_id):
|
||||
if self.state is HIDDEN:
|
||||
return
|
||||
bar_container = self.bar
|
||||
clear(bar_container)
|
||||
bar_container.style.maxWidth = 'min(50rem, 90vw)' if self.supports_css_min_max else '50rem'
|
||||
@ -76,4 +96,49 @@ class ReadAloud:
|
||||
]:
|
||||
bar_container.appendChild(x)
|
||||
bar = bar_container.firstChild
|
||||
bar
|
||||
|
||||
def cb(name, icon, text):
|
||||
ans = svgicon(icon, ICON_SIZE, ICON_SIZE, text)
|
||||
if name:
|
||||
ans.addEventListener('click', def(ev):
|
||||
ev.stopPropagation(), ev.preventDefault()
|
||||
self[name](ev)
|
||||
self.view.focus_iframe()
|
||||
)
|
||||
ans.classList.add('simple-link')
|
||||
ans.style.marginLeft = ans.style.marginRight = BUTTON_MARGIN
|
||||
return ans
|
||||
|
||||
if self.state is PLAYING:
|
||||
bar.appendChild(cb('pause', 'pause', _('Pause reading')))
|
||||
elif self.state is WAITING_FOR_PLAY_TO_START:
|
||||
bar.appendChild(cb(None, 'hourglass', _('Pause reading')))
|
||||
else:
|
||||
bar.appendChild(cb('play', 'play', _('Start reading') if self.state is STOPPED else _('Resume reading')))
|
||||
bar.appendChild(cb('hide', 'close', _('Close Read aloud')))
|
||||
|
||||
def play(self):
|
||||
if self.state is PAUSED:
|
||||
ui_operations.tts('resume')
|
||||
self.state = PLAYING
|
||||
elif self.state is STOPPED:
|
||||
self.send_message('play')
|
||||
self.state = WAITING_FOR_PLAY_TO_START
|
||||
|
||||
def pause(self):
|
||||
if self.state is PLAYING:
|
||||
ui_operations.tts('pause')
|
||||
self.state = PAUSED
|
||||
|
||||
def toggle(self):
|
||||
if self.state is PLAYING:
|
||||
self.pause()
|
||||
elif self.state is PAUSED or self.state is STOPPED:
|
||||
self.play()
|
||||
|
||||
def send_message(self, type, **kw):
|
||||
self.view.iframe_wrapper.send_message('tts', type=type, **kw)
|
||||
|
||||
def handle_message(self, msg):
|
||||
if msg.type is 'text-extracted':
|
||||
ui_operations.tts('play', {'text': msg.text})
|
||||
|
@ -188,6 +188,12 @@ def shortcuts_definition():
|
||||
_('Show/hide Table of Contents'),
|
||||
),
|
||||
|
||||
'read_aloud': desc(
|
||||
'Ctrl+s',
|
||||
'ui',
|
||||
_('Read aloud')
|
||||
),
|
||||
|
||||
'copy_to_clipboard': desc(
|
||||
v"['Ctrl+c', 'Meta+c']",
|
||||
'ui',
|
||||
|
@ -294,6 +294,7 @@ class View:
|
||||
ui_operations.search_result_not_found(data.search_result)
|
||||
,
|
||||
'annotations': self.on_annotations_message,
|
||||
'tts': self.on_tts_message,
|
||||
'copy_text_to_clipboard': def(data):
|
||||
ui_operations.copy_selection(data.text, data.html)
|
||||
,
|
||||
@ -345,6 +346,9 @@ class View:
|
||||
def on_annotations_message(self, data):
|
||||
self.selection_bar.handle_message(data)
|
||||
|
||||
def on_tts_message(self, data):
|
||||
self.read_aloud.handle_message(data)
|
||||
|
||||
def left_margin_clicked(self, event):
|
||||
if event.button is 0:
|
||||
event.preventDefault(), event.stopPropagation()
|
||||
@ -501,6 +505,8 @@ class View:
|
||||
ui_operations.toggle_full_screen()
|
||||
elif data.name is 'toggle_reference_mode':
|
||||
self.toggle_reference_mode()
|
||||
elif data.name is 'read_aloud':
|
||||
self.start_read_aloud()
|
||||
elif data.name is 'reload_book':
|
||||
ui_operations.reload_book()
|
||||
elif data.name is 'next_section':
|
||||
@ -668,6 +674,10 @@ class View:
|
||||
else:
|
||||
self.iframe.contentWindow.focus()
|
||||
|
||||
def start_read_aloud(self):
|
||||
self.selection_bar.hide()
|
||||
self.read_aloud.show()
|
||||
|
||||
def show_chrome(self, data):
|
||||
elements = {}
|
||||
if data and data.elements:
|
||||
|
@ -191,3 +191,17 @@ def move_end_of_selection(pos, start):
|
||||
else:
|
||||
if r.endContainer is not p.offsetNode or r.endOffset is not 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
|
||||
|
@ -400,6 +400,8 @@ if window is window.top:
|
||||
to_python.highlights_changed(amap.highlight)
|
||||
ui_operations.speak_simple_text = def(text):
|
||||
to_python.speak_simple_text(text)
|
||||
ui_operations.tts = def(action, data):
|
||||
to_python.tts(action, data or v'{}')
|
||||
|
||||
document.body.appendChild(E.div(id='view'))
|
||||
window.onerror = onerror
|
||||
|
Loading…
x
Reference in New Issue
Block a user