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)
|
highlights_changed = from_js(object)
|
||||||
open_url = from_js(object)
|
open_url = from_js(object)
|
||||||
speak_simple_text = from_js(object)
|
speak_simple_text = from_js(object)
|
||||||
|
tts = from_js(object, object)
|
||||||
|
|
||||||
create_view = to_js()
|
create_view = to_js()
|
||||||
start_book_load = to_js()
|
start_book_load = to_js()
|
||||||
@ -519,6 +520,7 @@ class WebView(RestartingWebEngineView):
|
|||||||
self.bridge.highlights_changed.connect(self.highlights_changed)
|
self.bridge.highlights_changed.connect(self.highlights_changed)
|
||||||
self.bridge.open_url.connect(safe_open_url)
|
self.bridge.open_url.connect(safe_open_url)
|
||||||
self.bridge.speak_simple_text.connect(self.speak_simple_text)
|
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.bridge.export_shortcut_map.connect(self.set_shortcut_map)
|
||||||
self.shortcut_map = {}
|
self.shortcut_map = {}
|
||||||
self.bridge.report_cfi.connect(self.call_callback)
|
self.bridge.report_cfi.connect(self.call_callback)
|
||||||
@ -553,6 +555,9 @@ class WebView(RestartingWebEngineView):
|
|||||||
except TTSSystemUnavailable as err:
|
except TTSSystemUnavailable as err:
|
||||||
return error_dialog(self, _('Text-to-Speech unavailable'), str(err), show=True)
|
return error_dialog(self, _('Text-to-Speech unavailable'), str(err), show=True)
|
||||||
|
|
||||||
|
def tts_action(self, action, data):
|
||||||
|
pass
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
if self._tts_client is not None:
|
if self._tts_client is not None:
|
||||||
self._tts_client.shutdown()
|
self._tts_client.shutdown()
|
||||||
|
@ -4,7 +4,7 @@ from __python__ import bound_methods, hash_literals
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
from gettext import gettext as _
|
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 fs_images import fix_fullscreen_svg_images
|
||||||
from iframe_comm import IframeClient
|
from iframe_comm import IframeClient
|
||||||
@ -143,6 +143,7 @@ class IframeBoss:
|
|||||||
'show_search_result': self.show_search_result,
|
'show_search_result': self.show_search_result,
|
||||||
'handle_navigation_shortcut': self.on_handle_navigation_shortcut,
|
'handle_navigation_shortcut': self.on_handle_navigation_shortcut,
|
||||||
'annotations': self.annotations_msg_received,
|
'annotations': self.annotations_msg_received,
|
||||||
|
'tts': self.tts_msg_received,
|
||||||
'copy_selection': self.copy_selection,
|
'copy_selection': self.copy_selection,
|
||||||
'replace_highlights': self.replace_highlights,
|
'replace_highlights': self.replace_highlights,
|
||||||
'clear_selection': def(): window.getSelection().removeAllRanges();,
|
'clear_selection': def(): window.getSelection().removeAllRanges();,
|
||||||
@ -546,7 +547,8 @@ class IframeBoss:
|
|||||||
annot_id = highlight_associated_with_selection(sel, annot_id_uuid_map)
|
annot_id = highlight_associated_with_selection(sel, annot_id_uuid_map)
|
||||||
r = sel.getRangeAt(0)
|
r = sel.getRangeAt(0)
|
||||||
start_is_anchor = r.startContainer is sel.anchorNode and r.startOffset is sel.anchorOffset
|
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(
|
self.send_message(
|
||||||
'selectionchange', text=text, empty=v'!!collapsed', annot_id=annot_id,
|
'selectionchange', text=text, empty=v'!!collapsed', annot_id=annot_id,
|
||||||
drag_mouse_position=drag_mouse_position, selection_change_caused_by_search=by_search,
|
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())
|
container.appendChild(s.getRangeAt(i).cloneContents())
|
||||||
self.send_message('copy_text_to_clipboard', text=text, html=container.innerHTML)
|
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():
|
def main():
|
||||||
main.boss = IframeBoss()
|
main.boss = IframeBoss()
|
||||||
|
@ -3,29 +3,35 @@
|
|||||||
from __python__ import bound_methods, hash_literals
|
from __python__ import bound_methods, hash_literals
|
||||||
|
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
from book_list.theme import get_color
|
from book_list.theme import get_color
|
||||||
from dom import clear, unique_id
|
from dom import clear, unique_id, svgicon
|
||||||
from read_book.globals import runtime
|
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
|
HIDDEN = 0
|
||||||
PAUSED = 1
|
WAITING_FOR_PLAY_TO_START = 1
|
||||||
PLAYING = 2
|
PAUSED = 2
|
||||||
|
PLAYING = 3
|
||||||
|
STOPPED = 4
|
||||||
|
|
||||||
|
|
||||||
class ReadAloud:
|
class ReadAloud:
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view):
|
||||||
self.view = view
|
self.view = view
|
||||||
self.state = HIDDEN
|
self._state = HIDDEN
|
||||||
self.bar_id = unique_id('bar')
|
self.bar_id = unique_id('bar')
|
||||||
container = self.container
|
container = self.container
|
||||||
container.setAttribute('tabindex', '0')
|
container.setAttribute('tabindex', '0')
|
||||||
container.style.overflow = 'hidden'
|
container.style.overflow = 'hidden'
|
||||||
|
container.style.textAlign = 'right'
|
||||||
container.appendChild(E.div(
|
container.appendChild(E.div(
|
||||||
id=self.bar_id,
|
id=self.bar_id,
|
||||||
style='position: fixed; border: solid 1px currentColor; border-radius: 5px;'
|
style='position: static; border: solid 1px currentColor; border-radius: 5px;'
|
||||||
'right: 95vw; top: 0; display: flex; flex-direction: column;'
|
'display: inline-flex; flex-direction: column; margin: 1rem;'
|
||||||
))
|
))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -44,7 +50,19 @@ class ReadAloud:
|
|||||||
def is_visible(self):
|
def is_visible(self):
|
||||||
return self.container.style.display is not 'none'
|
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):
|
def hide(self):
|
||||||
|
if self.state is not HIDDEN:
|
||||||
|
ui_operations.tts('stop')
|
||||||
self.state = HIDDEN
|
self.state = HIDDEN
|
||||||
self.container.style.display = 'none'
|
self.container.style.display = 'none'
|
||||||
self.view.focus_iframe()
|
self.view.focus_iframe()
|
||||||
@ -52,13 +70,15 @@ class ReadAloud:
|
|||||||
def show(self):
|
def show(self):
|
||||||
if self.state is HIDDEN:
|
if self.state is HIDDEN:
|
||||||
self.container.style.display = 'block'
|
self.container.style.display = 'block'
|
||||||
self.state = PAUSED
|
self.state = STOPPED
|
||||||
self.focus()
|
self.focus()
|
||||||
|
|
||||||
def focus(self):
|
def focus(self):
|
||||||
self.container.focus()
|
self.container.focus()
|
||||||
|
|
||||||
def build_bar(self, annot_id):
|
def build_bar(self, annot_id):
|
||||||
|
if self.state is HIDDEN:
|
||||||
|
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(50rem, 90vw)' if self.supports_css_min_max else '50rem'
|
||||||
@ -76,4 +96,49 @@ class ReadAloud:
|
|||||||
]:
|
]:
|
||||||
bar_container.appendChild(x)
|
bar_container.appendChild(x)
|
||||||
bar = bar_container.firstChild
|
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'),
|
_('Show/hide Table of Contents'),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
'read_aloud': desc(
|
||||||
|
'Ctrl+s',
|
||||||
|
'ui',
|
||||||
|
_('Read aloud')
|
||||||
|
),
|
||||||
|
|
||||||
'copy_to_clipboard': desc(
|
'copy_to_clipboard': desc(
|
||||||
v"['Ctrl+c', 'Meta+c']",
|
v"['Ctrl+c', 'Meta+c']",
|
||||||
'ui',
|
'ui',
|
||||||
|
@ -294,6 +294,7 @@ class View:
|
|||||||
ui_operations.search_result_not_found(data.search_result)
|
ui_operations.search_result_not_found(data.search_result)
|
||||||
,
|
,
|
||||||
'annotations': self.on_annotations_message,
|
'annotations': self.on_annotations_message,
|
||||||
|
'tts': self.on_tts_message,
|
||||||
'copy_text_to_clipboard': def(data):
|
'copy_text_to_clipboard': def(data):
|
||||||
ui_operations.copy_selection(data.text, data.html)
|
ui_operations.copy_selection(data.text, data.html)
|
||||||
,
|
,
|
||||||
@ -345,6 +346,9 @@ class View:
|
|||||||
def on_annotations_message(self, data):
|
def on_annotations_message(self, data):
|
||||||
self.selection_bar.handle_message(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):
|
def left_margin_clicked(self, event):
|
||||||
if event.button is 0:
|
if event.button is 0:
|
||||||
event.preventDefault(), event.stopPropagation()
|
event.preventDefault(), event.stopPropagation()
|
||||||
@ -501,6 +505,8 @@ class View:
|
|||||||
ui_operations.toggle_full_screen()
|
ui_operations.toggle_full_screen()
|
||||||
elif data.name is 'toggle_reference_mode':
|
elif data.name is 'toggle_reference_mode':
|
||||||
self.toggle_reference_mode()
|
self.toggle_reference_mode()
|
||||||
|
elif data.name is 'read_aloud':
|
||||||
|
self.start_read_aloud()
|
||||||
elif data.name is 'reload_book':
|
elif data.name is 'reload_book':
|
||||||
ui_operations.reload_book()
|
ui_operations.reload_book()
|
||||||
elif data.name is 'next_section':
|
elif data.name is 'next_section':
|
||||||
@ -668,6 +674,10 @@ class View:
|
|||||||
else:
|
else:
|
||||||
self.iframe.contentWindow.focus()
|
self.iframe.contentWindow.focus()
|
||||||
|
|
||||||
|
def start_read_aloud(self):
|
||||||
|
self.selection_bar.hide()
|
||||||
|
self.read_aloud.show()
|
||||||
|
|
||||||
def show_chrome(self, data):
|
def show_chrome(self, data):
|
||||||
elements = {}
|
elements = {}
|
||||||
if data and data.elements:
|
if data and data.elements:
|
||||||
|
@ -191,3 +191,17 @@ 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
|
||||||
|
@ -400,6 +400,8 @@ if window is window.top:
|
|||||||
to_python.highlights_changed(amap.highlight)
|
to_python.highlights_changed(amap.highlight)
|
||||||
ui_operations.speak_simple_text = def(text):
|
ui_operations.speak_simple_text = def(text):
|
||||||
to_python.speak_simple_text(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'))
|
document.body.appendChild(E.div(id='view'))
|
||||||
window.onerror = onerror
|
window.onerror = onerror
|
||||||
|
Loading…
x
Reference in New Issue
Block a user