feat: add keyboard shortcut t to toggle TTS (#1405)
This commit is contained in:
@@ -274,7 +274,7 @@ const TTSControl = () => {
|
||||
const handleStop = async (bookKey: string) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.stop();
|
||||
await ttsController.shutdown();
|
||||
ttsControllerRef.current = null;
|
||||
setTtsController(null);
|
||||
getView(bookKey)?.deselect();
|
||||
|
||||
@@ -17,7 +17,7 @@ interface UseBookShortcutsProps {
|
||||
}
|
||||
|
||||
const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) => {
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getView, getViewState, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { toggleSideBar, setSideBarBookKey } = useSidebarStore();
|
||||
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const { toggleNotebook } = useNotebookStore();
|
||||
@@ -136,6 +136,13 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings!));
|
||||
};
|
||||
|
||||
const toggleTTS = () => {
|
||||
if (!sideBarBookKey) return;
|
||||
const bookKey = sideBarBookKey;
|
||||
const viewState = getViewState(bookKey);
|
||||
eventDispatcher.dispatch(viewState?.ttsEnabled ? 'tts-stop' : 'tts-speak', { bookKey });
|
||||
};
|
||||
|
||||
useShortcuts(
|
||||
{
|
||||
onSwitchSideBar: switchSideBar,
|
||||
@@ -144,8 +151,9 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
onToggleScrollMode: toggleScrollMode,
|
||||
onOpenFontLayoutSettings: () => setFontLayoutSettingsDialogOpen(true),
|
||||
onToggleSearchBar: showSearchBar,
|
||||
onReloadPage: reloadPage,
|
||||
onToggleFullscreen: toggleFullscreen,
|
||||
onToggleTTS: toggleTTS,
|
||||
onReloadPage: reloadPage,
|
||||
onQuitApp: quitApp,
|
||||
onGoLeft: goLeft,
|
||||
onGoRight: goRight,
|
||||
|
||||
@@ -1,36 +1,11 @@
|
||||
export interface ShortcutConfig {
|
||||
onSwitchSideBar: string[];
|
||||
onToggleSideBar: string[];
|
||||
onToggleNotebook: string[];
|
||||
onToggleSearchBar: string[];
|
||||
onToggleScrollMode: string[];
|
||||
onToggleSelectMode: string[];
|
||||
onOpenFontLayoutSettings: string[];
|
||||
onReloadPage: string[];
|
||||
onToggleFullscreen: string[];
|
||||
onQuitApp: string[];
|
||||
onGoLeft: string[];
|
||||
onGoRight: string[];
|
||||
onGoNext: string[];
|
||||
onGoPrev: string[];
|
||||
onGoHalfPageDown: string[];
|
||||
onGoHalfPageUp: string[];
|
||||
onGoBack: string[];
|
||||
onGoForward: string[];
|
||||
onZoomIn: string[];
|
||||
onZoomOut: string[];
|
||||
onResetZoom: string[];
|
||||
onSaveNote: string[];
|
||||
onCloseNote: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
const DEFAULT_SHORTCUTS = {
|
||||
onSwitchSideBar: ['ctrl+Tab', 'opt+Tab', 'alt+Tab'],
|
||||
onToggleSideBar: ['s'],
|
||||
onToggleNotebook: ['n'],
|
||||
onToggleSearchBar: ['ctrl+f', 'cmd+f'],
|
||||
onToggleScrollMode: ['shift+j'],
|
||||
onToggleSelectMode: ['shift+s'],
|
||||
onToggleTTS: ['t'],
|
||||
onOpenFontLayoutSettings: ['shift+f'],
|
||||
onReloadPage: ['shift+r'],
|
||||
onToggleFullscreen: ['F11'],
|
||||
@@ -50,6 +25,10 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
onCloseNote: ['Escape'],
|
||||
};
|
||||
|
||||
export type ShortcutConfig = {
|
||||
[K in keyof typeof DEFAULT_SHORTCUTS]: string[];
|
||||
};
|
||||
|
||||
// Load shortcuts from localStorage or fallback to defaults
|
||||
export const loadShortcuts = (): ShortcutConfig => {
|
||||
if (typeof localStorage === 'undefined') return DEFAULT_SHORTCUTS;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { AppService } from '@/types/system';
|
||||
import { parseSSMLMarks } from '@/utils/ssml';
|
||||
import { TTSGranularity, TTSMark, TTSVoice } from './types';
|
||||
import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
import { TTSGranularity, TTSHighlightOptions, TTSMark, TTSVoice } from './types';
|
||||
import { WebSpeechClient } from './WebSpeechClient';
|
||||
import { NativeTTSClient } from './NativeTTSClient';
|
||||
import { EdgeTTSClient } from './EdgeTTSClient';
|
||||
@@ -18,6 +19,8 @@ type TTSState =
|
||||
| 'setrate-paused'
|
||||
| 'setvoice-paused';
|
||||
|
||||
const HIGHLIGHT_KEY = 'tts-highlight';
|
||||
|
||||
export class TTSController extends EventTarget {
|
||||
appService: AppService | null = null;
|
||||
view: FoliateView;
|
||||
@@ -75,13 +78,29 @@ export class TTSController extends EventTarget {
|
||||
this.ttsEdgeVoices = await this.ttsEdgeClient.getAllVoices();
|
||||
}
|
||||
|
||||
#getHighlighter(options: TTSHighlightOptions) {
|
||||
return (range: Range) => {
|
||||
const { style, color } = options;
|
||||
const { overlayer } = this.view.renderer.getContents()[0] as { overlayer: Overlayer };
|
||||
overlayer?.remove(HIGHLIGHT_KEY);
|
||||
overlayer?.add(HIGHLIGHT_KEY, range, Overlayer[style], { color });
|
||||
this.view.renderer.scrollToAnchor(range);
|
||||
};
|
||||
}
|
||||
|
||||
#clearHighlighter() {
|
||||
const { overlayer } = this.view.renderer.getContents()[0] as { overlayer: Overlayer };
|
||||
overlayer?.remove(HIGHLIGHT_KEY);
|
||||
}
|
||||
|
||||
async initViewTTS() {
|
||||
let granularity: TTSGranularity = this.view.language.isCJK ? 'sentence' : 'word';
|
||||
const supportedGranularities = this.ttsClient.getGranularities();
|
||||
if (!supportedGranularities.includes(granularity)) {
|
||||
granularity = supportedGranularities[0]!;
|
||||
}
|
||||
await this.view.initTTS(granularity);
|
||||
const highlightOptions: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
|
||||
await this.view.initTTS(granularity, this.#getHighlighter(highlightOptions));
|
||||
}
|
||||
|
||||
async preloadSSML(ssml: string | undefined) {
|
||||
@@ -334,6 +353,7 @@ export class TTSController extends EventTarget {
|
||||
|
||||
async shutdown() {
|
||||
await this.stop();
|
||||
this.#clearHighlighter();
|
||||
if (this.ttsWebClient.initialized) {
|
||||
await this.ttsWebClient.shutdown();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
export type TTSGranularity = 'sentence' | 'word';
|
||||
|
||||
export type TTSHighlightOptions = {
|
||||
style: 'highlight' | 'underline' | 'squiggly' | 'outline';
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type TTSVoice = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface FoliateView extends HTMLElement {
|
||||
clearSearch: () => void;
|
||||
select: (target: string | number | { fraction: number }) => void;
|
||||
deselect: () => void;
|
||||
initTTS: (granularity?: TTSGranularity) => Promise<void>;
|
||||
initTTS: (granularity?: TTSGranularity, highlight?: (range: Range) => void) => Promise<void>;
|
||||
book: BookDoc;
|
||||
tts: TTS | null;
|
||||
language: {
|
||||
@@ -49,7 +49,8 @@ export interface FoliateView extends HTMLElement {
|
||||
prevSection?: () => Promise<void>;
|
||||
goTo?: (params: { index: number; anchor: number }) => void;
|
||||
setStyles?: (css: string) => void;
|
||||
getContents: () => { doc: Document; index?: number }[];
|
||||
getContents: () => { doc: Document; index?: number; overlayer?: unknown }[];
|
||||
scrollToAnchor: (anchor: number | Range) => void;
|
||||
addEventListener: (
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 530237eab5...97f9879568
Reference in New Issue
Block a user