diff --git a/src/common/view.js b/src/common/view.js index c99ed661..221aff70 100644 --- a/src/common/view.js +++ b/src/common/view.js @@ -431,6 +431,16 @@ class View { return sdt ? sdt.mapper.sdtToSourcePosition(sdtAnchor) : null; } + /** + * Top-level structured-document-text block index currently in view, or null. Used to start Read Aloud playback + * where the reader is. + * @returns {Promise} + */ + async getVisibleBlockIndex() { + let sdt = await this._loadSDT(); + return sdt ? (this._view.getVisibleBlockIndex?.(sdt.structure) ?? null) : null; + } + async createAnnotationFromSDT({ sdtAnchor, type, color, comment, tags }) { let sdt = await this._loadSDT(); if (!sdt) { diff --git a/src/index.ios.js b/src/index.ios.js index 5da98e2f..1785394f 100644 --- a/src/index.ios.js +++ b/src/index.ios.js @@ -23,6 +23,18 @@ function decodeBase64(base64) { return decoder.decode(base64ToBytes(base64)); } +// Read Aloud annotation preview: while a highlight session is active the previewed annotation is rendered in the reader +// but its saves are withheld from the app, so nothing is written to the database until the session is confirmed. +let readAloudPreviewAnnotation = null; +const readAloudPreviewIds = new Set(); + +let readAloudPreviewQueue = Promise.resolve(); +function enqueueReadAloudPreview(task) { + readAloudPreviewQueue = readAloudPreviewQueue.then(task).catch((error) => { + log("Read Aloud preview operation failed: " + error); + }); +} + window.createView = (options) => { log("Create " + options.type + " view"); const annotations = JSON.parse(decodeBase64(options.annotations)); @@ -42,10 +54,15 @@ window.createView = (options) => { postMessage('onViewContentInitialized'); }, onSaveAnnotations: (annotations) => { - postMessage('onSaveAnnotations', { annotations }); + // Withhold read-aloud preview annotations (not yet confirmed) so they aren't persisted mid-session. + const saved = annotations.filter(annotation => !readAloudPreviewIds.has(annotation.id)); + if (!saved.length) { + return; + } + postMessage('onSaveAnnotations', { annotations: saved }); - if (annotations[0].type == "note") { - window._view.selectAnnotations([annotations[0].id]); + if (saved[0].type == "note") { + window._view.selectAnnotations([saved[0].id]); } }, onSetOutline: (outline) => { @@ -160,11 +177,65 @@ window.getReadAloudSegments = async (options) => { postMessage('onReadAloudSegments', { requestID: options.requestID, segments }); }; -window.setReadAloudAnnotation = async (options) => { - const params = JSON.parse(decodeBase64(options.params)); - log("Set Read Aloud annotation: " + params.type); - const annotation = await window._view.setReadAloudAnnotation(params); - postMessage('onReadAloudAnnotation', { requestID: options.requestID, annotation }); +window.getReadAloudStartBlockIndex = async (options) => { + let blockIndex = null; + try { + blockIndex = await window._view.getVisibleBlockIndex(); + } + catch (error) { + log("Read Aloud start block index unavailable: " + error); + } + postMessage('onReadAloudStartBlockIndex', { requestID: options.requestID, blockIndex }); +}; + +window.setReadAloudAnnotation = (options) => { + enqueueReadAloudPreview(async () => { + const params = JSON.parse(decodeBase64(options.params)); + if (readAloudPreviewAnnotation) { + params.id = readAloudPreviewAnnotation.id; + } + log("Set Read Aloud annotation preview: " + params.type); + const annotation = await window._view.setReadAloudAnnotation(params); + if (annotation) { + readAloudPreviewAnnotation = annotation; + readAloudPreviewIds.add(annotation.id); + } + postMessage('onReadAloudAnnotation', { requestID: options.requestID, annotation }); + }); +}; + +window.confirmReadAloudAnnotation = () => { + // Confirm the session: stop withholding the preview annotation and report it as a normal save so it is persisted. + enqueueReadAloudPreview(async () => { + if (!readAloudPreviewAnnotation) { + return; + } + const annotation = readAloudPreviewAnnotation; + readAloudPreviewIds.delete(annotation.id); + readAloudPreviewAnnotation = null; + log("Confirm Read Aloud annotation"); + postMessage('onSaveAnnotations', { annotations: [annotation] }); + }); +}; + +window.cancelReadAloudAnnotation = () => { + enqueueReadAloudPreview(async () => { + if (!readAloudPreviewAnnotation) { + return; + } + const id = readAloudPreviewAnnotation.id; + readAloudPreviewIds.delete(id); + readAloudPreviewAnnotation = null; + log("Cancel Read Aloud annotation"); + window._view.unsetAnnotations([id]); + }); +}; + +window.setReadAloudSpotlight = async (options) => { + const anchor = options.anchor ? JSON.parse(decodeBase64(options.anchor)) : null; + log("Set Read Aloud spotlight: " + (anchor ? JSON.stringify(anchor) : "clear")); + const position = anchor ? await window._view.sdtAnchorToPosition(anchor) : null; + window._view.setReadAloudSpotlight(position); }; // Notify when iframe is loaded