diff --git a/src/components/AppSettings.tsx b/src/components/AppSettings.tsx index 00cab2e4c6..e5ba855a19 100644 --- a/src/components/AppSettings.tsx +++ b/src/components/AppSettings.tsx @@ -5,8 +5,11 @@ import React, { useCallback, useContext, useState } from 'react' */ export type AppSettings = { sound: boolean + pronunciation: pronunciationType } +export type pronunciationType = 'us' | 'uk' | false + export type AppSettingsContextData = { settings: AppSettings dispatch: (settings: AppSettings) => void @@ -25,8 +28,15 @@ export function useSetSoundState(): [status: boolean, setSound: (state: boolean) return [settings.sound, setSound] } +export function useSetPronunciationState(): [status: pronunciationType, setpronunciation: (state: pronunciationType) => void] { + const { settings, dispatch } = useContext(AppSettingsContext) + const setpronunciation = useCallback((state: pronunciationType) => dispatch({ ...settings, pronunciation: state }), [settings, dispatch]) + return [settings.pronunciation, setpronunciation] +} + const defaultSettings: AppSettings = { sound: true, + pronunciation: 'us', } export const AppSettingsProvider: React.FC> = ({ children }) => { diff --git a/src/components/Word/index.tsx b/src/components/Word/index.tsx index 0d8e76e1d7..3b123323c7 100644 --- a/src/components/Word/index.tsx +++ b/src/components/Word/index.tsx @@ -3,6 +3,7 @@ import Letter, { LetterState } from './Letter' import { isLegal, isChineseSymbol } from '../../utils/utils' import useSounds from 'hooks/useSounds' import style from './index.module.css' +import usePronunciationSound from 'hooks/usePronouncation' const Word: React.FC = ({ word = 'defaultWord', onFinish, isStart, wordVisible = true }) => { word = word.replace(new RegExp(' ', 'g'), '_') @@ -12,6 +13,7 @@ const Word: React.FC = ({ word = 'defaultWord', onFinish, isStart, wo const [isFinish, setIsFinish] = useState(false) const [hasWrong, setHasWrong] = useState(false) const [playKeySound, playBeepSound, playHintSound] = useSounds() + const playPronounce = usePronunciationSound(word) const onKeydown = useCallback( (e) => { @@ -59,6 +61,14 @@ const Word: React.FC = ({ word = 'defaultWord', onFinish, isStart, wo } }, [hasWrong, playBeepSound]) + useEffect(() => { + if (isStart && inputWord.length === 0) { + playPronounce() + } + // SAFETY: Don't depend on `playPronounce`! It will cost audio play again and again. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isStart, word, inputWord]) + useLayoutEffect(() => { let hasWrong = false, wordLength = word.length, diff --git a/src/hooks/usePronouncation.ts b/src/hooks/usePronouncation.ts new file mode 100644 index 0000000000..ef88f7313d --- /dev/null +++ b/src/hooks/usePronouncation.ts @@ -0,0 +1,31 @@ +import { useAppSettings } from 'components/AppSettings' +import { noop } from 'lodash' +import { useEffect, useState } from 'react' + +declare type PronounceFunction = () => void + +const pronunciationApi = 'http://dict.youdao.com/dictvoice?audio=' + +export default function usePronunciationSound(word: string): PronounceFunction { + const [audio, setAudio] = useState(null) + const { pronunciation } = useAppSettings() + const ukPronounceFunction = () => setAudio(new Audio(pronunciationApi + word + '&type=1')) + const usPronounceFunction = () => setAudio(new Audio(pronunciationApi + word + '&type=2')) + + useEffect(() => { + audio?.play() + return () => { + // Pause the audio when unmount + audio?.pause() + } + }, [audio, word]) + + switch (pronunciation) { + case false: + return noop + case 'uk': + return ukPronounceFunction + case 'us': + return usPronounceFunction + } +} diff --git a/src/icon.ts b/src/icon.ts index 6f4554646b..8cada6d829 100644 --- a/src/icon.ts +++ b/src/icon.ts @@ -9,6 +9,8 @@ import { faEyeSlash, faAssistiveListeningSystems, faCoffee, + faMicrophone, + faMicrophoneSlash, } from '@fortawesome/free-solid-svg-icons' import { faGithub } from '@fortawesome/free-brands-svg-icons' @@ -23,4 +25,6 @@ library.add( faEyeSlash, faAssistiveListeningSystems, faCoffee, + faMicrophone, + faMicrophoneSlash, ) diff --git a/src/pages/Typing/PronunciationSwitcher/index.tsx b/src/pages/Typing/PronunciationSwitcher/index.tsx new file mode 100644 index 0000000000..855b2b9790 --- /dev/null +++ b/src/pages/Typing/PronunciationSwitcher/index.tsx @@ -0,0 +1,26 @@ +export type PronunciationSwitcherPropsType = { + state: string + changeState: any +} + +const PronunciationSwitcher: React.FC = ({ state, changeState }) => { + return ( +
+
+ +
+
+ ) +} + +export default PronunciationSwitcher diff --git a/src/pages/Typing/hooks/usePronunciation.ts b/src/pages/Typing/hooks/usePronunciation.ts new file mode 100644 index 0000000000..0aa1f24144 --- /dev/null +++ b/src/pages/Typing/hooks/usePronunciation.ts @@ -0,0 +1,25 @@ +import { useSetPronunciationState, pronunciationType } from 'components/AppSettings' + +export type SwitcherDispatchType = (newStatus?: string) => void + +const usePronunciation = (): [pronunciationType, SwitcherDispatchType] => { + const [pronunciation, setPronunciation] = useSetPronunciationState() + + const dispatch: SwitcherDispatchType = (newStatus) => { + switch (newStatus) { + case 'uk': + setPronunciation('uk') + break + case 'us': + setPronunciation('us') + break + case 'false': + setPronunciation(false) + break + } + } + + return [pronunciation, dispatch] +} + +export default usePronunciation diff --git a/src/pages/Typing/hooks/useSwitcherState.ts b/src/pages/Typing/hooks/useSwitcherState.ts index 00c215aa28..67c8a64974 100644 --- a/src/pages/Typing/hooks/useSwitcherState.ts +++ b/src/pages/Typing/hooks/useSwitcherState.ts @@ -24,6 +24,7 @@ const useSwitcherState = (initialState: { phonetic: boolean; wordVisible: boolea break case 'sound': newStatus === undefined ? setSound(!sound) : setSound(newStatus) + break } } diff --git a/src/pages/Typing/index.tsx b/src/pages/Typing/index.tsx index f29f93f4b4..16ffbcf6cb 100644 --- a/src/pages/Typing/index.tsx +++ b/src/pages/Typing/index.tsx @@ -8,6 +8,7 @@ import Speed from 'components/Speed' import Modals from 'components/Modals' import Loading from 'components/Loading' import Phonetic from 'components/Phonetic' +import PronunciationSwitcher from './PronunciationSwitcher' import { isLegal } from 'utils/utils' import { useHotkeys } from 'react-hotkeys-hook' import { useModals } from 'utils/hooks' @@ -16,6 +17,7 @@ import Switcher from './Switcher' import DictSwitcher from './DictSwitcher' import { dictList, useWordList } from './hooks/useWordList' import { useLocalStorage } from 'react-use' +import usePronunciation from './hooks/usePronunciation' type LocalStorage = { dictName: string @@ -36,7 +38,7 @@ const App: React.FC = () => { const [localStorage, setLocalStorage] = useLocalStorage('Dict') const [switcherState, switcherStateDispatch] = useSwitcherState({ wordVisible: true, phonetic: false }) const [dictName, chapter, chapterListLength, wordList, wordListDispatch] = useWordList(chapterLength) - + const [pronuncition, pronuncitionDispatch] = usePronunciation() const { modalState, title: modalTitle, @@ -162,6 +164,10 @@ const App: React.FC = () => { (dictName: string) => { setOrder(0) setIsLoading(true) + // Need to stop the game, in order to prevent pronounce wrong word. + // Besides, it makes sense because users are about to stop when they change dict/chapter. + // Otherwise, introduce a new parameter to allow pronunciation begin. + setIsStart(false) wordListDispatch('setDictName', dictName, () => { setIsLoading(false) }) @@ -172,10 +178,17 @@ const App: React.FC = () => { const changeChapter = useCallback( (chapter: number) => { setOrder(0) + setIsStart(false) // Same story as above. wordListDispatch('setChapter', chapter) }, [wordListDispatch], ) + const changeState = useCallback( + (state: string) => { + pronuncitionDispatch(state) + }, + [pronuncitionDispatch], + ) return ( <> @@ -203,6 +216,7 @@ const App: React.FC = () => { changeDict={changeDict} changeChapter={changeChapter} /> +