Skip to content

Commit a5cf766

Browse files
committed
Read aloud support and fixes added
1 parent 34aada5 commit a5cf766

1 file changed

Lines changed: 111 additions & 8 deletions

File tree

src/index.ios.js

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ function decodeBase64(base64) {
2323
return decoder.decode(base64ToBytes(base64));
2424
}
2525

26+
// Read Aloud annotation preview: while a highlight session is active the previewed annotation is rendered in the reader
27+
// but its saves are withheld from the app, so nothing is written to the database until the session is confirmed.
28+
let readAloudPreviewAnnotation = null;
29+
const readAloudPreviewIds = new Set();
30+
31+
// Serialize preview create/resize/confirm/cancel. Each is async (awaits the reader), so without a queue a rapid second
32+
// call would read a stale `readAloudPreviewAnnotation` (still null / the previous one) and create a duplicate preview
33+
// annotation — leaving an orphan and making the highlight jump or disappear as the session moves it.
34+
let readAloudPreviewQueue = Promise.resolve();
35+
function enqueueReadAloudPreview(task) {
36+
readAloudPreviewQueue = readAloudPreviewQueue.then(task).catch((error) => {
37+
log("Read Aloud preview operation failed: " + error);
38+
});
39+
}
40+
2641
window.createView = (options) => {
2742
log("Create " + options.type + " view");
2843
const annotations = JSON.parse(decodeBase64(options.annotations));
@@ -42,10 +57,15 @@ window.createView = (options) => {
4257
postMessage('onViewContentInitialized');
4358
},
4459
onSaveAnnotations: (annotations) => {
45-
postMessage('onSaveAnnotations', { annotations });
60+
// Withhold read-aloud preview annotations (not yet confirmed) so they aren't persisted mid-session.
61+
const saved = annotations.filter(annotation => !readAloudPreviewIds.has(annotation.id));
62+
if (!saved.length) {
63+
return;
64+
}
65+
postMessage('onSaveAnnotations', { annotations: saved });
4666

47-
if (annotations[0].type == "note") {
48-
window._view.selectAnnotations([annotations[0].id]);
67+
if (saved[0].type == "note") {
68+
window._view.selectAnnotations([saved[0].id]);
4969
}
5070
},
5171
onSetOutline: (outline) => {
@@ -160,11 +180,94 @@ window.getReadAloudSegments = async (options) => {
160180
postMessage('onReadAloudSegments', { requestID: options.requestID, segments });
161181
};
162182

163-
window.setReadAloudAnnotation = async (options) => {
164-
const params = JSON.parse(decodeBase64(options.params));
165-
log("Set Read Aloud annotation: " + params.type);
166-
const annotation = await window._view.setReadAloudAnnotation(params);
167-
postMessage('onReadAloudAnnotation', { requestID: options.requestID, annotation });
183+
window.getReadAloudStartBlockIndex = async (options) => {
184+
// The structured-document-text block index currently in view, so playback can start where the reader is. Read at
185+
// play time (not load) so it reflects the current scroll position.
186+
let blockIndex = null;
187+
try {
188+
const sdt = await window._view._loadSDT();
189+
if (sdt) {
190+
blockIndex = window._view._view.getVisibleBlockIndex?.(sdt.structure) ?? null;
191+
}
192+
}
193+
catch (error) {
194+
log("Read Aloud start block index unavailable: " + error);
195+
}
196+
postMessage('onReadAloudStartBlockIndex', { requestID: options.requestID, blockIndex });
197+
};
198+
199+
window.setReadAloudAnnotation = (options) => {
200+
// Creates or resizes the highlight-session PREVIEW annotation. It renders in the reader but is withheld from the
201+
// app (see readAloudPreviewIds) until `confirmReadAloudAnnotation`. Resizes the current preview if one exists.
202+
// Queued so concurrent move/extend calls resize the single preview instead of racing to create duplicates.
203+
enqueueReadAloudPreview(async () => {
204+
const params = JSON.parse(decodeBase64(options.params));
205+
if (readAloudPreviewAnnotation) {
206+
params.id = readAloudPreviewAnnotation.id;
207+
}
208+
log("Set Read Aloud annotation preview: " + params.type);
209+
const annotation = await window._view.setReadAloudAnnotation(params);
210+
if (annotation) {
211+
readAloudPreviewAnnotation = annotation;
212+
readAloudPreviewIds.add(annotation.id);
213+
}
214+
postMessage('onReadAloudAnnotation', { requestID: options.requestID, annotation });
215+
});
216+
};
217+
218+
window.confirmReadAloudAnnotation = () => {
219+
// Confirm the session: stop withholding the preview annotation and report it as a normal save so it is persisted.
220+
enqueueReadAloudPreview(async () => {
221+
if (!readAloudPreviewAnnotation) {
222+
return;
223+
}
224+
const annotation = readAloudPreviewAnnotation;
225+
readAloudPreviewIds.delete(annotation.id);
226+
readAloudPreviewAnnotation = null;
227+
log("Confirm Read Aloud annotation");
228+
postMessage('onSaveAnnotations', { annotations: [annotation] });
229+
});
230+
};
231+
232+
window.cancelReadAloudAnnotation = () => {
233+
// Discard the session: remove the preview annotation from the reader. onDelete is a no-op on iOS, so nothing is
234+
// persisted (it was never saved). Safe no-op if already confirmed.
235+
enqueueReadAloudPreview(async () => {
236+
if (!readAloudPreviewAnnotation) {
237+
return;
238+
}
239+
const id = readAloudPreviewAnnotation.id;
240+
readAloudPreviewIds.delete(id);
241+
readAloudPreviewAnnotation = null;
242+
log("Cancel Read Aloud annotation");
243+
window._view.unsetAnnotations([id]);
244+
});
245+
};
246+
247+
window.setReadAloudSpotlight = async (options) => {
248+
// Spotlight the currently-read segment. `anchor` is an SDT position ({ start, end }); omit it (or pass null) to clear.
249+
const anchor = options.anchor ? JSON.parse(decodeBase64(options.anchor)) : null;
250+
log("Set Read Aloud spotlight: " + (anchor ? JSON.stringify(anchor) : "clear"));
251+
const position = anchor ? await window._view.sdtAnchorToPosition(anchor) : null;
252+
try {
253+
window._view.setReadAloudSpotlight(position);
254+
}
255+
catch (error) {
256+
log("Read Aloud spotlight failed: " + error);
257+
}
258+
// Follow the reading position: scroll/turn to the current segment. `navigateToSelector` moves the view to a raw
259+
// selector; the reader's own read-aloud follow (read-aloud.ts) uses it the same way. (The built-in spotlight
260+
// navigate instead passes the selector to `navigate`, which only acts on `{ position }` / `{ annotationID }`
261+
// locations and is therefore a no-op — that's why the view never moved.) Options mirror desktop: `ifNeeded` skips
262+
// the move when already visible, `block: 'center'` keeps the read text centered.
263+
if (position) {
264+
try {
265+
window._view._view.navigateToSelector(position, { ifNeeded: true, block: 'center', behavior: 'smooth' });
266+
}
267+
catch (error) {
268+
log("Read Aloud follow navigate failed: " + error);
269+
}
270+
}
168271
};
169272

170273
// Notify when iframe is loaded

0 commit comments

Comments
 (0)