From 99208ffceda2c5861398ebc984157f04c56e4472 Mon Sep 17 00:00:00 2001 From: gelove Date: Wed, 11 Aug 2021 00:28:32 +0800 Subject: [PATCH] refactor: (useSpeech,usePermission) improved types --- src/usePermission.ts | 64 ++++++++++++-------- src/useSpeech.ts | 116 +++++++++++++++++++++++------------- stories/useSpeech.story.tsx | 3 +- 3 files changed, 116 insertions(+), 67 deletions(-) diff --git a/src/usePermission.ts b/src/usePermission.ts index 00046059..0aae85f5 100644 --- a/src/usePermission.ts +++ b/src/usePermission.ts @@ -1,45 +1,59 @@ import { useEffect, useState } from 'react'; import { noop, off, on } from './misc/util'; -type PermissionDesc = +export type IState = PermissionState | ''; + +interface IPushPermissionDescriptor extends PermissionDescriptor { + name: 'push'; + userVisibleOnly?: boolean; +} + +interface IMidiPermissionDescriptor extends PermissionDescriptor { + name: 'midi'; + sysex?: boolean; +} + +interface IDevicePermissionDescriptor extends PermissionDescriptor { + name: 'camera' | 'microphone' | 'speaker'; + deviceId?: string; +} + +export type IPermissionDescriptor = | PermissionDescriptor - | DevicePermissionDescriptor - | MidiPermissionDescriptor - | PushPermissionDescriptor; + | IPushPermissionDescriptor + | IMidiPermissionDescriptor + | IDevicePermissionDescriptor; -type State = PermissionState | ''; - -const usePermission = (permissionDesc: PermissionDesc): State => { - let mounted = true; - let permissionStatus: PermissionStatus | null = null; - - const [state, setState] = useState(''); - - const onChange = () => { - if (mounted && permissionStatus) { - setState(permissionStatus.state); - } - }; - - const changeState = () => { - onChange(); - on(permissionStatus, 'change', onChange); - }; +// const usePermission = (permissionDesc: T): IState => { +const usePermission = (permissionDesc: IPermissionDescriptor): IState => { + const [state, setState] = useState(''); useEffect(() => { + let mounted = true; + let permissionStatus: PermissionStatus | null = null; + + const onChange = () => { + if (!mounted) { + return; + } + setState(() => permissionStatus?.state ?? ''); + }; + navigator.permissions .query(permissionDesc) .then((status) => { permissionStatus = status; - changeState(); + on(permissionStatus, 'change', onChange); + onChange(); }) .catch(noop); return () => { - mounted = false; permissionStatus && off(permissionStatus, 'change', onChange); + mounted = false; + permissionStatus = null; }; - }, []); + }, [permissionDesc]); return state; }; diff --git a/src/useSpeech.ts b/src/useSpeech.ts index b8eaee97..a38037db 100644 --- a/src/useSpeech.ts +++ b/src/useSpeech.ts @@ -1,54 +1,90 @@ -import { useRef } from 'react'; -import useMount from './useMount'; -import useSetState from './useSetState'; -import { isBrowser } from './misc/util'; +import { useCallback, useEffect, useRef, useState } from 'react'; -export interface SpeechState { - isPlaying: boolean; +type SpeechOptions = { lang: string; - voice: SpeechSynthesisVoice; + voice?: SpeechSynthesisVoice; rate: number; pitch: number; volume: number; +}; + +export type ISpeechOptions = Partial; + +export type VoiceInfo = Pick; + +export type ISpeechState = SpeechOptions & { + isPlaying: boolean; + status: string; + voiceInfo: VoiceInfo; +}; + +enum Status { + init, + play, + pause, + end, } -export interface SpeechOptions { - lang?: string; - voice?: SpeechSynthesisVoice; - rate?: number; - pitch?: number; - volume?: number; -} - -const voices = - isBrowser && typeof window.speechSynthesis === 'object' ? window.speechSynthesis.getVoices() : []; - -const useSpeech = (text: string, opts: SpeechOptions = {}): SpeechState => { - const [state, setState] = useSetState({ - isPlaying: false, - lang: opts.lang || 'default', - voice: opts.voice || voices[0], - rate: opts.rate || 1, - pitch: opts.pitch || 1, - volume: opts.volume || 1, +const useSpeech = (text: string, options: ISpeechOptions): ISpeechState => { + let mounted = useRef(false); + const [state, setState] = useState(() => { + const { lang = 'default', name = '' } = options.voice || {}; + return { + isPlaying: false, + status: Status[Status.init], + lang: options.lang || 'default', + voiceInfo: { lang, name }, + rate: options.rate || 1, + pitch: options.pitch || 1, + volume: options.volume || 1, + }; }); - const uterranceRef = useRef(null); + const handlePlay = useCallback(() => { + if (!mounted.current) { + return; + } + setState((preState) => { + return { ...preState, isPlaying: true, status: Status[Status.play] }; + }); + }, []); - useMount(() => { + const handlePause = useCallback(() => { + if (!mounted.current) { + return; + } + setState((preState) => { + return { ...preState, isPlaying: false, status: Status[Status.pause] }; + }); + }, []); + + const handleEnd = useCallback(() => { + if (!mounted.current) { + return; + } + setState((preState) => { + return { ...preState, isPlaying: false, status: Status[Status.end] }; + }); + }, []); + + useEffect(() => { + mounted.current = true; const utterance = new SpeechSynthesisUtterance(text); - opts.lang && (utterance.lang = opts.lang); - opts.voice && (utterance.voice = opts.voice); - utterance.rate = opts.rate || 1; - utterance.pitch = opts.pitch || 1; - utterance.volume = opts.volume || 1; - utterance.onstart = () => setState({ isPlaying: true }); - utterance.onresume = () => setState({ isPlaying: true }); - utterance.onend = () => setState({ isPlaying: false }); - utterance.onpause = () => setState({ isPlaying: false }); - uterranceRef.current = utterance; - window.speechSynthesis.speak(uterranceRef.current); - }); + options.lang && (utterance.lang = options.lang); + options.voice && (utterance.voice = options.voice); + utterance.rate = options.rate || 1; + utterance.pitch = options.pitch || 1; + utterance.volume = options.volume || 1; + utterance.onstart = handlePlay; + utterance.onpause = handlePause; + utterance.onresume = handlePlay; + utterance.onend = handleEnd; + window.speechSynthesis.speak(utterance); + + return () => { + mounted.current = false; + }; + }, []); return state; }; diff --git a/stories/useSpeech.story.tsx b/stories/useSpeech.story.tsx index 146bb593..e1948e6e 100644 --- a/stories/useSpeech.story.tsx +++ b/stories/useSpeech.story.tsx @@ -3,9 +3,8 @@ import * as React from 'react'; import { useSpeech } from '../src'; import ShowDocs from './util/ShowDocs'; -const voices = window.speechSynthesis.getVoices(); - const Demo = () => { + const voices = window.speechSynthesis.getVoices(); const state = useSpeech('Hello world!', { rate: 0.8, pitch: 0.5, voice: voices[0] }); return
{JSON.stringify(state, null, 2)}
;