diff --git a/apps/readest-app/src/app/reader/components/FooterBar.tsx b/apps/readest-app/src/app/reader/components/FooterBar.tsx index 7a68451e..b7200670 100644 --- a/apps/readest-app/src/app/reader/components/FooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/FooterBar.tsx @@ -109,11 +109,10 @@ const FooterBar: React.FC = ({ const handleSpeakText = async () => { if (!view || !progress) return; - const { range } = progress; if (eventDispatcher.dispatchSync('tts-is-speaking')) { eventDispatcher.dispatch('tts-stop', { bookKey }); } else { - eventDispatcher.dispatch('tts-speak', { bookKey, range }); + eventDispatcher.dispatch('tts-speak', { bookKey }); } }; diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx index 2fdf2da8..ff40ba5f 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx @@ -12,6 +12,7 @@ import { parseSSMLLang } from '@/utils/ssml'; import { getOSPlatform } from '@/utils/misc'; import { throttle } from '@/utils/throttle'; import { invokeUseBackgroundAudio } from '@/utils/bridge'; +import { CFI } from '@/libs/document'; import Popup from '@/components/Popup'; import TTSPanel from './TTSPanel'; import TTSIcon from './TTSIcon'; @@ -107,7 +108,7 @@ const TTSControl = () => { const progress = getProgress(bookKey); const { sectionLabel } = progress || {}; const mark = (e as CustomEvent).detail; - if ('mediaSession' in navigator) { + if (appService?.isMobileApp && 'mediaSession' in navigator) { navigator.mediaSession.metadata = new MediaMetadata({ title: mark.text, artist: sectionLabel || title, @@ -127,9 +128,10 @@ const TTSControl = () => { const handleTTSSpeak = async (event: CustomEvent) => { const { bookKey, range } = event.detail; const view = getView(bookKey); + const progress = getProgress(bookKey); const viewSettings = getViewSettings(bookKey); const bookData = getBookData(bookKey); - if (!view || !viewSettings || !bookData || !bookData.book) return; + if (!view || !progress || !viewSettings || !bookData || !bookData.book) return; if (bookData.book?.format === 'PDF') { eventDispatcher.dispatch('toast', { message: _('TTS not supported for PDF'), @@ -138,6 +140,21 @@ const TTSControl = () => { return; } + let ttsFromRange = range || progress.range; + if (viewSettings.ttsLocation) { + const { location } = progress; + const ttsCfi = viewSettings.ttsLocation; + const start = CFI.collapse(location); + const end = CFI.collapse(location, true); + if (CFI.compare(start, ttsCfi) * CFI.compare(end, ttsCfi) <= 0) { + const { index, anchor } = view.resolveCFI(ttsCfi); + const { doc } = view.renderer.getContents().find((x) => (x.index = index)) || {}; + if (doc) { + ttsFromRange = anchor(doc) || ttsFromRange; + } + } + } + const primaryLang = bookData.book.primaryLanguage; setBookKey(bookKey); @@ -160,7 +177,7 @@ const TTSControl = () => { const ttsController = new TTSController(view); await ttsController.init(); await ttsController.initViewTTS(); - const ssml = view.tts?.from(range); + const ssml = view.tts?.from(ttsFromRange); if (ssml) { let lang = parseSSMLLang(ssml) || 'en'; // We will not trust 'en' language from ssml, as it may be a fallback or hardcoded value diff --git a/apps/readest-app/src/app/reader/hooks/useTextSelector.ts b/apps/readest-app/src/app/reader/hooks/useTextSelector.ts index 8a086866..66963f3e 100644 --- a/apps/readest-app/src/app/reader/hooks/useTextSelector.ts +++ b/apps/readest-app/src/app/reader/hooks/useTextSelector.ts @@ -12,10 +12,10 @@ export const useTextSelector = ( handleDismissPopup: () => void, ) => { const { getBookData } = useBookDataStore(); - const { getView, getViewSettings } = useReaderStore(); + const { getView, getViewState, getViewSettings, setViewSettings } = useReaderStore(); const view = getView(bookKey); - const viewSettings = getViewSettings(bookKey)!; const bookData = getBookData(bookKey)!; + const viewSettings = getViewSettings(bookKey)!; const primaryLang = bookData.book?.primaryLanguage || 'en'; const osPlatform = getOSPlatform(); @@ -81,9 +81,17 @@ export const useTextSelector = ( // On Android no proper events are fired to notify selection done, // we make the popup show when the selection is changed // note that selection may be initiated by a tts speak - if (osPlatform === 'android' && isTouchStarted.current) { + if (isTouchStarted.current && osPlatform === 'android') { makeSelection(sel, index, false); } + if (!isTouchStarted.current && getViewState(bookKey)?.ttsEnabled) { + const viewSettings = getViewSettings(bookKey)!; + const cfi = view?.getCFI(index, sel.getRangeAt(0)); + if (cfi) { + viewSettings.ttsLocation = cfi || ''; + setViewSettings(bookKey, viewSettings); + } + } isUpToPopup.current = true; }; const handlePointerup = (doc: Document, index: number) => { diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index 8189b497..93e812e0 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -146,6 +146,7 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = { export const DEFAULT_TTS_CONFIG: TTSConfig = { ttsRate: 1.3, ttsVoice: '', + ttsLocation: '', }; export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = { diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index c733d822..b520e344 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -125,6 +125,7 @@ export interface ViewConfig { export interface TTSConfig { ttsRate: number; ttsVoice: string; + ttsLocation: string; } export interface TranslatorConfig { diff --git a/apps/readest-app/src/types/view.ts b/apps/readest-app/src/types/view.ts index dfaa67c6..9b379a00 100644 --- a/apps/readest-app/src/types/view.ts +++ b/apps/readest-app/src/types/view.ts @@ -15,6 +15,7 @@ export interface FoliateView extends HTMLElement { goLeft: () => void; goRight: () => void; getCFI: (index: number, range: Range) => string; + resolveCFI: (cfi: string) => { index: number; anchor: (doc: Document) => Range }; addAnnotation: (note: BookNote, remove?: boolean) => { index: number; label: string }; search: (config: BookSearchConfig) => AsyncGenerator; clearSearch: () => void;