Skip to content

Commit

Permalink
useSpeech hook implemented
Browse files Browse the repository at this point in the history
resolves #9
  • Loading branch information
PunitSoniME committed Jan 10, 2024
1 parent 51f50ca commit 15ff256
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 13 deletions.
24 changes: 24 additions & 0 deletions example/src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react"

import { cn } from "@/lib/utils"

export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"

export { Textarea }
99 changes: 99 additions & 0 deletions example/src/hooks/useSpeech/SpeechComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { lazy, Suspense, useEffect, useState } from 'react';
import { useSpeech } from '../../../../src/';
import { packageName } from '@/lib/utils';
import Muted from '@/common/Muted';
import { Button } from '@/components/ui/button';
import { Textarea } from "@/components/ui/textarea"

const Title = lazy(() => import('@/common/Details/Title'));
const Block = lazy(() => import('@/common/Details/Block'));
const Api = lazy(() => import('@/common/Details/Api'));
const Lead = lazy(() => import('@/common/Lead'));

const hook = 'useSpeech';

const api = [
{ execute: 'loadSpeech', type: 'function', description: 'This function accepts 2 parameters, 1st of is HTMLElement, 2nd one is string, you can pass anyone of them at a time' },
{ execute: 'play', type: 'function', description: 'Call this function to start the speech, before calling this function, make sure you call loadSpeech first' },
{ execute: 'resume', type: 'function', description: 'Call this function to resume the speech' },
{ execute: 'pause', type: 'function', description: 'Call this function to pause the speech' },
{ execute: 'stop', type: 'function', description: 'Call this function to stop the speech' },
{ execute: 'currentPlayingState', type: 'string', description: 'Current playing state among these - initialized | playing | paused | ended | resumed' },
];

export default function SpeechComponent() {

const [text, setText] = useState("Lorem ipsum dolor sit amet consectetur adipisicing elit. Cum nam veritatis incidunt debitis. Repellendus at corrupti odit, nam deleniti cum fugiat temporibus libero suscipit harum fugit aliquid saepe minima ipsam.");
const { play, resume, pause, stop, loadSpeech, currentPlayingState } = useSpeech();

return (
<Suspense fallback={<></>}>

<Title>{hook}</Title>

<Lead className='text-md'>
The <code>{hook}</code> will help you implement speeching facility
</Lead>

<Block title='Usage'>
<div className='flex flex-col gap-5'>
<code>
{`import { ${hook} } from "${packageName}";`}
</code>

<code>
{`const { loadSpeech, play, resume, pause, stop, currentPlayingState } = ${hook}();`}
</code>
</div>
</Block>

<Api api={api} />

<Block title='Example'>

<Textarea
defaultValue={text}
className='mb-4'
onChange={(e) => {
setText(e.target.value);
}} />

<div className="flex gap-3 mb-2">
<Button size="sm" disabled={!(currentPlayingState === 'initialized')}
onClick={() => {
loadSpeech(document.getElementById('text-to-speech-content'), text);
play();
}}>Play</Button>

<Button size="sm" disabled={!(['playing', 'resumed'].includes(currentPlayingState))}
onClick={() => {
pause();
}}>Pause</Button>

<Button size="sm" disabled={currentPlayingState !== 'paused'}
onClick={() => {
resume();
}}>Resume</Button>

<Button size="sm" variant="destructive"
disabled={!(['playing', 'resumed', 'paused'].includes(currentPlayingState))}
onClick={() => {
stop();
}}>Stop</Button>
</div>

<div className='flex justify-between flex-col items-start gap-3'>

<div id="text-to-speech-content"
dangerouslySetInnerHTML={{
__html: text
}}
>
</div>
</div>

</Block>

</Suspense>
)
}
5 changes: 2 additions & 3 deletions example/src/hooks/useWindowFocus/WindowFocusComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { lazy, Suspense } from 'react';
import { useWindowFocus } from '../../../../.';
import { useWindowFocus } from '../../../..';
import { packageName } from '@/lib/utils';
import Muted from '@/common/Muted';
import { CheckCircle } from 'lucide-react';

const Title = lazy(() => import('@/common/Details/Title'));
const Block = lazy(() => import('@/common/Details/Block'));
Expand All @@ -15,7 +14,7 @@ const api = [
{ execute: 'windowFocus', type: 'boolean', description: 'Returns true if your focus is in current window' },
];

export default function WindowDimensionsComponent() {
export default function WindowFocusComponent() {

const windowFocus = useWindowFocus();

Expand Down
2 changes: 2 additions & 0 deletions example/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const PreviousComponent = lazy(() => import('@/hooks/usePrevious/PreviousCompone
const StateWithHistoryComponent = lazy(() => import('@/hooks/useStateWithHistory/StateWithHistoryComponent'));
const StorageComponent = lazy(() => import('@/hooks/useStorage/StorageComponent'));
const AsyncComponent = lazy(() => import('@/hooks/useAsync/AsyncComponent'));
const SpeechComponent = lazy(() => import('@/hooks/useSpeech/SpeechComponent'));

export const hooks: Record<string, any> = {
'useToggle': ToggleComponent,
Expand All @@ -59,6 +60,7 @@ export const hooks: Record<string, any> = {
'useColorBlend': ColorBlendComponent,
'useGroupByFirstLetter': GroupByFirstLetterComponent,
'useScrollToTop': ScrollToTopComponent,
'useSpeech': SpeechComponent
};

export const props = {
Expand Down
62 changes: 62 additions & 0 deletions src/docs/useSpeech.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## useSpeech

### This hook will help you implement speeching facility

<br />

> Example
```jsx
import { useState } from 'react';
import { useSpeech } from 'react-helper-hooks';

export default function SpeechComponent() {
const { loadSpeech, play, resume, pause, stop, currentPlayingState } = useSpeech();
const [text, setText] = useState("");

return (
<div>

<textarea
value={text}
onChange={(e) => {
setText(e.target.value);
}}
></textarea>

<button type="button" disabled={!(currentPlayingState === 'initialized')}
onClick={() => {
loadSpeech(document.getElementById('text-to-speech-content'), text);
play();
}}>Play</button>

<button type="button" disabled={!(['playing', 'resumed'].includes(currentPlayingState))}
onClick={() => {
pause();
}}>Pause</button>

<button type="button" disabled={currentPlayingState !== 'paused'}
onClick={() => {
resume();
}}>Resume</button>

<button type="button"
disabled={!(['playing', 'resumed', 'paused'].includes(currentPlayingState))}
onClick={() => {
stop();
}}>Stop</button>

<br />

<div id="text-to-speech-content"
dangerouslySetInnerHTML={{
__html: text
}}
>
</div>
</div>
)
}


```
4 changes: 2 additions & 2 deletions src/docs/useWindowFocus.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
```jsx

import { useWindowFocus } from 'react-helper-hooks';;
import { useWindowFocus } from 'react-helper-hooks';

export default function WindowFocus() {
export default function WindowFocusComponent() {
const windowFocus = useWindowFocus();

return (
Expand Down
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import useColorBlend from './useColorBlend/useColorBlend';
import useGroupByFirstLetter from './useGroupByFirstLetter/useGroupByFirstLetter';
import useScrollToTop from './useScrollToTop/useScrollToTop';
import useHash from './useHash';
import useSpeech from './useSpeech/useSpeech';

export {
useToggle,
Expand Down Expand Up @@ -49,4 +50,5 @@ export {
useGroupByFirstLetter,
useScrollToTop,
useHash,
useSpeech,
};
90 changes: 90 additions & 0 deletions src/useSpeech/speech.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { PlayingState } from './utils';

export type SpeechEngineOptions = {
onBoundary: (e: SpeechSynthesisEvent) => void;
onEnd: (e: SpeechSynthesisEvent) => void;
onResume: (e: SpeechSynthesisEvent) => void;
onStateUpdate: (state: PlayingState) => void;
};

export type SpeechEngineState = {
utterance: SpeechSynthesisUtterance | null;
config: {
rate: number;
volume: number;
voice: SpeechSynthesisVoice;
};
};

export type SpeechEngine = ReturnType<typeof createSpeechEngine>;

/**
* This speech engine is meant to be a simple adapter for using speech synthesis api.
* This should generally be left for the candidate to use as the speech synthesis apis have a few nuances
* that the candidate might not be familiar with.
*/
const createSpeechEngine = (options: SpeechEngineOptions) => {
const state: SpeechEngineState = {
utterance: null,
config: {
rate: 1,
volume: 1,
voice: window.speechSynthesis.getVoices()[0],
},
};

window.speechSynthesis.onvoiceschanged = (e) => {
state.config.voice = speechSynthesis.getVoices()[0];
};

const load = (text: string) => {
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = state.config.rate;
utterance.volume = state.config.volume;
utterance.voice = state.config.voice;
// set up listeners
utterance.onboundary = (e) => options.onBoundary(e);
utterance.onend = (e) => {
options.onStateUpdate('ended');
options.onEnd(e);
};

// set it up as active utterance
state.utterance = utterance;
};

const play = () => {
if (!state.utterance) throw new Error('No active utterance found to play');
state.utterance.onstart = () => {
options.onStateUpdate('playing');
};
window.speechSynthesis.cancel();
window.speechSynthesis.speak(state.utterance);
};

const pause = () => {
options.onStateUpdate('paused');
window.speechSynthesis.pause();
};

const cancel = () => {
options.onStateUpdate('initialized');
window.speechSynthesis.cancel();
};

const resume = () => {
options.onStateUpdate('resumed');
window.speechSynthesis.resume();
};

return {
state,
play,
resume,
pause,
cancel,
load,
};
};

export { createSpeechEngine };
Loading

0 comments on commit 15ff256

Please sign in to comment.