style: simple audio player
This commit is contained in:
@@ -11,9 +11,11 @@ const AudioWrapper = styled.div`
|
||||
|
||||
const AudioElement: React.FC<any> = (props) => {
|
||||
return (
|
||||
<AudioWrapper>
|
||||
<audio {...props}></audio>
|
||||
</AudioWrapper>
|
||||
<div>
|
||||
<AudioWrapper>
|
||||
<audio {...props} controls></audio>
|
||||
</AudioWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
import { formatTime } from '@/utils/index';
|
||||
import { DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Dropdown, Slider, type MenuProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { round } from 'lodash';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo
|
||||
} from 'react';
|
||||
import IconFont from '../icon-font';
|
||||
|
||||
type ActionItem = 'download' | 'delete' | 'speed';
|
||||
|
||||
interface AudioPlayerProps {
|
||||
autoplay?: boolean;
|
||||
url: string;
|
||||
speed?: number;
|
||||
ref?: any;
|
||||
name?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
duration?: number;
|
||||
actions?: ActionItem[];
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
// @ts-ignore
|
||||
const isDarkMode = token.darkMode as boolean;
|
||||
return {
|
||||
wrapper: css`
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 6px 10px;
|
||||
background-color: ${isDarkMode
|
||||
? 'var(--ant-color-fill-secondary)'
|
||||
: '#F1F3F4'};
|
||||
border-radius: 28px;
|
||||
.inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
gap: 10px;
|
||||
.slider {
|
||||
flex: 1;
|
||||
&:hover {
|
||||
.ant-slider-handle {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
&:focus-within {
|
||||
.ant-slider-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-slider-handle {
|
||||
opacity: 0;
|
||||
&::before {
|
||||
background-color: var(--ant-color-bg-spotlight);
|
||||
border-radius: 50%;
|
||||
}
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
};
|
||||
});
|
||||
|
||||
const sliderStyles = {
|
||||
rail: {
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'var(--ant-color-bg-spotlight)'
|
||||
}
|
||||
};
|
||||
|
||||
const speedOptions = [
|
||||
{ label: '1x', value: 1 },
|
||||
{ label: '2x', value: 2 },
|
||||
{ label: '3x', value: 3 },
|
||||
{ label: '4x', value: 4 }
|
||||
];
|
||||
|
||||
const speedConfig = {
|
||||
min: 0.5,
|
||||
max: 2,
|
||||
step: 0.25
|
||||
};
|
||||
|
||||
const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
|
||||
const intl = useIntl();
|
||||
const { styles } = useStyles();
|
||||
const {
|
||||
autoplay = false,
|
||||
speed: defaultSpeed = 1,
|
||||
actions = ['delete'],
|
||||
onDelete
|
||||
} = props;
|
||||
const audioRef = React.useRef<HTMLAudioElement>(null);
|
||||
const [audioState, setAudioState] = React.useState<{
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
}>({
|
||||
currentTime: 0,
|
||||
duration: 0
|
||||
});
|
||||
const [playOn, setPlayOn] = React.useState<boolean>(false);
|
||||
const [speakerOn, setSpeakerOn] = React.useState<boolean>(false);
|
||||
const [volume, setVolume] = React.useState<number>(1);
|
||||
const [speed, setSpeed] = React.useState<number>(defaultSpeed);
|
||||
const timer = React.useRef<any>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
play: () => {
|
||||
audioRef.current?.play();
|
||||
},
|
||||
pause: () => {
|
||||
audioRef.current?.pause();
|
||||
}
|
||||
}));
|
||||
const handleShowVolume = useCallback(() => {
|
||||
setSpeakerOn(!speakerOn);
|
||||
}, [speakerOn]);
|
||||
|
||||
const handleSeepdChange = useCallback((value: number | string) => {
|
||||
setSpeed(value as number);
|
||||
audioRef.current!.playbackRate = value as number;
|
||||
}, []);
|
||||
|
||||
const handleAudioOnPlay = useCallback(() => {
|
||||
timer.current = setInterval(() => {
|
||||
setAudioState((prestate) => {
|
||||
return {
|
||||
currentTime: Math.ceil(audioRef.current?.currentTime || 0),
|
||||
duration:
|
||||
prestate.duration || Math.ceil(audioRef.current?.duration || 0)
|
||||
};
|
||||
});
|
||||
|
||||
if (audioRef.current?.paused || audioRef.current?.ended) {
|
||||
clearInterval(timer.current);
|
||||
setPlayOn(false);
|
||||
setAudioState((prestate: any) => {
|
||||
return {
|
||||
currentTime: audioRef.current?.ended ? 0 : prestate.currentTime,
|
||||
duration: prestate.duration
|
||||
};
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
const handlePlay = useCallback(() => {
|
||||
setPlayOn(!playOn);
|
||||
if (playOn) {
|
||||
audioRef.current?.pause();
|
||||
} else {
|
||||
audioRef.current?.play();
|
||||
}
|
||||
}, [playOn]);
|
||||
|
||||
const handleFormatVolume = (val?: number) => {
|
||||
if (val === undefined) {
|
||||
return `${round(volume * 100)}%`;
|
||||
}
|
||||
return `${round(val * 100)}%`;
|
||||
};
|
||||
|
||||
const handleVolumeChange = useCallback((value: number) => {
|
||||
audioRef.current!.volume = round(value, 2);
|
||||
setVolume(round(value, 2));
|
||||
}, []);
|
||||
|
||||
const initPlayerConfig = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current!.volume = volume;
|
||||
audioRef.current!.playbackRate = speed;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadedMetadata = useCallback(
|
||||
(data: any) => {
|
||||
const duration = Math.ceil(audioRef.current?.duration || 0);
|
||||
setAudioState({
|
||||
currentTime: 0,
|
||||
duration:
|
||||
duration && duration !== Infinity ? duration : props.duration || 0
|
||||
});
|
||||
setPlayOn(autoplay);
|
||||
},
|
||||
[autoplay, props.duration]
|
||||
);
|
||||
|
||||
const handleCurrentChange = useCallback((val: number) => {
|
||||
audioRef.current!.currentTime = val;
|
||||
setAudioState((prestate) => {
|
||||
return {
|
||||
currentTime: val,
|
||||
duration: prestate.duration
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleReduceSpeed = () => {
|
||||
setSpeed((pre) => {
|
||||
if (pre - speedConfig.step < speedConfig.min) {
|
||||
return speedConfig.min;
|
||||
}
|
||||
const next = pre - speedConfig.step;
|
||||
audioRef.current!.playbackRate = next;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddSpeed = () => {
|
||||
setSpeed((pre) => {
|
||||
if (pre + speedConfig.step > speedConfig.max) {
|
||||
return speedConfig.max;
|
||||
}
|
||||
const next = pre + speedConfig.step;
|
||||
audioRef.current!.playbackRate = next;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnLoad = (e: any) => {
|
||||
console.log('onload', e);
|
||||
};
|
||||
|
||||
const onDownload = useCallback(() => {
|
||||
const url = props.url || '';
|
||||
const filename = props.name;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'audio.mp3'; // Default filename
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}, [props.url, props.name]);
|
||||
|
||||
const items: MenuProps['items'] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'download',
|
||||
label: intl.formatMessage({ id: 'common.button.download' }),
|
||||
icon: <DownloadOutlined />,
|
||||
onClick: onDownload
|
||||
},
|
||||
{
|
||||
key: 'speed',
|
||||
label: intl.formatMessage({ id: 'playground.params.speed' }),
|
||||
icon: <IconFont type="icon-play-speed"></IconFont>,
|
||||
children: speedOptions.map((item) => ({
|
||||
key: item.value,
|
||||
label: item.label,
|
||||
onClick: () => handleSeepdChange(item.value)
|
||||
}))
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: intl.formatMessage({ id: 'common.button.delete' }),
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current.src = '';
|
||||
audioRef.current.load();
|
||||
}
|
||||
setAudioState({ currentTime: 0, duration: 0 });
|
||||
setPlayOn(false);
|
||||
onDelete?.();
|
||||
}
|
||||
}
|
||||
].filter((item) => actions.includes(item.key as ActionItem));
|
||||
}, [actions, intl, onDownload, onDelete, handleSeepdChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioRef.current) {
|
||||
initPlayerConfig();
|
||||
}
|
||||
}, [audioRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearInterval(timer.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.wrapper}
|
||||
style={{ width: props.width || '100%', height: props.height || '54px' }}
|
||||
>
|
||||
<div className="inner">
|
||||
<Button
|
||||
size="middle"
|
||||
type="text"
|
||||
onClick={handlePlay}
|
||||
shape="circle"
|
||||
disabled={!audioState?.duration}
|
||||
icon={
|
||||
!playOn ? (
|
||||
<IconFont
|
||||
type="icon-play"
|
||||
style={{ fontSize: '22px' }}
|
||||
></IconFont>
|
||||
) : (
|
||||
<IconFont
|
||||
type="icon-pause"
|
||||
style={{ fontSize: '22px' }}
|
||||
></IconFont>
|
||||
)
|
||||
}
|
||||
></Button>
|
||||
<span className="time current">
|
||||
{formatTime(audioState.currentTime)} /{' '}
|
||||
{formatTime(audioState.duration)}
|
||||
</span>
|
||||
<div className="slider">
|
||||
<Slider
|
||||
tooltip={{ open: false }}
|
||||
min={0}
|
||||
step={1}
|
||||
styles={sliderStyles}
|
||||
max={audioState.duration}
|
||||
value={audioState.currentTime}
|
||||
onChange={handleCurrentChange}
|
||||
/>
|
||||
</div>
|
||||
<Dropdown menu={{ items }} trigger={['click']}>
|
||||
<Button
|
||||
icon={<IconFont type="icon-more"></IconFont>}
|
||||
type="text"
|
||||
size="middle"
|
||||
shape="circle"
|
||||
></Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<audio
|
||||
crossOrigin="anonymous"
|
||||
autoPlay={autoplay}
|
||||
src={props.url}
|
||||
ref={audioRef}
|
||||
preload="metadata"
|
||||
style={{ opacity: 0, position: 'absolute', left: '-9999px' }}
|
||||
onPlay={handleAudioOnPlay}
|
||||
onLoadedMetadata={handleLoadedMetadata}
|
||||
></audio>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default React.memo(AudioPlayer);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
import './iconfont/iconfont.js';
|
||||
// import './iconfont/iconfont.js';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: ''
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_gxrotg9r0p.js'
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -69,6 +69,7 @@ export default {
|
||||
}
|
||||
},
|
||||
token: {
|
||||
darkMode: true,
|
||||
fontFamily:
|
||||
"Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
|
||||
colorText: '#ccc',
|
||||
|
||||
@@ -66,6 +66,7 @@ export default {
|
||||
}
|
||||
},
|
||||
token: {
|
||||
darkMode: false,
|
||||
fontFamily:
|
||||
"Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
|
||||
colorText: 'rgba(0,0,0,1)',
|
||||
|
||||
@@ -7,5 +7,6 @@ export async function queryDashboardData() {
|
||||
}
|
||||
|
||||
export async function queryDashboardUsageData() {
|
||||
return request(`${DASHBOARD_API}/usage`);
|
||||
// return request(`${DASHBOARD_API}/usage`);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import AudioElement from '@/components/audio-player/audio-element';
|
||||
import SimpleAudio from '@/components/audio-player/simple-audio';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import UploadAudio from '@/components/upload-audio';
|
||||
import HotKeys, { KeyMap } from '@/config/hotkeys';
|
||||
@@ -322,14 +322,10 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteAudio = (uid: number | string) => {
|
||||
const list = _.filter(
|
||||
message.audio,
|
||||
(item: MessageItem) => item.uid !== uid
|
||||
);
|
||||
const handleDeleteAudio = () => {
|
||||
setMessage({
|
||||
...message,
|
||||
audio: list
|
||||
audio: []
|
||||
});
|
||||
};
|
||||
|
||||
@@ -517,11 +513,11 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
||||
></ThumbImg>
|
||||
{message.audio && message.audio.length > 0 && (
|
||||
<AudioWrapper>
|
||||
<AudioElement
|
||||
src={message.audio?.[0].data?.url}
|
||||
<SimpleAudio
|
||||
url={message.audio?.[0].data?.url}
|
||||
height={44}
|
||||
onDelete={handleDeleteAudio}
|
||||
controls
|
||||
></AudioElement>
|
||||
></SimpleAudio>
|
||||
</AudioWrapper>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import AudioElement from '@/components/audio-player/audio-element';
|
||||
import SimpleAudio from '@/components/audio-player/simple-audio';
|
||||
import FullMarkdown from '@/components/markdown-viewer/full-markdown';
|
||||
import { Input } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
@@ -17,9 +17,7 @@ import ThinkContent from './think-content';
|
||||
|
||||
const AudioWrapper = styled.div`
|
||||
padding-left: 10px;
|
||||
audio {
|
||||
padding-top: 10px;
|
||||
}
|
||||
padding-top: 10px;
|
||||
`;
|
||||
|
||||
interface MessageBodyProps {
|
||||
@@ -132,6 +130,16 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteAudio = () => {
|
||||
updateMessage?.({
|
||||
role: data.role,
|
||||
content: data.content,
|
||||
uid: data.uid,
|
||||
audio: [],
|
||||
imgs: data.imgs || []
|
||||
});
|
||||
};
|
||||
|
||||
const handleMessageChange = (e: any) => {
|
||||
updateMessage?.({
|
||||
imgs: data.imgs || [],
|
||||
@@ -207,10 +215,11 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
||||
/>
|
||||
{data.audio && data.audio.length > 0 && (
|
||||
<AudioWrapper>
|
||||
<AudioElement
|
||||
src={data.audio?.[0]?.data.url}
|
||||
controls
|
||||
></AudioElement>
|
||||
<SimpleAudio
|
||||
url={data.audio?.[0]?.data.url}
|
||||
actions={[]}
|
||||
height={44}
|
||||
></SimpleAudio>
|
||||
</AudioWrapper>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,10 +245,11 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
||||
/>
|
||||
{data.audio && data.audio.length > 0 && (
|
||||
<AudioWrapper>
|
||||
<AudioElement
|
||||
src={data.audio?.[0]?.data.url}
|
||||
controls
|
||||
></AudioElement>
|
||||
<SimpleAudio
|
||||
url={data.audio?.[0]?.data.url}
|
||||
onDelete={handleDeleteAudio}
|
||||
height={44}
|
||||
></SimpleAudio>
|
||||
</AudioWrapper>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user