mirror of
https://github.com/readest/readest.git
synced 2026-05-22 02:49:49 +00:00
fix(tts): media control in the lock screen and with airpods for iOS, closes #1407 (#1949)
Some checks are pending
Deploy to vercel on merge / build_and_deploy (push) Waiting to run
Some checks are pending
Deploy to vercel on merge / build_and_deploy (push) Waiting to run
This commit is contained in:
parent
61dda9a517
commit
32954e025c
5 changed files with 76 additions and 33 deletions
|
|
@ -123,6 +123,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
|||
bookKey={bookKey}
|
||||
bookDoc={bookDoc}
|
||||
config={config}
|
||||
gridInsets={gridInsets}
|
||||
contentInsets={contentInsets}
|
||||
/>
|
||||
{viewSettings.vertical && viewSettings.scrolled && (
|
||||
|
|
|
|||
|
|
@ -58,16 +58,18 @@ const FoliateViewer: React.FC<{
|
|||
bookKey: string;
|
||||
bookDoc: BookDoc;
|
||||
config: BookConfig;
|
||||
gridInsets: Insets;
|
||||
contentInsets: Insets;
|
||||
}> = ({ bookKey, bookDoc, config, contentInsets: insets }) => {
|
||||
}> = ({ bookKey, bookDoc, config, gridInsets, contentInsets: insets }) => {
|
||||
const { appService, envConfig } = useEnv();
|
||||
const { themeCode, isDarkMode } = useThemeStore();
|
||||
const { settings } = useSettingsStore();
|
||||
const { loadCustomFonts, getLoadedFonts } = useCustomFontStore();
|
||||
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
|
||||
const { getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getViewState, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getParallels } = useParallelViewStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const viewState = getViewState(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
|
||||
const viewRef = useRef<FoliateView | null>(null);
|
||||
|
|
@ -308,6 +310,7 @@ const FoliateViewer: React.FC<{
|
|||
|
||||
const applyMarginAndGap = () => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const viewState = getViewState(bookKey);
|
||||
const viewInsets = getViewInsets(viewSettings);
|
||||
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
|
||||
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
|
||||
|
|
@ -315,7 +318,11 @@ const FoliateViewer: React.FC<{
|
|||
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
|
||||
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
|
||||
const moreTopInset = showTopHeader ? Math.max(0, 44 - insets.top) : 0;
|
||||
const moreBottomInset = showBottomFooter ? Math.max(0, 44 - insets.bottom) : 0;
|
||||
const ttsBarHeight =
|
||||
viewState?.ttsEnabled && viewSettings.showTTSBar ? 52 + gridInsets.bottom * 0.33 : 0;
|
||||
const moreBottomInset = showBottomFooter
|
||||
? Math.max(0, Math.max(ttsBarHeight, 44) - insets.bottom)
|
||||
: Math.max(0, ttsBarHeight);
|
||||
const moreRightInset = showDoubleBorderHeader ? 32 : 0;
|
||||
const moreLeftInset = showDoubleBorderFooter ? 32 : 0;
|
||||
const topMargin = (showTopHeader ? insets.top : viewInsets.top) + moreTopInset;
|
||||
|
|
@ -379,6 +386,8 @@ const FoliateViewer: React.FC<{
|
|||
viewSettings?.doubleBorder,
|
||||
viewSettings?.showHeader,
|
||||
viewSettings?.showFooter,
|
||||
viewSettings?.showTTSBar,
|
||||
viewState?.ttsEnabled,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -68,11 +68,6 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
|||
unblockerAudioRef.current.addEventListener('play', () => {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = null;
|
||||
navigator.mediaSession.setActionHandler('play', null);
|
||||
navigator.mediaSession.setActionHandler('pause', null);
|
||||
navigator.mediaSession.setActionHandler('stop', null);
|
||||
navigator.mediaSession.setActionHandler('seekbackward', null);
|
||||
navigator.mediaSession.setActionHandler('seekforward', null);
|
||||
}
|
||||
});
|
||||
unblockerAudioRef.current.preload = 'auto';
|
||||
|
|
@ -127,7 +122,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
|||
const mark = (e as CustomEvent<TTSMark>).detail;
|
||||
if (appService?.isMobileApp && 'mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: mark.text,
|
||||
title: mark?.text || '',
|
||||
artist: sectionLabel || title,
|
||||
album: author,
|
||||
artwork: [{ src: coverImageUrl || '/icon.png', sizes: '512x512', type: 'image/png' }],
|
||||
|
|
@ -244,7 +239,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
const handleTogglePlay = useCallback(async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (!ttsController) return;
|
||||
|
||||
|
|
@ -263,41 +258,54 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
|||
await ttsController.start();
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [isPlaying, isPaused]);
|
||||
|
||||
const handleBackward = async () => {
|
||||
const handleBackward = useCallback(async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.backward();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleForward = async () => {
|
||||
const handleForward = useCallback(async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.forward();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleStop = async (bookKey: string) => {
|
||||
const handlePause = useCallback(async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.shutdown();
|
||||
ttsControllerRef.current = null;
|
||||
setTtsController(null);
|
||||
getView(bookKey)?.deselect();
|
||||
setIsPlaying(false);
|
||||
setShowPanel(false);
|
||||
setShowIndicator(false);
|
||||
setIsPaused(true);
|
||||
await ttsController.pause();
|
||||
}
|
||||
if (appService?.isIOSApp) {
|
||||
await invokeUseBackgroundAudio({ enabled: false });
|
||||
}
|
||||
if (appService?.isMobile) {
|
||||
releaseUnblockAudio();
|
||||
}
|
||||
setTTSEnabled(bookKey, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleStop = useCallback(
|
||||
async (bookKey: string) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.shutdown();
|
||||
ttsControllerRef.current = null;
|
||||
setTtsController(null);
|
||||
getView(bookKey)?.deselect();
|
||||
setIsPlaying(false);
|
||||
setShowPanel(false);
|
||||
setShowIndicator(false);
|
||||
}
|
||||
if (appService?.isIOSApp) {
|
||||
await invokeUseBackgroundAudio({ enabled: false });
|
||||
}
|
||||
if (appService?.isMobile) {
|
||||
releaseUnblockAudio();
|
||||
}
|
||||
setTTSEnabled(bookKey, false);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[appService],
|
||||
);
|
||||
|
||||
// rate range: 0.5 - 3, 1.0 is normal speed
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -415,6 +423,30 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
|||
setShowPanel(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('play', () => {
|
||||
handleTogglePlay();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
handleTogglePlay();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('stop', () => {
|
||||
handlePause();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('seekforward', () => {
|
||||
handleForward();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('seekbackward', () => {
|
||||
handleBackward();
|
||||
});
|
||||
}
|
||||
}, [handleTogglePlay, handlePause, handleForward, handleBackward]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!iconRef.current || !showPanel) return;
|
||||
const parentElement = iconRef.current.parentElement;
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@ export class TTSController extends EventTarget {
|
|||
await this.initViewTTS();
|
||||
this.#speak(ssml).catch((e) => this.error(e));
|
||||
this.preloadNextSSML();
|
||||
this.dispatchSpeakMark();
|
||||
}
|
||||
|
||||
play() {
|
||||
|
|
@ -357,8 +358,8 @@ export class TTSController extends EventTarget {
|
|||
return this.ttsClient.getSpeakingLang();
|
||||
}
|
||||
|
||||
dispatchSpeakMark(mark: TTSMark) {
|
||||
this.dispatchEvent(new CustomEvent('tts-speak-mark', { detail: mark }));
|
||||
dispatchSpeakMark(mark?: TTSMark) {
|
||||
this.dispatchEvent(new CustomEvent('tts-speak-mark', { detail: mark || { text: '' } }));
|
||||
}
|
||||
|
||||
error(e: unknown) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ compatibility_flags = ["nodejs_compat"]
|
|||
|
||||
[observability]
|
||||
enabled = true
|
||||
head_sampling_rate = 1
|
||||
head_sampling_rate = 0.01
|
||||
|
||||
[assets]
|
||||
directory = ".open-next/assets"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue