Skip to content

Commit d57f0f2

Browse files
committed
Implement Read Aloud for EPUB and snapshot
1 parent 21cbfec commit d57f0f2

27 files changed

Lines changed: 881 additions & 172 deletions

res/icons/20/advanced-options.svg

Lines changed: 3 additions & 0 deletions
Loading

res/icons/20/format-text-focus.svg

Lines changed: 3 additions & 0 deletions
Loading

res/icons/20/pause.svg

Lines changed: 3 additions & 0 deletions
Loading

res/icons/20/play.svg

Lines changed: 3 additions & 0 deletions
Loading

res/icons/20/read-aloud.svg

Lines changed: 3 additions & 0 deletions
Loading

res/icons/20/skip-ahead.svg

Lines changed: 3 additions & 0 deletions
Loading

res/icons/20/skip-back.svg

Lines changed: 3 additions & 0 deletions
Loading

src/common/components/reader-ui.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import PasswordPopup from './modal-popup/password-popup';
1717
import PrintPopup from './modal-popup/print-popup';
1818
import AppearancePopup from "./modal-popup/appearance-popup";
1919
import ThemePopup from './modal-popup/theme-popup';
20+
import ReadAloudPopup from "./utility-popup/read-aloud-popup";
2021
import { bundle } from '../../fluent';
2122

2223
function View(props) {
@@ -132,7 +133,9 @@ const ReaderUI = React.forwardRef((props, ref) => {
132133
enableNavigateBack={viewStats.canNavigateBack}
133134
enableNavigateToPreviousPage={viewStats.canNavigateToPreviousPage}
134135
enableNavigateToNextPage={viewStats.canNavigateToNextPage}
136+
focusModeEnabled={viewStats.focusModeEnabled}
135137
appearancePopup={state.appearancePopup}
138+
readAloudState={state.readAloudState}
136139
findPopupOpen={findState.popupOpen}
137140
themes={state.themes}
138141
onChangeTheme={props.onChangeTheme}
@@ -151,6 +154,8 @@ const ReaderUI = React.forwardRef((props, ref) => {
151154
onChangeTool={props.onChangeTool}
152155
onOpenColorContextMenu={props.onOpenColorContextMenu}
153156
onToggleAppearancePopup={props.onToggleAppearancePopup}
157+
onChangeReadAloudState={props.onChangeReadAloudState}
158+
onToggleReadAloud={props.onToggleReadAloud}
154159
onToggleFind={props.onToggleFind}
155160
onToggleContextPane={props.onToggleContextPane}
156161
/>
@@ -252,6 +257,13 @@ const ReaderUI = React.forwardRef((props, ref) => {
252257
onClose={props.onCloseThemePopup}
253258
/>
254259
)}
260+
{state.readAloudState.active && (
261+
<ReadAloudPopup
262+
params={state.readAloudState}
263+
onChange={props.onChangeReadAloudState}
264+
onClose={() => props.onToggleReadAloud(false)}
265+
/>
266+
)}
255267
<div id="a11yAnnouncement" aria-live="polite"></div>
256268
</Fragment>
257269
</LocalizationProvider>

src/common/components/toolbar.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import IconChevronLeft from '../../../res/icons/20/chevron-left.svg';
1414
import IconChevronUp from '../../../res/icons/20/chevron-up.svg';
1515
import IconChevronDown from '../../../res/icons/20/chevron-down.svg';
1616
import IconFormatText from '../../../res/icons/20/format-text.svg';
17+
import IconFormatTextFocus from '../../../res/icons/20/format-text-focus.svg';
1718
import IconHighlight from '../../../res/icons/20/annotate-highlight.svg';
1819
import IconUnderline from '../../../res/icons/20/annotate-underline.svg';
1920
import IconNote from '../../../res/icons/20/annotate-note.svg';
@@ -22,6 +23,7 @@ import IconImage from '../../../res/icons/20/annotate-area.svg';
2223
import IconInk from '../../../res/icons/20/annotate-ink.svg';
2324
import IconEraser from '../../../res/icons/20/annotate-eraser.svg';
2425
import IconFind from '../../../res/icons/20/magnifier.svg';
26+
import IconReadAloud from '../../../res/icons/20/read-aloud.svg';
2527
import IconChevronDown8 from '../../../res/icons/8/chevron-8.svg';
2628

2729
function Toolbar(props) {
@@ -111,8 +113,17 @@ function Toolbar(props) {
111113
className={cx('toolbar-button', { active: props.appearancePopup })}
112114
title={l10n.getString('reader-appearance')}
113115
tabIndex={-1}
114-
onClick={props.onToggleAppearancePopup}
115-
><IconFormatText/></button>
116+
onClick={() => props.onToggleAppearancePopup()}
117+
>{props.focusModeEnabled ? <IconFormatTextFocus/> : <IconFormatText/>}</button>
118+
{['epub', 'snapshot'].includes(props.type) && (
119+
<button
120+
id="read-aloud"
121+
className={cx('toolbar-button', { active: props.readAloudState.active })}
122+
title={l10n.getString('reader-read-aloud')}
123+
tabIndex={-1}
124+
onClick={() => props.onToggleReadAloud()}
125+
><IconReadAloud/></button>
126+
)}
116127
<div className="divider"/>
117128
<button
118129
id="navigateBack"
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import cx from 'classnames';
3+
4+
const EDGE_PADDING = 10;
5+
6+
function UtilityPopup(props) {
7+
let { children, className } = props;
8+
9+
let ref = useRef();
10+
let [dragOrigin, setDragOrigin] = useState(null);
11+
let [x, setX] = useState(null);
12+
let [y, setY] = useState(null);
13+
14+
let [windowWidth, windowHeight] = useWindowSize();
15+
16+
useEffect(() => {
17+
if (!ref.current || x === null || y === null) {
18+
return;
19+
}
20+
let left = Math.max(Math.min(x, windowWidth - ref.current.offsetWidth - EDGE_PADDING), EDGE_PADDING);
21+
let top = Math.max(Math.min(y, windowHeight - ref.current.offsetHeight - EDGE_PADDING), EDGE_PADDING);
22+
ref.current.style.left = `${left}px`;
23+
ref.current.style.top = `${top}px`;
24+
}, [x, y, windowWidth, windowHeight]);
25+
26+
function getOffset(event) {
27+
let boundingRect = ref.current.getBoundingClientRect();
28+
return [event.clientX - boundingRect.x, event.clientY - boundingRect.y];
29+
}
30+
31+
let handlePointerDown = (event) => {
32+
if (event.button !== 0 || event.target.closest('input, button, select, a')) {
33+
return;
34+
}
35+
ref.current.setPointerCapture(event.pointerId);
36+
setDragOrigin(getOffset(event));
37+
};
38+
39+
let handlePointerMove = (event) => {
40+
if (!dragOrigin || !ref.current.hasPointerCapture(event.pointerId)) {
41+
return;
42+
}
43+
let x = event.clientX - dragOrigin[0];
44+
let y = event.clientY - dragOrigin[1];
45+
setX(x);
46+
setY(y);
47+
};
48+
49+
let handlePointerUp = (event) => {
50+
if (!dragOrigin || !ref.current.hasPointerCapture(event.pointerId)) {
51+
return;
52+
}
53+
ref.current.releasePointerCapture(event.pointerId);
54+
setDragOrigin(null);
55+
};
56+
57+
return (
58+
<div
59+
className={cx('utility-popup', className)}
60+
role="application"
61+
onPointerDown={handlePointerDown}
62+
onPointerMove={handlePointerMove}
63+
onPointerUp={handlePointerUp}
64+
onPointerCancel={handlePointerUp}
65+
style={{ pointerEvents: dragOrigin ? 'none' : 'auto' }}
66+
ref={ref}
67+
>
68+
{children}
69+
</div>
70+
);
71+
}
72+
73+
function useWindowSize(win = window) {
74+
const [size, setSize] = useState([win.innerWidth, win.innerHeight]);
75+
76+
useEffect(() => {
77+
let handleResize = () => {
78+
setSize([win.innerWidth, win.innerHeight]);
79+
};
80+
win.addEventListener('resize', handleResize);
81+
return () => {
82+
win.removeEventListener('resize', handleResize);
83+
};
84+
}, [win]);
85+
86+
return size;
87+
}
88+
89+
export default UtilityPopup;

0 commit comments

Comments
 (0)