Skip to content

Commit

Permalink
✨ feat: Add tts ui components
Browse files Browse the repository at this point in the history
  • Loading branch information
canisminor1990 committed Nov 9, 2023
1 parent b0c00c3 commit 1a37f4f
Show file tree
Hide file tree
Showing 17 changed files with 553 additions and 78 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"not ie <= 10"
],
"dependencies": {
"lodash-es": "^4",
"lodash-es": "^4.17.21",
"microsoft-cognitiveservices-speech-sdk": "^1",
"query-string": "^8",
"ssml-document": "^1",
Expand All @@ -74,7 +74,7 @@
"@commitlint/cli": "^18",
"@lobehub/lint": "latest",
"@lobehub/ui": "latest",
"@types/lodash-es": "^4",
"@types/lodash-es": "^4.17.10",
"@types/node": "^20",
"@types/query-string": "^6",
"@types/react": "^18",
Expand Down
36 changes: 36 additions & 0 deletions src/AudioPlayer/demos/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AudioPlayer } from '@lobehub/tts';
import { StoryBook, useControls, useCreateStore } from '@lobehub/ui';
import { useCallback } from 'react';

export default () => {
const store = useCreateStore();

const { url, ...options }: any = useControls(
{
allowPause: false,
showSlider: true,
showTime: true,
timeRender: {
options: ['text', 'tag'],
value: 'text',
},
timeType: {
options: ['left', 'current', 'combine'],
value: 'left',
},
url: 'https://gw.alipayobjects.com/os/kitchen/lnOJK2yZ0K/sound.mp3',
},
{ store },
);

const Content = useCallback(
() => <AudioPlayer audio={new Audio(url)} {...options} />,
[url, options],
);

return (
<StoryBook levaStore={store}>
<Content />
</StoryBook>
);
};
9 changes: 9 additions & 0 deletions src/AudioPlayer/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
nav: Components
group: UI
title: AudioPlayer
---

## defualt

<code src="./demos/index.tsx" nopadding></code>
104 changes: 104 additions & 0 deletions src/AudioPlayer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ActionIcon, ActionIconProps, Tag } from '@lobehub/ui';
import { Slider } from 'antd';
import { Pause, Play, StopCircle } from 'lucide-react';
import React, { memo, useMemo } from 'react';
import { Flexbox } from 'react-layout-kit';

import { useAudioPlayer } from '@/hooks/useAudioPlayer';
import { secondsToMinutesAndSeconds } from '@/utils/secondsToMinutesAndSeconds';

export interface AudioPlayerProps {
allowPause?: boolean;
audio: HTMLAudioElement;
buttonSize?: ActionIconProps['size'];
className?: string;
showSlider?: boolean;
showTime?: boolean;
style?: React.CSSProperties;
timeRender?: 'tag' | 'text';
timeStyle?: React.CSSProperties;
timeType?: 'left' | 'current' | 'combine';
}

const AudioPlayer = memo<AudioPlayerProps>(
({
style,
timeStyle,
buttonSize,
className,
audio,
allowPause,
timeType = 'left',
showTime = true,
showSlider = true,
timeRender = 'text',
}) => {
const {
isPlaying,
play,
stop,
togglePlayPause,
duration,
setTime,
currentTime,
formatedLeftTime,
formatedCurrentTime,
formatedDuration,
} = useAudioPlayer(audio);

const Time = useMemo(
() => (timeRender === 'tag' ? Tag : (props: any) => <time {...props} />),
[timeRender],
);

return (
<Flexbox
align={'center'}
className={className}
gap={8}
horizontal
style={{ paddingRight: 8, width: '100%', ...style }}
>
{allowPause ? (
<ActionIcon
icon={isPlaying ? Pause : Play}
onClick={togglePlayPause}
size={buttonSize}
style={{ flex: 'none' }}
/>
) : (
<ActionIcon
icon={isPlaying ? StopCircle : Play}
onClick={isPlaying ? stop : play}
size={buttonSize}
style={{ flex: 'none' }}
/>
)}
{showSlider && (
<Slider
max={duration}
min={0}
onChange={(e) => setTime(e)}
style={{ flex: 1 }}
tooltip={{ formatter: secondsToMinutesAndSeconds as any }}
value={currentTime}
/>
)}
{showTime && (
<Time style={{ flex: 'none', ...timeStyle }}>
{timeType === 'left' && formatedLeftTime}
{timeType === 'current' && formatedCurrentTime}
{timeType === 'combine' && (
<span>
{formatedCurrentTime}
<span style={{ opacity: 0.66 }}>{` / ${formatedDuration}`}</span>
</span>
)}
</Time>
)}
</Flexbox>
);
},
);

export default AudioPlayer;
72 changes: 72 additions & 0 deletions src/AudioVisualizer/demos/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { AudioVisualizer, useBlobUrl } from '@lobehub/tts';
import { StoryBook, useControls, useCreateStore } from '@lobehub/ui';
import { useCallback } from 'react';
import { Flexbox } from 'react-layout-kit';

export default () => {
const store = useCreateStore();
const { url }: any = useControls(
{
url: 'https://gw.alipayobjects.com/os/kitchen/lnOJK2yZ0K/sound.mp3',
},
{ store },
);

const barStyle: any = useControls(
{
borderRadius: {
max: 100,
min: 0,
step: 1,
value: 24,
},
count: {
max: 48,
min: 0,
step: 1,
value: 4,
},
gap: {
max: 24,
min: 0,
step: 1,
value: 4,
},
maxHeight: {
max: 480,
min: 0,
step: 1,
value: 144,
},
minHeight: {
max: 480,
min: 0,
step: 1,
value: 48,
},
width: {
max: 480,
min: 0,
step: 1,
value: 48,
},
},
{ store },
);

const { audio, isLoading } = useBlobUrl(url);

const Content = useCallback(() => {
if (!audio?.src || isLoading) return 'Loading...';
audio.play();
return <AudioVisualizer audio={audio} barStyle={barStyle} />;
}, [isLoading, audio, barStyle]);

return (
<StoryBook levaStore={store}>
<Flexbox gap={8}>
<Content />
</Flexbox>
</StoryBook>
);
};
9 changes: 9 additions & 0 deletions src/AudioVisualizer/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
nav: Components
group: UI
title: AudioVisualizer
---

## defualt

<code src="./demos/index.tsx" nopadding></code>
45 changes: 45 additions & 0 deletions src/AudioVisualizer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useTheme } from 'antd-style';
import React, { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import { useAudioVisualizer } from '@/hooks/useAudioVisualizer';

export interface AudioVisualizerProps {
audio: HTMLAudioElement;
barStyle?: {
borderRadius?: number;
count?: number;
gap?: number;
maxHeight?: number;
minHeight?: number;
width?: number;
};
color?: string;
}

const AudioVisualizer = memo<AudioVisualizerProps>(({ color, audio, barStyle }) => {
const { count, width, gap } = { count: 4, gap: 4, width: 48, ...barStyle };
const maxHeight = barStyle?.maxHeight || width * 3;
const minHeight = barStyle?.minHeight || width;
const borderRadius = barStyle?.borderRadius || width / 2;
const theme = useTheme();
const bars = useAudioVisualizer(audio, { count });
return (
<Flexbox align={'center'} gap={gap} horizontal style={{ height: maxHeight }}>
{bars.map((bar, index) => (
<div
key={index}
style={{
background: color || theme.colorPrimary,
borderRadius,
height: minHeight + (bar / 255) * (maxHeight - minHeight),
transition: 'height 50ms cubic-bezier(.2,-0.5,.8,1.5)',
width,
}}
/>
))}
</Flexbox>
);
});

export default AudioVisualizer;
81 changes: 81 additions & 0 deletions src/hooks/useAudioPlayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useCallback, useEffect, useRef, useState } from 'react';

import { secondsToMinutesAndSeconds } from '@/utils/secondsToMinutesAndSeconds';

export const useAudioPlayer = (audio: HTMLAudioElement) => {
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const audioRef = useRef(audio);

useEffect(() => {
if (!audio) return;
const currentAudio = audioRef.current;
const onLoadedMetadata = () => {
setDuration(currentAudio.duration);
};
const onTimeUpdate = () => {
setCurrentTime(currentAudio.currentTime);
};
const onEnded = () => {
setIsPlaying(false);
currentAudio.pause();
currentAudio.currentTime = 0;
setCurrentTime(0);
};

currentAudio.addEventListener('loadedmetadata', onLoadedMetadata);
currentAudio.addEventListener('timeupdate', onTimeUpdate);
currentAudio.addEventListener('ended', onEnded);

return () => {
currentAudio.pause();
currentAudio.load();
currentAudio.removeEventListener('loadedmetadata', onLoadedMetadata);
currentAudio.removeEventListener('timeupdate', onTimeUpdate);
currentAudio.removeEventListener('ended', onEnded);
};
}, [audio]);

const hangleTogglePlayPause = useCallback(() => {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
}, [isPlaying, audioRef]);

const handlePlay = useCallback(() => {
setIsPlaying(true);
audioRef.current.play();
}, [audioRef]);

const handleStop = useCallback(() => {
setIsPlaying(false);
audioRef.current.pause();
audioRef.current.currentTime = 0;
}, [audioRef]);

const setTime = useCallback(
(value: number) => {
setCurrentTime(value);
audioRef.current.currentTime = value;
},
[audioRef],
);

return {
currentTime,
duration,
formatedCurrentTime: secondsToMinutesAndSeconds(currentTime),
formatedDuration: secondsToMinutesAndSeconds(duration),
formatedLeftTime: secondsToMinutesAndSeconds(duration - currentTime),
isPlaying,
leftTime: duration - currentTime,
play: handlePlay,
setTime,
stop: handleStop,
togglePlayPause: hangleTogglePlayPause,
};
};
Loading

0 comments on commit 1a37f4f

Please sign in to comment.