import React, { useState, useRef, useEffect } from 'react'; import { TTSController } from '@/services/tts/TTSController'; import { WebSpeechClient } from '@/services/tts'; import { getPopupPosition, Position } from '@/utils/sel'; import { eventDispatcher } from '@/utils/event'; import { useReaderStore } from '@/store/readerStore'; import Popup from '@/components/Popup'; import TTSPanel from './TTSPanel'; import TTSIcon from './TTSIcon'; const POPUP_WIDTH = 260; const POPUP_HEIGHT = 80; const POPUP_PADDING = 10; const TTSControl = () => { const { getView } = useReaderStore(); const [isPlaying, setIsPlaying] = useState(false); const [showPanel, setShowPanel] = useState(false); const [panelPosition, setPanelPosition] = useState(); const [trianglePosition, setTrianglePosition] = useState(); const iconRef = useRef(null); const ttsControllerRef = useRef(null); useEffect(() => { return () => { if (ttsControllerRef.current) { ttsControllerRef.current.kill(); ttsControllerRef.current = null; } }; }, []); useEffect(() => { eventDispatcher.on('speak', handleSpeak); return () => { eventDispatcher.off('speak', handleSpeak); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSpeak = async (event: CustomEvent) => { const { bookKey, ssml } = event.detail; const view = getView(bookKey); if (!view) return; const ttsClient = new WebSpeechClient(); const ttsController = new TTSController(ttsClient, view); ttsControllerRef.current = ttsController; ttsControllerRef.current.speak(ssml); setIsPlaying(true); }; const handleTogglePlay = async () => { const ttsController = ttsControllerRef.current; if (!ttsController) return; if (isPlaying) { ttsController.pause(); setIsPlaying(false); } else { await ttsController.start(); setIsPlaying(true); } }; const handleBackward = async () => { const ttsController = ttsControllerRef.current; if (ttsController) { await ttsController.backward(); } }; const handleForward = async () => { const ttsController = ttsControllerRef.current; if (ttsController) { await ttsController.forward(); } }; const handleStop = async () => { const ttsController = ttsControllerRef.current; if (ttsController) { ttsController.stop(); ttsControllerRef.current = null; setIsPlaying(false); setShowPanel(false); } }; const updatePanelPosition = () => { if (iconRef.current) { const rect = iconRef.current.getBoundingClientRect(); const windowRect = document.documentElement.getBoundingClientRect(); const trianglePos = { dir: 'up', point: { x: rect.left + rect.width / 2, y: rect.top - 12 }, } as Position; const popupPos = getPopupPosition( trianglePos, windowRect, POPUP_WIDTH, POPUP_HEIGHT, POPUP_PADDING, ); setPanelPosition(popupPos); setTrianglePosition(trianglePos); } }; const togglePopup = () => { updatePanelPosition(); setShowPanel((prev) => !prev); }; return (
{ttsControllerRef.current && (
)} {showPanel && panelPosition && trianglePosition && ( )}
); }; export default TTSControl;