diff --git a/apps/readest-app/src/__tests__/helpers/shortcuts.test.ts b/apps/readest-app/src/__tests__/helpers/shortcuts.test.ts new file mode 100644 index 00000000..ed6e48d4 --- /dev/null +++ b/apps/readest-app/src/__tests__/helpers/shortcuts.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; + +// Use dynamic import to avoid module resolution issues in test +const getDefaults = async () => { + const mod = await import('../../helpers/shortcuts'); + return mod.loadShortcuts(); +}; + +describe('TTS play/pause shortcut', () => { + it('should have onTTSPlayPause shortcut with space', async () => { + const shortcuts = await getDefaults(); + expect(shortcuts.onTTSPlayPause).toEqual([' ']); + }); + + it('should also have space in onGoRight as fallback', async () => { + const shortcuts = await getDefaults(); + expect(shortcuts.onGoRight).toContain(' '); + }); +}); + +describe('TTS navigation shortcuts', () => { + it('should have onTTSGoNextSentence shortcut with ctrl+] and cmd+]', async () => { + const shortcuts = await getDefaults(); + expect(shortcuts.onTTSGoNextSentence).toEqual(['ctrl+]', 'cmd+]']); + }); + + it('should have onTTSGoPreviousSentence shortcut with ctrl+[ and cmd+[', async () => { + const shortcuts = await getDefaults(); + expect(shortcuts.onTTSGoPreviousSentence).toEqual(['ctrl+[', 'cmd+[']); + }); + + it('should have onTTSGoNextParagraph shortcut with ctrl+shift+} and cmd+shift+}', async () => { + const shortcuts = await getDefaults(); + expect(shortcuts.onTTSGoNextParagraph).toEqual(['ctrl+shift+}', 'cmd+shift+}']); + }); + + it('should have onTTSGoPreviousParagraph shortcut with ctrl+shift+{ and cmd+shift+{', async () => { + const shortcuts = await getDefaults(); + expect(shortcuts.onTTSGoPreviousParagraph).toEqual(['ctrl+shift+{', 'cmd+shift+{']); + }); +}); diff --git a/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts b/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts index 21fab87b..004756a8 100644 --- a/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts +++ b/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts @@ -230,6 +230,34 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) = eventDispatcher.dispatch(viewState?.ttsEnabled ? 'tts-stop' : 'tts-speak', { bookKey }); }; + const ttsPlayPause = () => { + if (!sideBarBookKey) return false; + const viewState = getViewState(sideBarBookKey); + if (!viewState?.ttsEnabled) return false; + eventDispatcher.dispatch('tts-toggle-play', { bookKey: sideBarBookKey }); + return true; + }; + + const ttsGoNextSentence = () => { + if (!sideBarBookKey) return; + eventDispatcher.dispatch('tts-forward', { bookKey: sideBarBookKey, byMark: true }); + }; + + const ttsGoPreviousSentence = () => { + if (!sideBarBookKey) return; + eventDispatcher.dispatch('tts-backward', { bookKey: sideBarBookKey, byMark: true }); + }; + + const ttsGoNextParagraph = () => { + if (!sideBarBookKey) return; + eventDispatcher.dispatch('tts-forward', { bookKey: sideBarBookKey, byMark: false }); + }; + + const ttsGoPreviousParagraph = () => { + if (!sideBarBookKey) return; + eventDispatcher.dispatch('tts-backward', { bookKey: sideBarBookKey, byMark: false }); + }; + const toggleBookmark = () => { if (!sideBarBookKey) return; eventDispatcher.dispatch('toggle-bookmark', { bookKey: sideBarBookKey }); @@ -280,6 +308,11 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) = onShowSearchBar: showSearchBar, onToggleFullscreen: toggleFullscreen, onToggleTTS: toggleTTS, + onTTSPlayPause: ttsPlayPause, + onTTSGoNextSentence: ttsGoNextSentence, + onTTSGoPreviousSentence: ttsGoPreviousSentence, + onTTSGoNextParagraph: ttsGoNextParagraph, + onTTSGoPreviousParagraph: ttsGoPreviousParagraph, onReloadPage: reloadPage, onCloseWindow: closeWindow, onQuitApp: quitApp, diff --git a/apps/readest-app/src/app/reader/hooks/useTTSControl.ts b/apps/readest-app/src/app/reader/hooks/useTTSControl.ts index 5995b877..b166685d 100644 --- a/apps/readest-app/src/app/reader/hooks/useTTSControl.ts +++ b/apps/readest-app/src/app/reader/hooks/useTTSControl.ts @@ -59,12 +59,56 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp deinitMediaSession, } = useTTSMediaSession({ bookKey }); + const handleTTSForward = async (event: CustomEvent) => { + const detail = event.detail as { bookKey: string; byMark?: boolean } | undefined; + if (detail?.bookKey !== bookKey) return; + const ttsController = ttsControllerRef.current; + if (ttsController) { + await ttsController.forward(detail?.byMark ?? false); + } + }; + + const handleTTSBackward = async (event: CustomEvent) => { + const detail = event.detail as { bookKey: string; byMark?: boolean } | undefined; + if (detail?.bookKey !== bookKey) return; + const ttsController = ttsControllerRef.current; + if (ttsController) { + await ttsController.backward(detail?.byMark ?? false); + } + }; + + const handleTTSTogglePlay = async (event: CustomEvent) => { + const detail = event.detail as { bookKey: string } | undefined; + if (detail?.bookKey !== bookKey) return; + const ttsController = ttsControllerRef.current; + if (!ttsController) return; + if (ttsController.state === 'playing') { + setIsPlaying(false); + setIsPaused(true); + await ttsController.pause(); + } else { + setIsPlaying(true); + setIsPaused(false); + if (ttsController.state === 'paused') { + await ttsController.resume(); + } else { + await ttsController.start(); + } + } + }; + useEffect(() => { eventDispatcher.on('tts-speak', handleTTSSpeak); eventDispatcher.on('tts-stop', handleTTSStop); + eventDispatcher.on('tts-forward', handleTTSForward); + eventDispatcher.on('tts-backward', handleTTSBackward); + eventDispatcher.on('tts-toggle-play', handleTTSTogglePlay); return () => { eventDispatcher.off('tts-speak', handleTTSSpeak); eventDispatcher.off('tts-stop', handleTTSStop); + eventDispatcher.off('tts-forward', handleTTSForward); + eventDispatcher.off('tts-backward', handleTTSBackward); + eventDispatcher.off('tts-toggle-play', handleTTSTogglePlay); if (ttsControllerRef.current) { ttsControllerRef.current.shutdown(); ttsControllerRef.current = null; diff --git a/apps/readest-app/src/helpers/shortcuts.ts b/apps/readest-app/src/helpers/shortcuts.ts index cd92dee2..12e408da 100644 --- a/apps/readest-app/src/helpers/shortcuts.ts +++ b/apps/readest-app/src/helpers/shortcuts.ts @@ -7,6 +7,11 @@ const DEFAULT_SHORTCUTS = { onToggleSelectMode: ['shift+s'], onToggleBookmark: ['ctrl+d', 'cmd+d'], onToggleTTS: ['t'], + onTTSPlayPause: [' '], + onTTSGoNextSentence: ['ctrl+]', 'cmd+]'], + onTTSGoPreviousSentence: ['ctrl+[', 'cmd+['], + onTTSGoNextParagraph: ['ctrl+shift+}', 'cmd+shift+}'], + onTTSGoPreviousParagraph: ['ctrl+shift+{', 'cmd+shift+{'], onToggleParagraphMode: ['shift+p'], onHighlightSelection: ['ctrl+h', 'cmd+h'], onUnderlineSelection: ['ctrl+u', 'cmd+u'], @@ -19,7 +24,7 @@ const DEFAULT_SHORTCUTS = { onReadAloudSelection: ['ctrl+r', 'cmd+r'], onProofreadSelection: ['ctrl+p', 'cmd+p'], onOpenFontLayoutSettings: ['shift+f', 'ctrl+,', 'cmd+,'], - onOpenCommandPalette: ['ctrl+Shift+p', 'cmd+Shift+p'], + onOpenCommandPalette: ['ctrl+shift+p', 'cmd+shift+p'], onOpenBooks: ['ctrl+o'], onReloadPage: ['shift+r'], onToggleFullscreen: ['F11'],