feat(tts): hotkey to highlight the currently-spoken sentence (#4085) (#4368)

Add a "Highlight Current Sentence" keyboard action (default Shift+M, in the
Text to Speech shortcut section) that persists the sentence TTS is reading
aloud as a normal highlight using the user's default style/color — no text
selection, eyes-off, silent, and idempotent (a repeat press on the same
sentence is a no-op rather than a duplicate).

Flow: the shortcut handler in useBookShortcuts dispatches tts-highlight-sentence
→ useTTSControl (which owns the TTSController) resolves the current sentence via
the new TTSController.getSpokenSentence() and relays create-tts-highlight
→ Annotator builds the BookNote with the pure, unit-tested buildTTSSentenceHighlight
helper and persists/renders it like any other highlight.

Closes #4085

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-05-31 02:33:28 +08:00
committed by GitHub
parent c23c21d37d
commit ef603852b7
10 changed files with 1023 additions and 3 deletions
@@ -47,7 +47,11 @@ import { getWordCount } from '@/utils/word';
import { getIndexFromCfi, isCfiInLocation } from '@/utils/cfi';
import { TransformContext } from '@/services/transformers/types';
import { transformContent } from '@/services/transformService';
import { getHighlightColorHex, removeBookNoteOverlays } from '../../utils/annotatorUtil';
import {
buildTTSSentenceHighlight,
getHighlightColorHex,
removeBookNoteOverlays,
} from '../../utils/annotatorUtil';
import {
expandAllRenderedSections,
expandGlobalAnnotation,
@@ -536,10 +540,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
eventDispatcher.on('export-annotations', handleExportMarkdown);
eventDispatcher.on('clear-annotations', handleClearAnnotations);
eventDispatcher.on('import-annotations', handleImportAnnotations);
eventDispatcher.on('create-tts-highlight', handleCreateTTSHighlight);
return () => {
eventDispatcher.off('export-annotations', handleExportMarkdown);
eventDispatcher.off('clear-annotations', handleClearAnnotations);
eventDispatcher.off('import-annotations', handleImportAnnotations);
eventDispatcher.off('create-tts-highlight', handleCreateTTSHighlight);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -839,6 +845,29 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}
};
const handleCreateTTSHighlight = (event: CustomEvent) => {
const detail = event.detail as { bookKey: string; cfi: string; text: string } | undefined;
if (!detail || detail.bookKey !== bookKey) return;
const { settings } = useSettingsStore.getState();
const style = settings.globalReadSettings.highlightStyle;
const color = settings.globalReadSettings.highlightStyles[style];
const { booknotes: annotations = [] } = getConfig(bookKey)!;
const page = getProgress(bookKey)?.page;
const annotation = buildTTSSentenceHighlight(
annotations,
{ cfi: detail.cfi, text: detail.text, style, color, page },
Date.now(),
);
if (!annotation) return;
annotations.push(annotation);
const updatedConfig = updateBooknotes(bookKey, annotations);
if (updatedConfig) {
saveConfig(envConfig, bookKey, updatedConfig, settings);
}
const views = getViewsById(bookKey.split('-')[0]!);
views.forEach((view) => view?.addAnnotation(annotation));
};
/**
* Toggle the `global` flag on the annotation currently anchored at
* `selection.cfi`. When enabling, fan out overlays for every other